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/client/package-lock.json b/client/package-lock.json index c9ddd8a19..f4676e8d4 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 47d7e72e6..dc05642c9 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/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)