+
+
\ No newline at end of file
diff --git a/labcontrol/gui/js_tests/addPlateModal_tests.js b/labcontrol/gui/js_tests/addPlateModal_tests.js
new file mode 100644
index 00000000..4b743336
--- /dev/null
+++ b/labcontrol/gui/js_tests/addPlateModal_tests.js
@@ -0,0 +1,154 @@
+// Requires
+// QUnit
+
+// Library under test:
+// addPlateModal.js
+
+// Tests of function insertPlateModalDiv
+QUnit.module("insertPlateModalDiv", function (hooks) {
+ // Verify correct behavior using default arguments
+ QUnit.test("default arguments", function (assert) {
+ //jquery selector strings
+ let modalIdDivSelectorStr = "body div#addPlateModal";
+ let tableIdSelectorStr = "body table#searchPlateTable";
+
+ // before running function under test, body has no div with
+ // default modal id and no table with default table name
+ assert.equal($(modalIdDivSelectorStr).length, 0);
+ assert.equal($(tableIdSelectorStr).length, 0);
+
+ insertPlateModalDiv();
+
+ // after running function under test, table has these elements
+ assert.equal($(modalIdDivSelectorStr).length, 1);
+ assert.equal($(tableIdSelectorStr).length, 1);
+
+ // NOTE: unlike python unit tests, qunit does not abort the whole
+ // test case when an assert fails. Therefore, the cleanup can
+ // simply be done in-line at the end of the test.
+ // Also note that this clean-up can't be done automatically as the dom
+ // objects were added (intentionally) to body rather than to the
+ // qunit-fixture div.
+ $(modalIdDivSelectorStr).remove();
+ });
+
+ // Verify correct behavior when arguments are explicitly specified
+ QUnit.test("explicit arguments", function (assert) {
+ //explicit names
+ let modalId = 'myModal';
+ let tableId = 'myPlateTable';
+ let parentId = 'qunit-fixture';
+
+ //jquery selector strings
+ let modalIdDivSelectorStr = "#" + parentId + " div#" + modalId;
+ let tableIdSelectorStr = "#" + parentId + " table#" + tableId;
+
+ // before running function under test, qunit-fixture has no div
+ // with explicit modal id and no table with explicit table name
+ assert.equal($(modalIdDivSelectorStr).length, 0);
+ assert.equal($(tableIdSelectorStr).length, 0);
+
+ insertPlateModalDiv(parentId, modalId, tableId);
+
+ // after running function under test, table has these elements
+ assert.equal($(modalIdDivSelectorStr).length, 1);
+ assert.equal($(tableIdSelectorStr).length, 1);
+ });
+});
+
+// Tests of functions that depend on the plate modal div having already
+// been inserted
+QUnit.module("modal-div-dependent tests", function (hooks) {
+ hooks.beforeEach(function (assert) {
+ insertPlateModalDiv("qunit-fixture");
+ });
+
+ // No need for an afterEach hook because the modal div is inserted into
+ // the qunit-fixture div, which QUnit automatically empties after each test
+
+ // Tests of the function populatePlateTable
+ QUnit.module("populatePlateTable", function (hooks) {
+ let qunitTbodySelectorStr = "#qunit-fixture tbody";
+
+ // Verify correct behavior when some realistic plates are input
+ QUnit.test("happy path", function (assert) {
+ let tbodyInnerHtml = '
33
Test plate 4
<' +
+ 'td>2019-05-09 21:21:15.386036' +
+ '
Identification of the Microbiomes for ' +
+ 'Cannabis Soils
' +
+ '
30
Test plate 3
' +
+ '
2019-05-09 21:21:15.386036
Identification ' +
+ 'of the Microbiomes for Cannabis Soils
' +
+ '
' +
+ '
' +
+ '
27
Test plate 2
<' +
+ 'td>2019-05-09 21:21:15.386036' +
+ '
Identification of the Microbiomes for Cannabis ' +
+ 'Soils Some other study
' +
+ '
' +
+ '
21
Test plate 1
' +
+ '
2019-05-09 21:21:15.386036
' +
+ '
Identification of the Microbiomes for Cannabis ' +
+ 'Soils
' +
+ '
';
+ let testPlateInfo = [
+ [21, "Test plate 1",
+ "2019-05-09 21:21:15.386036",
+ ["Identification of the Microbiomes for Cannabis Soils"]
+ ],
+ [27, "Test plate 2", "2019-05-09 21:21:15.386036",
+ ["Identification of the Microbiomes for Cannabis Soils",
+ "Some other study"]
+ ],
+ [30, "Test plate 3", "2019-05-09 21:21:15.386036",
+ ["Identification of the Microbiomes for Cannabis Soils"]
+ ],
+ [33, "Test plate 4", "2019-05-09 21:21:15.386036",
+ ["Identification of the Microbiomes for Cannabis Soils"]
+ ]
+ ];
+
+ // before running function under test, table has no tbody elements
+ assert.equal($(qunitTbodySelectorStr).length, 0);
+
+ populatePlateTable(testPlateInfo, "#searchPlateTable",
+ "addAplate");
+
+ // after running function under test, table has 1 tbody element
+ // containing rows for 4 plates
+ let tbodysList = $($(qunitTbodySelectorStr));
+ assert.equal(tbodysList.length, 1);
+ assert.equal(tbodysList[0].innerHTML, tbodyInnerHtml)
+ });
+
+ // Verify correct behavior when no plates are input
+ QUnit.test("no plates", function (assert) {
+ let tbodyInnerHtml = '
No data available in ' +
+ 'table
';
+
+ // before running function under test, table has no tbody elements
+ assert.equal($(qunitTbodySelectorStr).length, 0);
+
+ populatePlateTable([], "#searchPlateTable", "addAplate");
+
+ // after running function under test, table has 1 tbody element
+ // containing a row saying there are no records
+ let tbodysList = $($(qunitTbodySelectorStr));
+ assert.equal(tbodysList.length, 1);
+ assert.equal(tbodysList[0].innerHTML, tbodyInnerHtml)
+ });
+ });
+});
+
+
diff --git a/labcontrol/gui/static/js/addPlateModal.js b/labcontrol/gui/static/js/addPlateModal.js
index cda231dc..0a055001 100644
--- a/labcontrol/gui/static/js/addPlateModal.js
+++ b/labcontrol/gui/static/js/addPlateModal.js
@@ -1,38 +1,131 @@
// NB: Any page importing this script MUST define a function
// addPlate that takes 1 parameter (the plate id) and does the work
// of actually collecting the necessary info from the plate the
-// user has chosen to add.
+// user has chosen to add for use by the specific page in question.
-// plateTypeList: a list of the types of plates to include in the add plate
-// modal; an acceptable value for plateTypeList would be, e.g.,
+// Additionally, this script requires the following javascript libraries:
+// jquery
+// datatables
+
+// This method creates the plate modal div that is filled and then
+// shown/hidden by other methods in this library
+function insertPlateModalDiv(elementIdToAppendTo = undefined,
+ addPlateModalId = "addPlateModal",
+ plateTableId = "searchPlateTable"){
+ let div = $("
\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ " \n" +
+ "
Select plate(s) to add
\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ " \n" +
+ "
\n" +
+ "
Plate id
\n" +
+ "
Plate name
\n" +
+ "
Creation timestamp
\n" +
+ "
Studies
\n" +
+ "
Add
\n" +
+ "
\n" +
+ " \n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "
\n" +
+ "
");
+
+ let selectorStr = "body";
+ if (elementIdToAppendTo !== undefined){
+ selectorStr = "#" + elementIdToAppendTo
+ }
+
+ div.appendTo($(selectorStr));
+}
+
+// This method makes a get call to retrieve a list of information (specified
+// by the server-side handler, not here) about each
+// plate with the specified type and quantification state.
+// plateTypeList: a list of the types of plates to include results;
+// an acceptable value for plateTypeList would be, e.g.,
// ['gDNA', 'compressed gDNA'].
// getOnlyQuantified: a boolean that is true if the only plates included in
-// the add plate modal should be those that have already been quantified,
+// the results should be those that have already been quantified,
// false if all plates of the relevant types should be included.
-function setUpAddPlateModal(plateTypeList, getOnlyQuantified) {
+function getPlateListPromise(plateTypeList, getOnlyQuantified) {
+ // NB: This does NOT return a list of plate info--instead, it returns a
+ // "promise" that will eventually provide the user access to a list of
+ // plate info--AFTER the asynchronous ajax call is finished, whenever
+ // that turns out to be.
+ return $.ajax({
+ url: '/plate_list',
+ type: 'GET',
+ data: {
+ 'plate_type': JSON.stringify(plateTypeList),
+ 'only_quantified': getOnlyQuantified
+ }
+ })
+ .fail(function(xhr, ajaxOptions, thrownError){
+ // Probably some more detailed error handling is called for, but
+ // a little is better than none.
+ alert("getPlateListPromise ajax call failed with error " +
+ xhr.status + ": '" + thrownError + "'.");
+ })
+}
+
+/*
+* This method creates and populates a DataTables object with the input
+* plateListInfo, and sets it as the contents of the dom object with the
+* JQuery selector tableSelectorStr (which must already exist in the dom).
+* tableSelectorStr is the string specifying the JQuery selector for the table
+* to be populated, e.g. "#searchPlateTable".
+*
+* plateListInfo is a nested list of the format
+* [
+* [21, "Test plate 1", "2019-05-09 21:21:15.386036",
+* ["Identification of the Microbiomes for Cannabis Soils"]
+* ],
+* [27, "Test plate 2", "2019-05-09 21:21:15.386036",
+* ["Identification of the Microbiomes for Cannabis Soils",
+* "Some Other Qiita Study"]
+* ]
+* ]
+* ... where the outer list contains one entry for each plate, which in turn
+* contains four entries: the plate id, the plate external id (e.g., name),
+* the date the plate was created, and a list of the names of all the Qiita
+* studies to which ANY of the samples on the plate belong.
+* specificAddPlateBtnBaseId is the base part--that is, the part that doesn't
+* actually include the plate id--of the the string id for the button one
+* clicks to add a particular, specific plate; e.g., "addBtnPlate".
+*/
+function populatePlateTable(plateListInfo, tableSelectorStr,
+ specificAddPlateBtnBaseId) {
// populate the table of potential plates to add
- var table = $('#searchPlateTable').DataTable({
- 'ajax': {'url': '/plate_list',
- 'data': {
- 'plate_type': JSON.stringify(plateTypeList),
- 'only_quantified': getOnlyQuantified
- }
- },
+ return $(tableSelectorStr).DataTable({
+ 'data': plateListInfo,
'columnDefs': [
{
- 'targets': -1, // last column
- 'data': null,
- 'render': function (data, type, row, meta) {
- var plateId = data[0];
- return "";
+ 'targets': -1, // last column
+ 'data': null,
+ 'render': function (data, type, row, meta) {
+ var plateId = data[0];
+ return "";
}
},
{
- 'targets': -2, // second to last column
- 'data': null,
- 'render': function (data, type, row, meta) {
- // index 3 in row is the list of studies
- return data[3].join(' ');
+ 'targets': -2, // second to last column
+ 'data': null,
+ 'render': function (data, type, row, meta) {
+ // index 3 in row is the list of studies
+ return data[3].join(' ');
}
}
],
@@ -44,19 +137,33 @@ function setUpAddPlateModal(plateTypeList, getOnlyQuantified) {
'order': [[0, "desc"]] // order rows in desc order by plate id
}
);
+}
- // Remove any existing event handler already attached to plate add buttons
- $('#searchPlateTable tbody').off('click', 'button');
-
- // Add function called by clicking on one of the buttons that adds a plate
- $('#searchPlateTable tbody').on('click', 'button', function () {
- var plateId = table.row($(this).parents('tr')).data()[0];
+// This method generates a function that, when called, runs addPlate() for a
+// specific plateId and appropriately enables/disables the add plate modal and
+// buttons. Closures are used to make that function parameterless. The
+// returned function is intended to be used as the onclick action of a button
+// embedded in a row in a DataTables object, and won't act correctly if it
+// is used elsewhere.
+// dataTable is the actual DataTables.js object produced by a call to
+// $(#).DataTable().
+// addPlateModalId is the string id of the div that represents the plate modal,
+// e.g. "addPlateModal".
+// specificAddPlateBtnBaseId is the base part--that is, the part that doesn't
+// actually include the plate id--of the the string id for the button one
+// clicks to add a particular, specific plate; e.g., "addBtnPlate".
+function makeAddSpecificPlateButtonOnclick(dataTable, addPlateModalId,
+ specificAddPlateBtnBaseId) {
+ return function () {
+ let plateId = dataTable.row($(this).parents('tr')).data()[0];
+ let addPlateModalSelector = $("#" + addPlateModalId);
+ let addPlateBtnSelector = $("#" + specificAddPlateBtnBaseId + plateId);
// Hide the modal to add plates
- $('#addPlateModal').modal('hide');
+ addPlateModalSelector.modal('hide');
- // Disable the button to add this particular plate while we try to add it
- $('#addBtnPlate' + plateId).prop('disabled', true);
+ // Disable button to add this particular plate while we try to add it
+ addPlateBtnSelector.prop('disabled', true);
try {
addPlate(plateId);
@@ -64,7 +171,36 @@ function setUpAddPlateModal(plateTypeList, getOnlyQuantified) {
// if adding this plate *failed*, re-enable the add plate button
// for this plate so the user can try again if they fix their
// problem.
- $('#addBtnPlate' + plateId).prop('disabled', false);
+ addPlateBtnSelector.prop('disabled', false);
}
+ }
+}
+
+function setUpAddPlateModal(plateTypeList, getOnlyQuantified,
+ plateTableId = "searchPlateTable",
+ addPlateModalId = "addPlateModal",
+ specificAddPlateBtnBaseId = "addBtnPlate") {
+
+ // NOTA BENE: ajax calls are asynchronous! (synchronous are deprecated)
+ // This means we don't know when the result will come back and we can't do
+ // ANYTHING that depends on this result--i.e., everything in this function
+ // --until we know it is done. Failure handling is not specific to this
+ // function so it is specified in getPlateListPromise.
+ getPlateListPromise(plateTypeList, getOnlyQuantified)
+ .done(function (data) {
+ //The data come back as in a dictionary object named data, which
+ // contains just one entry--also with the key "data"
+ let plateListInfo = (data["data"]);
+ let tableSelectorStr = "#" + plateTableId;
+ let table = populatePlateTable(tableSelectorStr, plateListInfo);
+
+ let tbodySelector = $(tableSelectorStr + ' tbody');
+ // Remove any existing event handler already attached to plate add btns
+ tbodySelector.off('click', 'button');
+
+ // Add function called by clicking on one of the btns that adds a plate
+ let clickAddSpecificPlate = makeAddSpecificPlateButtonOnclick(table,
+ addPlateModalId, specificAddPlateBtnBaseId);
+ tbodySelector.on('click', 'button', clickAddSpecificPlate);
});
}
\ No newline at end of file