diff --git a/api/src/Downstream/BigEPPhasing.php b/api/src/Downstream/BigEPPhasing.php index 3101c6fa2..03d872110 100644 --- a/api/src/Downstream/BigEPPhasing.php +++ b/api/src/Downstream/BigEPPhasing.php @@ -12,7 +12,7 @@ class BigEPPhasing extends DownstreamPlugin { function results() { $dat = array(); - if (array_key_exists('program_id', $this->process['PARAMETERS'])) { + if (array_key_exists('scaling_id', $this->process['PARAMETERS'])) { $parent = $this->_lookup_parent_autoproc(); if ($parent) { $dat['PARENTAUTOPROCPROGRAM'] = $parent['PROCESSINGPROGRAMS']; diff --git a/api/src/Downstream/DownstreamProcessing.php b/api/src/Downstream/DownstreamProcessing.php index b424d4f7b..05b41be71 100644 --- a/api/src/Downstream/DownstreamProcessing.php +++ b/api/src/Downstream/DownstreamProcessing.php @@ -288,6 +288,7 @@ function legacy() { $resp['TYPE'] = $this->friendlyname; $resp['PROCESS'] = $this->process; $resp['MESSAGES'] = $this->process['MESSAGES']; + $resp['DCID'] = $this->process['DCID']; $resp['FEATURES'] = array( 'MAPMODEL' => $this->has_mapmodel, 'IMAGES' => $this->has_images, diff --git a/api/src/Page/Exp.php b/api/src/Page/Exp.php index 88cc5f9bb..7e8887e12 100644 --- a/api/src/Page/Exp.php +++ b/api/src/Page/Exp.php @@ -141,7 +141,7 @@ class Exp extends Page array('/plans/detectors/:DATACOLLECTIONPLANHASDETECTORID', 'patch', '_dp_update_detector'), array('/plans/detectors/:DATACOLLECTIONPLANHASDETECTORID', 'delete', '_dp_remove_detector'), - array('/setup', 'get', '_get_beamline_setups'), + array('/setup(/:BEAMLINESETUPID)', 'get', '_get_beamline_setups'), array('/setup', 'post', '_add_beamline_setup'), array('/setup/:BEAMLINESETUPID', 'patch', '_update_beamline_setup'), @@ -164,7 +164,7 @@ function _detectors() array_push($args, $this->arg('BEAMLINENAME')); } - $tot = $this->db->pq("SELECT count(d.detectorid) as tot + $tot = $this->db->pq("SELECT count(distinct d.detectorid) as tot FROM detector d LEFT OUTER JOIN beamlinesetup bls ON bls.detectorid = d.detectorid WHERE $where", $args); @@ -177,10 +177,24 @@ function _detectors() 'd.detectorid ASC' ); - $rows = $this->db->paginate("SELECT d.detectorid, d.detectortype, d.detectormanufacturer, d.detectorserialnumber, d.sensorthickness, d.detectormodel, d.detectorpixelsizehorizontal, d.detectorpixelsizevertical, d.detectordistancemin, d.detectordistancemax, d.density, d.composition, concat(d.detectormanufacturer,' ',d.detectormodel, ' (',d.detectortype,')') as description, d.detectormaxresolution, d.detectorminresolution, count(distinct dc.datacollectionid) as dcs, count(distinct bls.beamlinesetupid) as blsetups, (SELECT count(distinct dphd.detectorid) FROM DataCollectionPlan_has_Detector dphd WHERE dphd.detectorid = d.detectorid) as dps, GROUP_CONCAT(distinct bls.beamlinename) as beamlines, d.numberofpixelsx, d.numberofpixelsy, d.detectorrollmin, d.detectorrollmax + $rows = $this->db->paginate("SELECT d.detectorid, d.detectortype, d.detectormanufacturer, d.detectorserialnumber, d.sensorthickness, d.detectormodel, d.detectorpixelsizehorizontal, d.detectorpixelsizevertical, d.detectordistancemin, d.detectordistancemax, d.density, d.composition, d.detectormaxresolution, d.detectorminresolution, d.numberofpixelsx, d.numberofpixelsy, d.detectorrollmin, d.detectorrollmax, + concat(d.detectormanufacturer,' ',d.detectormodel, ' (', ifnull(d.detectortype, ''), ')') as description, + count(distinct bls.beamlinesetupid) as blsetups, + GROUP_CONCAT(distinct bls.beamlinename) as beamlines, + COALESCE(datacollections.dcs, 0) as dcs, + COALESCE(datacollectionplans.dps, 0) as dps FROM detector d - LEFT OUTER JOIN datacollection dc ON dc.detectorid = d.detectorid LEFT OUTER JOIN beamlinesetup bls ON bls.detectorid = d.detectorid + LEFT JOIN ( + SELECT detectorid, COUNT(datacollectionid) as dcs + FROM datacollection + GROUP BY detectorid + ) datacollections ON datacollections.detectorid = d.detectorid + LEFT JOIN ( + SELECT detectorid, COUNT(DISTINCT detectorid) as dps + FROM DataCollectionPlan_has_Detector + GROUP BY detectorid + ) datacollectionplans ON datacollectionplans.detectorid = d.detectorid WHERE $where GROUP BY d.detectorid ORDER BY $order", $args); @@ -925,4 +939,4 @@ function _get_experiment_types() $this->_output(array('total' => count($rows), 'data' => $rows)); } -} \ No newline at end of file +} diff --git a/api/src/Page/Processing.php b/api/src/Page/Processing.php index 88c76253e..378e78d9b 100644 --- a/api/src/Page/Processing.php +++ b/api/src/Page/Processing.php @@ -799,7 +799,7 @@ function _get_downstreams($dcid = null, $aid = null) { } $downstreams = $this->db->pq( - "SELECT app.autoprocprogramid, app.processingprograms, pj.automatic, + "SELECT app.autoprocprogramid, app.processingprograms, pj.automatic, pj.datacollectionid as dcid, app.processingstatus, app.processingmessage, app.processingstarttime, app.processingendtime, pj.recipe, pj.comments as processingcomments, dc.imageprefix as dcimageprefix, dc.imagedirectory as dcimagedirectory, diff --git a/api/src/Page/Sample.php b/api/src/Page/Sample.php index 074eede3f..410a5716f 100644 --- a/api/src/Page/Sample.php +++ b/api/src/Page/Sample.php @@ -1966,15 +1966,19 @@ function _ligands() array_push($args, $this->arg('lid')); } - $tot = $this->db->pq("SELECT count(distinct l.ligandid) as tot FROM ligand l INNER JOIN proposal p ON p.proposalid = l.proposalid WHERE $where", $args); - $tot = intval($tot[0]['TOT']); - if ($this->has_arg('s')) { $st = sizeof($args) + 1; - $where .= " AND l.name LIKE CONCAT('%',:" . $st . ", '%')"; - array_push($args, $this->arg('s')); + $where .= " AND ( + l.name LIKE CONCAT('%',:" . $st . ", '%') + OR l.libraryname LIKE CONCAT('%',:" . ($st + 1) . ", '%') + OR l.librarybatchnumber LIKE CONCAT('%',:" . ($st + 2) . ", '%') + OR l.platebarcode LIKE CONCAT('%',:" . ($st + 3) . ", '%') + )"; + array_push($args, $this->arg('s'), $this->arg('s'), $this->arg('s'), $this->arg('s')); } + $tot = $this->db->pq("SELECT count(distinct l.ligandid) as tot FROM ligand l WHERE $where", $args); + $tot = intval($tot[0]['TOT']); $start = 0; $pp = $this->has_arg('per_page') ? $this->arg('per_page') : 15; @@ -1990,12 +1994,18 @@ function _ligands() array_push($args, $end); $order = 'l.ligandid DESC'; - $group = 'l.ligandid'; if ($this->has_arg('sort_by')) { $cols = array( 'NAME' => 'l.name', + 'SMILES' => 'l.smiles', + 'LIBRARYNAME' => 'l.libraryname', + 'LIBRARYBATCHNUMBER' => 'l.librarybatchnumber', + 'PLATEBARCODE' => 'l.platebarcode', + 'SOURCEWELL' => 'l.sourcewell', + 'SCOUNT' => 'scount', + 'DCOUNT' => 'dcount', ); $dir = $this->has_arg('order') ? ($this->arg('order') == 'asc' ? 'ASC' : 'DESC') : 'ASC'; if (array_key_exists($this->arg('sort_by'), $cols)) @@ -2006,7 +2016,6 @@ function _ligands() COUNT(DISTINCT dc.datacollectionid) AS dcount, COUNT(DISTINCT b.blsampleid) AS scount FROM ligand l - INNER JOIN proposal p ON p.proposalid = l.proposalid LEFT OUTER JOIN ligand_has_pdb lhp ON lhp.ligandid = l.ligandid LEFT OUTER JOIN blsample_has_ligand bhl ON bhl.ligandid = l.ligandid LEFT OUTER JOIN blsample b ON b.blsampleid = bhl.blsampleid @@ -2081,29 +2090,51 @@ function _add_ligand() { if (!$this->has_arg('prop')) $this->_error('No proposal specified'); - if (!$this->has_arg('NAME')) - $this->_error('No ligand name'); - $smiles = $this->has_arg('SMILES') ? $this->arg('SMILES') : ''; + $ligs = array(); + $lids = array(); + $libname = $this->has_arg('LIBRARYNAME') ? $this->arg('LIBRARYNAME') : null; $libbatch = $this->has_arg('LIBRARYBATCHNUMBER') ? $this->arg('LIBRARYBATCHNUMBER') : null; $barcode = $this->has_arg('PLATEBARCODE') ? $this->arg('PLATEBARCODE') : null; - $well = $this->has_arg('SOURCEWELL') ? $this->arg('SOURCEWELL') : null; - $chk = $this->db->pq("SELECT name FROM ligand - WHERE proposalid=:1 AND name=:2", array($this->proposalid, $this->arg('NAME'))); - if (sizeof($chk)) - $this->_error('That ligand name already exists in this proposal'); + $json = $this->request['json'] ?? null; + if ($json && is_array($json)) { - $this->db->pq( - 'INSERT INTO ligand (proposalid,name,smiles,libraryname,librarybatchnumber,platebarcode,sourcewell) - VALUES (:1,:2,:3,:4,:5,:6,:7) RETURNING ligandid INTO :id', - array($this->proposalid, $this->arg('NAME'), $smiles, $libname, $libbatch, $barcode, $well) - ); + foreach ($json as $row) { + if (isset($row->NAME)) { + $name = $row->NAME; + $smiles = isset($row->SMILES) ? $row->SMILES : null; + $well = isset($row->SOURCEWELL) ? $row->SOURCEWELL : null; + array_push($ligs, array($this->proposalid, $name, $smiles, $libname, $libbatch, $barcode, $well)); + } + } + } else { + if (!$this->has_arg('NAME')) + $this->_error('No ligand name'); + + $smiles = $this->has_arg('SMILES') ? $this->arg('SMILES') : null; + $well = $this->has_arg('SOURCEWELL') ? $this->arg('SOURCEWELL') : null; - $lid = $this->db->id(); + array_push($ligs, array($this->proposalid, $this->arg('NAME'), $smiles, $libname, $libbatch, $barcode, $well)); + + $chk = $this->db->pq("SELECT name FROM ligand + WHERE proposalid=:1 AND name=:2", array($this->proposalid, $this->arg('NAME'))); + if (sizeof($chk)) + $this->_error('That ligand name already exists in this proposal'); + } + + foreach ($ligs as $lig) { + $this->db->pq( + 'INSERT INTO ligand (proposalid,name,smiles,libraryname,librarybatchnumber,platebarcode,sourcewell) + VALUES (:1,:2,:3,:4,:5,:6,:7) RETURNING ligandid INTO :id', + $lig + ); + + array_push($lids, $this->db->id()); + } - $this->_output(array('LIGANDID' => $lid)); + $this->_output(array('LIGANDIDS' => $lids)); } # ------------------------------------------------------------------------ diff --git a/api/src/Page/Shipment.php b/api/src/Page/Shipment.php index e374f59f8..3d4f67a00 100644 --- a/api/src/Page/Shipment.php +++ b/api/src/Page/Shipment.php @@ -538,10 +538,11 @@ function _add_history() $email_location = $dewar_complete_email_locations[$last_location]; $send_return_email = preg_match($email_location, strtolower($this->arg('LOCATION'))); } - // If dewar status is dispatch-requested - don't change it. + // If dewar status is dispatch-requested or dispatch-booked - don't change it. // This way the dewar can be moved between storage locations and keep the record that a user requested the dispatch // Default status is 'at-facility' - $dewarstatus = strtolower($dew['DEWARSTATUS']) == 'dispatch-requested' ? 'dispatch-requested' : 'at facility'; + $status = strtolower($dew['DEWARSTATUS']); + $dewarstatus = ($status == 'dispatch-requested' || $status == 'dispatch-booked') ? $status : 'at facility'; $dewarlocation = $this->arg('LOCATION'); } else { diff --git a/client/package-lock.json b/client/package-lock.json index a48bd3981..28ceb3494 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -34,6 +34,7 @@ "jquery.flot.tooltip": "^0.9.0", "lodash-es": "^4.17.21", "markdown": "^0.5.0", + "papaparse": "^5.4.1", "plotly.js": "^1.52.2", "portal-vue": "2.1.7", "promise": "^8.0.3", @@ -12573,7 +12574,8 @@ "node_modules/papaparse": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", - "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==", + "license": "MIT" }, "node_modules/param-case": { "version": "2.1.1", diff --git a/client/package.json b/client/package.json index ab7179f75..749045185 100644 --- a/client/package.json +++ b/client/package.json @@ -82,6 +82,7 @@ "jquery.flot.tooltip": "^0.9.0", "lodash-es": "^4.17.21", "markdown": "^0.5.0", + "papaparse": "^5.4.1", "plotly.js": "^1.52.2", "portal-vue": "2.1.7", "promise": "^8.0.3", diff --git a/client/src/files/ligand_template.csv b/client/src/files/ligand_template.csv new file mode 100644 index 000000000..291af8ef0 --- /dev/null +++ b/client/src/files/ligand_template.csv @@ -0,0 +1 @@ +Name,SMILES,Well diff --git a/client/src/js/collections/detectors.js b/client/src/js/collections/detectors.js index fb4df7862..ad6442032 100644 --- a/client/src/js/collections/detectors.js +++ b/client/src/js/collections/detectors.js @@ -9,7 +9,7 @@ define(['backbone.paginator', 'models/detector', 'utils/kvcollection'], function valueAttribute: 'DETECTORID', state: { - pageSize: 5, + pageSize: 9999, }, parseState: function(r, q, state, options) { diff --git a/client/src/js/modules/dc/views/downstreamwrapper.js b/client/src/js/modules/dc/views/downstreamwrapper.js index 97bb31cfd..bc1c918cb 100644 --- a/client/src/js/modules/dc/views/downstreamwrapper.js +++ b/client/src/js/modules/dc/views/downstreamwrapper.js @@ -97,7 +97,7 @@ define(['backbone', 'marionette', var mapButton = null if (this.getOption('links')) { var links = [ - ' Map / Model Viewer', + ' Map / Model Viewer', ' Logs & Files', ' Download Zip', ] diff --git a/client/src/js/modules/fault/views/list.js b/client/src/js/modules/fault/views/list.js index 313d5619a..3763e8f87 100644 --- a/client/src/js/modules/fault/views/list.js +++ b/client/src/js/modules/fault/views/list.js @@ -5,7 +5,7 @@ define(['marionette', 'modules/fault/views/filters', 'views/table', 'utils/table argument: 'FAULTID', }) - + return Marionette.LayoutView.extend({ className: 'content', template: _.template('

Faults

<% if (app.user_can(\'fault_add\')) { %>
Add Fault Report
<% } %>
'), @@ -14,6 +14,7 @@ define(['marionette', 'modules/fault/views/filters', 'views/table', 'utils/table search: true, initialize: function(options) { + const params = this.getOption('params') || {} var columns = [ { name: 'TITLE', label: 'Title', cell: table.HTMLCell, editable: false }, { name: 'STARTTIME', label: 'Time', cell: 'string', editable: false }, @@ -26,23 +27,23 @@ define(['marionette', 'modules/fault/views/filters', 'views/table', 'utils/table { name: 'NAME', label: 'Reporter', cell: 'string', editable: false }, ] - + if (app.mobile()) { _.each([3,4,5,6,8], function(v) { columns[v].renderable = false }) } + + this.table = new TableView({ collection: options.collection, columns: columns, tableClass: 'proposals', filter: 's', search: params.s, loading: true, backgrid: { row: ClickableRow, emptyText: 'No faults found' } }) - this.table = new TableView({ collection: options.collection, columns: columns, tableClass: 'proposals', filter: 's', search: options.params.s, loading: true, backgrid: { row: ClickableRow, emptyText: 'No faults found' } }) - - if (this.getOption('filters')) this.filters = new FilterView({ collection: options.collection, params: this.getOption('params') }) + if (this.getOption('filters')) this.filters = new FilterView({ collection: options.collection, params: params }) }, - + onRender: function() { this.wrap.show(this.table) if (this.getOption('filters')) this.flts.show(this.filters) }, - + onShow: function() { this.table.focusSearch() }, diff --git a/client/src/js/modules/imaging/views/admin/params.js b/client/src/js/modules/imaging/views/admin/params.js index d0518ec6c..c7f35df02 100644 --- a/client/src/js/modules/imaging/views/admin/params.js +++ b/client/src/js/modules/imaging/views/admin/params.js @@ -180,6 +180,11 @@ define(['marionette', var DetectorsLimitsCellStatic = Backgrid.Cell.extend({ + initialize: function(options) { + Backgrid.Cell.prototype.initialize.apply(this, arguments) + this.listenTo(this.model, 'sync change', this.render) + }, + render: function() { this.$el.empty() if (!this.model.isNew()) { @@ -238,9 +243,17 @@ define(['marionette', console.log('model changed', arguments) // Changed Attributes are not being detected correctly. // Therefore we can crudely set all parameters to enforce a PATCH request - if (!m.isNew()) m.save(_.clone(m.attributes), { patch: true }) - }, + if (!m.isNew()) { + m.save(_.clone(m.attributes), { + patch: true, + wait: true, + success: function() { + m.fetch() + } + }); + } + }, onRender: function() { @@ -297,4 +310,4 @@ define(['marionette', }) -}) \ No newline at end of file +}) diff --git a/client/src/js/modules/samples/routes.js b/client/src/js/modules/samples/routes.js index 92e3daaaa..48f06c823 100644 --- a/client/src/js/modules/samples/routes.js +++ b/client/src/js/modules/samples/routes.js @@ -58,6 +58,10 @@ app.addInitializer(function() { app.navigate('/ligands/lid/'+lid) }) + app.on('ligands:viewall', function() { + app.navigate('/ligands') + }) + app.on('phases:view', function(pid) { app.navigate('/phases/pid/'+pid) }) diff --git a/client/src/js/modules/samples/views/ligandadd.js b/client/src/js/modules/samples/views/ligandadd.js index 43fe23169..f1a14d5c0 100644 --- a/client/src/js/modules/samples/views/ligandadd.js +++ b/client/src/js/modules/samples/views/ligandadd.js @@ -1,24 +1,142 @@ -define(['marionette', 'views/form', +define(['marionette', + 'papaparse', + 'views/form', 'models/ligand', 'templates/samples/ligandadd.html'], - function(Marionette, TableView, Ligand, template) { - - + function(Marionette, Papa, TableView, Ligand, template) { + return FormView.extend({ template: template, + ui: { + fileRadio: 'input[name="fileupload"]', + single: '.single', + csv: '.csv', + }, + + events: { + 'change @ui.fileRadio': 'toggleInputMode', + 'change input[type="file"]': 'validateCSV' + }, + + onRender: function() { + this.toggleInputMode() + }, + + toggleInputMode: function() { + let isFileUpload = this.ui.fileRadio.filter(':checked').val() === 'true' + + if (isFileUpload) { + this.ui.single.hide() + this.ui.csv.show() + this.model.validation.NAME.required = false + this.model.validation.LIBRARYNAME.required = true + this.model.validation.LIBRARYBATCHNUMBER.required = true + this.model.validation.PLATEBARCODE.required = true + } else { + this.ui.single.show() + this.ui.csv.hide() + this.model.validation.NAME.required = true + this.model.validation.LIBRARYNAME.required = false + this.model.validation.LIBRARYBATCHNUMBER.required = false + this.model.validation.PLATEBARCODE.required = false + } + }, + + validateCSV: function(e) { + const file = e.target.files[0] + if (!file) return + let seen = new Set() + + Papa.parse(file, { + header: true, + skipEmptyLines: true, + transformHeader: function(h, i) { + if (i === 0) { + seen = new Set() + } + const header = h.trim().toUpperCase() + // Exact matches + if (header === 'NAME') { + seen.add('NAME') + return 'NAME' + } + if (header === 'SMILES') { + seen.add('SMILES') + return 'SMILES' + } + if (header === 'SOURCEWELL' || header === 'SOURCE WELL' || header === 'WELL') { + seen.add('SOURCEWELL') + return 'SOURCEWELL' + } + // Partial matches only return the target if that slot hasn't been taken. + if (header.includes('NAME') && !header.includes('SALT') && !seen.has('NAME')) { + seen.add('NAME') + return 'NAME' + } + if (header.startsWith('SMILE') && !seen.has('SMILES')) { + seen.add('SMILES') + return 'SMILES' + } + if (header.startsWith('WELL') && !header.includes('VOLUME') && !seen.has('SOURCEWELL')) { + seen.add('SOURCEWELL') + return 'SOURCEWELL' + } + return header + }, + complete: (results) => { + let valid = true + if (results.data.length === 0) { + app.alert({ message: 'Invalid CSV: No data found' }) + valid = false + } else if (results.errors.length > 0) { + app.alert({ message: 'Invalid CSV: '+results.errors[0].message }) + valid = false + } + for (let i = 0; i < results.data.length; i++) { + let rowdata = results.data[i] + let rownum = i+2 + if (!rowdata.NAME) { + app.alert({ message: 'Invalid CSV: row '+rownum+' has no NAME value' }) + valid = false + } + if (!rowdata.SMILES) { + app.alert({ message: 'Invalid CSV: row '+rownum+' has no SMILES value' }) + valid = false + } + if (!rowdata.SOURCEWELL) { + app.alert({ message: 'Invalid CSV: row '+rownum+' has no WELL value' }) + valid = false + } + if (!valid) break + } + if (valid) { + this.model.set('json', results.data) + } else { + e.target.value = '' + } + } + }) + }, + createModel: function() { this.model = new Ligand() }, success: function(model, response, options) { console.log('success from ligand add', this.model) - app.trigger('ligands:view', model.get('LIGANDID')) + let ligandid = model.get('LIGANDIDS') + if (ligandid.length === 1) { + app.trigger('ligands:view', ligandid[0]) + } else { + app.message({ title: 'Ligands added successfully', message: ligandid.length+' ligands added successfully' }) + app.trigger('ligands:viewall') + } }, failure: function(model, xhr, options) { console.log(arguments) - json = null + let json = null if (xhr.responseText) { try { json = $.parseJSON(xhr.responseText) diff --git a/client/src/js/templates/samples/ligandadd.html b/client/src/js/templates/samples/ligandadd.html index be7f998fa..de9cf7676 100644 --- a/client/src/js/templates/samples/ligandadd.html +++ b/client/src/js/templates/samples/ligandadd.html @@ -4,8 +4,21 @@

New Ligand

- +
diff --git a/client/src/js/templates/samples/ligandlist.html b/client/src/js/templates/samples/ligandlist.html index 9760895cf..85822f1d7 100644 --- a/client/src/js/templates/samples/ligandlist.html +++ b/client/src/js/templates/samples/ligandlist.html @@ -2,7 +2,7 @@

<%-title%>s

This page lists all <%-title.toLowerCase()%>s associated with the currently selected proposal.

- Add <%-title%> + Add <%-title%>(s)