diff --git a/README.md b/README.md
index 9c5aa8fd3..a7eeb2f8a 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,8 @@ $ git clone https://github.com/DiamondLightSource/SynchWeb
### Customise front end - config.json
An example configuration is provided in client/src/js/config_sample.json
This file should be copied to create a client/src/js/config.json file and edited to customise the application for your site.
+To create your own logo at the top of the page, update the tailwind.config.js header-site-logo.
+The footer logo (ispyb logo image) can also be customised by setting the tailwind.config.js footer-site-logo before building the client.
| Parameter | Description |
| ------ | ------ |
@@ -35,9 +37,11 @@ This file should be copied to create a client/src/js/config.json file and edited
| maintenance_message | Can be used so app serves static page under maintenance periods |
| maintenance | Flag to indicate if client is in maintenance mode|
| ga_ident | Google Analytics id|
-| site_name | Site Name to display in footer |
-| site_link | URL to site home page |
| data_catalogue | Object that includes name and url property for a link to a data catalogue - displayed on the landing page |
+| csv_profile | The csv profile for importing shipments, currently only imca, see src/js/csv/imca.js |
+| enable_exp_plan | Whether to enable editing of experimental plan fields when creating samples |
+| auto_collect_label | Customise the auto collect label from the default 'Automated' |
+| queue_shipment | Allow entire shipment to be queued for automated / mail-in collection |
Site Image can be customised via the tailwind.config.js header-site-logo and footer-site-logo values.
diff --git a/api/docs/definitions/sample-new.yaml b/api/docs/definitions/sample-new.yaml
index 09738ccc2..3a8958861 100644
--- a/api/docs/definitions/sample-new.yaml
+++ b/api/docs/definitions/sample-new.yaml
@@ -31,6 +31,11 @@ properties:
description: Sample comments
example: in 25% PEG4000 50um
+ STAFFCOMMENTS:
+ type: string
+ description: Sample staff comments
+ example: no crystal in loop
+
SPACEGROUP:
type: string
description: Spacegroup
diff --git a/api/docs/dist/spec.json b/api/docs/dist/spec.json
index 4e2e12716..328be053f 100644
--- a/api/docs/dist/spec.json
+++ b/api/docs/dist/spec.json
@@ -3714,6 +3714,11 @@
"description": "Sample comments",
"example": "in 25% PEG4000 50um"
},
+ "STAFFCOMMENTS": {
+ "type": "string",
+ "description": "Sample staff comments",
+ "example": "no crystal in loop"
+ },
"SPACEGROUP": {
"type": "string",
"description": "Spacegroup",
@@ -4018,6 +4023,11 @@
"description": "Sample comments",
"example": "in 25% PEG4000 50um"
},
+ "STAFFCOMMENTS": {
+ "type": "string",
+ "description": "Sample staff comments",
+ "example": "no crystal in loop"
+ },
"SPACEGROUP": {
"type": "string",
"description": "Spacegroup",
diff --git a/api/src/Page/Assign.php b/api/src/Page/Assign.php
index 608295afe..2834fdf9f 100644
--- a/api/src/Page/Assign.php
+++ b/api/src/Page/Assign.php
@@ -7,7 +7,7 @@
class Assign extends Page
{
- public static $arg_list = array('visit' => '\w+\d+-\d+', 'cid' => '\d+', 'did' => '\d+', 'pos' => '\d+', 'bl' => '[\w-]+');
+ public static $arg_list = array('visit' => '\w+\d+-\d+', 'cid' => '\d+', 'did' => '\d+', 'pos' => '\d+', 'bl' => '[\w-]+', 'nodup' => '\d');
public static $dispatch = array(array('/visits(/:visit)', 'get', '_blsr_visits'),
array('/assign', 'get', '_assign'),
@@ -23,54 +23,107 @@ class Assign extends Page
# ------------------------------------------------------------------------
# Assign a container
function _assign() {
- if (!$this->has_arg('visit')) $this->_error('No visit specified');
+ if (!$this->has_arg('visit') && !$this->has_arg('prop')) $this->_error('No visit or prop specified');
if (!$this->has_arg('cid')) $this->_error('No container id specified');
if (!$this->has_arg('pos')) $this->_error('No position specified');
-
+
+ $where = 'c.containerid=:1';
+ $args = array($this->arg('cid'));
+
+ if ($this->has_arg('visit')) {
+ $where .= " AND CONCAT(p.proposalcode, p.proposalnumber, '-', bl.visit_number) LIKE :".(sizeof($args)+1);
+ array_push($args, $this->arg('visit'));
+ } else {
+ $where .= " AND CONCAT(p.proposalcode, p.proposalnumber) LIKE :".(sizeof($args)+1);
+ array_push($args, $this->arg('prop'));
+ }
+
$cs = $this->db->pq("SELECT d.dewarid,bl.beamlinename,c.containerid,c.code FROM container c
INNER JOIN dewar d ON d.dewarid = c.dewarid
INNER JOIN shipping s ON s.shippingid = d.shippingid
INNER JOIN blsession bl ON bl.proposalid = s.proposalid
INNER JOIN proposal p ON s.proposalid = p.proposalid
- WHERE CONCAT(CONCAT(CONCAT(p.proposalcode, p.proposalnumber), '-'), bl.visit_number) LIKE :1 AND c.containerid=:2", array($this->arg('visit'), $this->arg('cid')));
+ WHERE $where", $args);
if (sizeof($cs) > 0) {
$c = $cs[0];
+
+ $bl = $c['BEAMLINENAME'];
+ if ($this->staff) {
+ if ($this->has_arg('bl')) {
+ $bl = $this->arg('bl');
+ }
+ }
+
+ if ($this->has_arg(('nodup'))) {
+ $existing = $this->db->pq("SELECT c.containerid, c.name, CONCAT(p.proposalcode, p.proposalnumber) as prop
+ FROM container c
+ INNER JOIN dewar d ON d.dewarid = c.dewarid
+ INNER JOIN shipping s ON s.shippingid = d.shippingid
+ INNER JOIN proposal p ON s.proposalid = s.proposalid
+ WHERE beamlinelocation=1 AND samplechangerlocation:2", array($bl, $this->arg('pos')));
+
+ if (sizeof($existing)) {
+ $ex = $existing[0];
+ return $this->_error('A container is already a assigned that position: '+$ex[0]['NAME'] + '('+$ex['PROP']+')');
+ }
+ }
+
+
$this->db->pq("UPDATE dewar SET dewarstatus='processing' WHERE dewarid=:1", array($c['DEWARID']));
- $this->db->pq("UPDATE container SET beamlinelocation=:1,samplechangerlocation=:2,containerstatus='processing' WHERE containerid=:3", array($c['BEAMLINENAME'], $this->arg('pos'), $c['CONTAINERID']));
- $this->db->pq("INSERT INTO containerhistory (containerid,status,location,beamlinename) VALUES (:1,:2,:3,:4)", array($c['CONTAINERID'], 'processing', $this->arg('pos'), $c['BEAMLINENAME']));
- $this->_update_history($c['DEWARID'], 'processing', $c['BEAMLINENAME'], $c['CODE'].' => '.$this->arg('pos'));
+ $this->db->pq("UPDATE container SET beamlinelocation=:1,samplechangerlocation=:2,containerstatus='processing' WHERE containerid=:3", array($bl, $this->arg('pos'), $c['CONTAINERID']));
+ $this->db->pq("INSERT INTO containerhistory (containerid,status,location,beamlinename) VALUES (:1,:2,:3,:4)", array($c['CONTAINERID'], 'processing', $this->arg('pos'), $bl));
+ $this->_update_history($c['DEWARID'], 'processing', $bl, $c['CODE'].' => '.$this->arg('pos'));
$this->_output(1);
+ } else {
+ $this->_error('No such container');
}
-
- $this->_output(0);
}
# ------------------------------------------------------------------------
# Unassign a container
function _unassign() {
- if (!$this->has_arg('visit')) $this->_error('No visit specified');
+ if (!$this->has_arg('visit') && !$this->has_arg('prop')) $this->_error('No visit or prop specified');
if (!$this->has_arg('cid')) $this->_error('No container id specified');
-
+
+ $where = 'c.containerid=:1';
+ $args = array($this->arg('cid'));
+
+ if ($this->has_arg('visit')) {
+ $where .= " AND CONCAT(p.proposalcode, p.proposalnumber, '-', bl.visit_number) LIKE :".(sizeof($args)+1);
+ array_push($args, $this->arg('visit'));
+ } else {
+ $where .= " AND CONCAT(p.proposalcode, p.proposalnumber) LIKE :".(sizeof($args)+1);
+ array_push($args, $this->arg('prop'));
+ }
+
$cs = $this->db->pq("SELECT d.dewarid,bl.beamlinename,c.containerid FROM container c
INNER JOIN dewar d ON d.dewarid = c.dewarid
INNER JOIN shipping s ON s.shippingid = d.shippingid
INNER JOIN blsession bl ON bl.proposalid = s.proposalid
INNER JOIN proposal p ON s.proposalid = p.proposalid
- WHERE CONCAT(CONCAT(CONCAT(p.proposalcode, p.proposalnumber), '-'), bl.visit_number) LIKE :1 AND c.containerid=:2", array($this->arg('visit'), $this->arg('cid')));
+ WHERE $where", $args);
if (sizeof($cs) > 0) {
$c = $cs[0];
+
+ $bl = $c['BEAMLINENAME'];
+ if ($this->staff) {
+ if ($this->has_arg('bl')) {
+ $bl = $this->arg('bl');
+ }
+ }
$this->db->pq("UPDATE container SET samplechangerlocation='',beamlinelocation='',containerstatus='at facility' WHERE containerid=:1",array($c['CONTAINERID']));
- $this->db->pq("INSERT INTO containerhistory (containerid,status,beamlinename) VALUES (:1,:2,:3)", array($c['CONTAINERID'], 'at facility', $c['BEAMLINENAME']));
+ $this->db->pq("INSERT INTO containerhistory (containerid,status,beamlinename) VALUES (:1,:2,:3)", array($c['CONTAINERID'], 'at facility', $bl));
//$this->_update_history($c['DEWARID'], 'unprocessing');
$this->_output(1);
+ } else {
+ $this->_error('No such container');
}
- $this->_output(0);
}
@@ -102,10 +155,10 @@ function _deactivate() {
$this->db->pq("UPDATE container SET containerstatus='at facility', samplechangerlocation='', beamlinelocation='' WHERE containerid=:1", array($c['ID']));
$this->db->pq("INSERT INTO containerhistory (containerid,status) VALUES (:1,:2)", array($c['ID'], 'at facility'));
}
- $this->_output(1);
-
+ $this->_output(1);
+ } else {
+ $this->_error('No such dewar');
}
- $this->_output(0);
}
diff --git a/api/src/Page/Sample.php b/api/src/Page/Sample.php
index efde4053d..5d266cd07 100644
--- a/api/src/Page/Sample.php
+++ b/api/src/Page/Sample.php
@@ -64,6 +64,8 @@ class Sample extends Page
'NAME' => '[\w\s-()]+',
'COMMENTS' => '.*',
'SPACEGROUP' => '(\w|\s|\-|\/)+|^$', // Any word character (inc spaces bars and slashes) or empty string
+ 'STAFFCOMMENTS' => '.*',
+
'CELL_A' => '\d+(.\d+)?',
'CELL_B' => '\d+(.\d+)?',
'CELL_C' => '\d+(.\d+)?',
@@ -118,6 +120,9 @@ class Sample extends Page
'MONOCHROMATOR' => '\w+',
'PRESET' => '\d',
'BEAMLINENAME' => '[\w-]+',
+ 'AIMEDRESOLUTION' => '\d+(.\d+)?',
+ 'COLLECTIONMODE' => '\w+',
+ 'PRIORITY' => '\d+',
'queued' => '\d',
'UNQUEUE' => '\d',
@@ -139,8 +144,10 @@ class Sample extends Page
'TYPE' => '\w+',
'BLSAMPLEGROUPSAMPLEID' => '\d+-\d+',
'PLANORDER' => '\d',
+ 'SHIPPINGID' => '\d+',
'SAMPLEGROUPID' => '\d+',
+ 'QUEUESTATUS' => '\w+',
);
@@ -153,6 +160,8 @@ class Sample extends Page
array('/components', 'post', '_add_sample_component'),
array('/components/:scid', 'delete', '_remove_sample_component'),
+ array('/queue/:CONTAINERQUEUESAMPLEID', 'patch', '_update_sample_queue'),
+
array('/sub(/:ssid)(/sid/:sid)', 'get', '_sub_samples'),
array('/sub/:ssid', 'patch', '_update_sub_sample'),
array('/sub/:ssid', 'put', '_update_sub_sample_full'),
@@ -828,6 +837,12 @@ function _samples() {
array_push($args, $this->arg('BLSAMPLEGROUPID'));
}
+ # For a specific shipment
+ if ($this->has_arg('SHIPPINGID')) {
+ $where .= ' AND s.shippingid=:'.(sizeof($args)+1);
+ array_push($args, $this->arg('SHIPPINGID'));
+ }
+
# For a specific container
if ($this->has_arg('cid')) {
$where .= ' AND c.containerid=:'.(sizeof($args)+1);
@@ -915,6 +930,7 @@ function _samples() {
INNER JOIN dewar d ON d.dewarid = c.dewarid
LEFT OUTER JOIN datacollection dc ON b.blsampleid = dc.blsampleid
LEFT OUTER JOIN robotaction r ON r.blsampleid = b.blsampleid AND r.actiontype = 'LOAD'
+ INNER JOIN shipping s ON s.shippingid = d.shippingid
$join WHERE $where", $args);
$tot = intval($tot[0]['TOT']);
@@ -944,9 +960,16 @@ function _samples() {
if (array_key_exists($this->arg('sort_by'), $cols)) $order = $cols[$this->arg('sort_by')].' '.$dir;
}
- $rows = $this->db->paginate("SELECT distinct b.blsampleid, b.crystalid, b.screencomponentgroupid, ssp.blsampleid as parentsampleid, ssp.name as parentsample, b.blsubsampleid, count(distinct si.blsampleimageid) as inspections, CONCAT(p.proposalcode,p.proposalnumber) as prop, b.code, b.location, pr.acronym, pr.proteinid, cr.spacegroup,b.comments,b.name,s.shippingname as shipment,s.shippingid,d.dewarid,d.code as dewar, c.code as container, c.containerid, c.samplechangerlocation as sclocation, count(distinct IF(dc.overlap != 0,dc.datacollectionid,NULL)) as sc, count(distinct IF(dc.overlap = 0 AND dc.axisrange = 0,dc.datacollectionid,NULL)) as gr, count(distinct IF(dc.overlap = 0 AND dc.axisrange > 0,dc.datacollectionid,NULL)) as dc, count(distinct IF(dcg.experimenttype LIKE 'XRF map', dc.datacollectionid, NULL)) as xm, count(distinct IF(dcg.experimenttype LIKE 'XRF spectrum', dc.datacollectionid, NULL)) as xs, count(distinct IF(dcg.experimenttype LIKE 'Energy scan', dc.datacollectionid, NULL)) as es, count(distinct so.screeningid) as ai, count(distinct app.autoprocprogramid) as ap, count(distinct r.robotactionid) as r, round(min(st.rankingresolution),2) as scresolution, max(ssw.completeness) as sccompleteness, round(min(apss.resolutionlimithigh),2) as dcresolution, round(max(apss.completeness),1) as dccompleteness, dp.anomalousscatterer, dp.requiredresolution, cr.cell_a, cr.cell_b, cr.cell_c, cr.cell_alpha, cr.cell_beta, cr.cell_gamma, b.packingfraction, b.dimension1, b.dimension2, b.dimension3, b.shape, cr.theoreticaldensity, cr.name as crystal, pr.name as protein, b.looptype, dp.centringmethod, dp.experimentkind, cq.containerqueueid, TO_CHAR(cq.createdtimestamp, 'DD-MM-YYYY HH24:MI') as queuedtimestamp
+ $rows = $this->db->paginate("SELECT distinct b.blsampleid, b.crystalid, b.screencomponentgroupid, ssp.blsampleid as parentsampleid, ssp.name as parentsample, b.blsubsampleid, count(distinct si.blsampleimageid) as inspections, CONCAT(p.proposalcode,p.proposalnumber) as prop, b.code, b.location, pr.acronym, pr.proteinid, cr.spacegroup,b.comments,
+ b.staffcomments,
+ b.name,s.shippingname as shipment,s.shippingid,d.dewarid,d.code as dewar, c.code as container, c.containerid, c.samplechangerlocation as sclocation, count(distinct IF(dc.overlap != 0,dc.datacollectionid,NULL)) as sc, count(distinct IF(dc.overlap = 0 AND dc.axisrange = 0,dc.datacollectionid,NULL)) as gr, count(distinct IF(dc.overlap = 0 AND dc.axisrange > 0,dc.datacollectionid,NULL)) as dc,
+ count(distinct IF(dcg.experimenttype LIKE 'XRF map', dc.datacollectionid, NULL)) as xm, count(distinct IF(dcg.experimenttype LIKE 'XRF spectrum', dc.datacollectionid, NULL)) as xs, count(distinct IF(dcg.experimenttype LIKE 'Energy scan', dc.datacollectionid, NULL)) as es,
+ count(distinct so.screeningid) as ai, count(distinct app.autoprocprogramid) as ap, count(distinct r.robotactionid) as r, round(min(st.rankingresolution),2) as scresolution, max(ssw.completeness) as sccompleteness, round(min(apss.resolutionlimithigh),2) as dcresolution, round(max(apss.completeness),1) as dccompleteness, dp.anomalousscatterer, dp.requiredresolution, cr.cell_a, cr.cell_b, cr.cell_c, cr.cell_alpha, cr.cell_beta, cr.cell_gamma, b.packingfraction, b.dimension1, b.dimension2, b.dimension3, b.shape, cr.theoreticaldensity, cr.name as crystal, pr.name as protein, b.looptype, dp.centringmethod, dp.experimentkind, cq.containerqueueid, TO_CHAR(cq.createdtimestamp, 'DD-MM-YYYY HH24:MI') as queuedtimestamp
, $cseq $sseq string_agg(cpr.name) as componentnames, string_agg(cpr.density) as componentdensities
- ,string_agg(cpr.proteinid) as componentids, string_agg(cpr.acronym) as componentacronyms, string_agg(cpr.global) as componentglobals, string_agg(chc.abundance) as componentamounts, string_agg(ct.symbol) as componenttypesymbols, b.volume, pct.symbol,ROUND(cr.abundance,3) as abundance, TO_CHAR(b.recordtimestamp, 'DD-MM-YYYY') as recordtimestamp, dp.radiationsensitivity, dp.energy, dp.userpath
+ ,string_agg(cpr.proteinid) as componentids, string_agg(cpr.acronym) as componentacronyms, string_agg(cpr.global) as componentglobals, string_agg(chc.abundance) as componentamounts, string_agg(ct.symbol) as componenttypesymbols, b.volume, pct.symbol,ROUND(cr.abundance,3) as abundance, TO_CHAR(b.recordtimestamp, 'DD-MM-YYYY') as recordtimestamp, dp.radiationsensitivity, dp.energy, dp.userpath,
+ dp.aimedresolution, dp.preferredbeamsizex, dp.preferredbeamsizey, dp.exposuretime, dp.axisstart, dp.axisrange, dp.numberofimages, dp.transmission, dp.collectionmode, dp.priority,
+ GROUP_CONCAT(distinct a.spacegroup SEPARATOR ', ') as dcspacegroup,
+ cqss.status as lastqueuestatus, cq.containerqueueid, cqs.containerqueuesampleid
FROM blsample b
@@ -964,10 +987,16 @@ function _samples() {
INNER JOIN proposal p ON p.proposalid = pr.proposalid
LEFT OUTER JOIN containerqueue cq ON cq.containerid = c.containerid AND cq.completedtimestamp IS NULL
-
+ LEFT OUTER JOIN containerqueuesample cqs ON cqs.blsampleid = b.blsampleid AND cq.containerqueueid = cqs.containerqueueid
+
+ LEFT OUTER JOIN containerqueuesample cqss ON cqss.containerqueuesampleid = (
+ SELECT MAX(containerqueuesampleid) FROM containerqueuesample _cqs WHERE _cqs.blsampleid = b.blsampleid
+ )
+
LEFT OUTER JOIN diffractionplan dp ON dp.diffractionplanid = b.diffractionplanid
LEFT OUTER JOIN datacollection dc ON b.blsampleid = dc.blsampleid
LEFT OUTER JOIN datacollectiongroup dcg ON dc.datacollectiongroupid = dcg.datacollectiongroupid
+ LEFT OUTER JOIN datacollectioncomment dcc ON dc.datacollectionid = dcc.datacollectionid
LEFT OUTER JOIN screening sc ON dc.datacollectionid = sc.datacollectionid
LEFT OUTER JOIN screeningoutput so ON sc.screeningid = so.screeningid
@@ -979,6 +1008,8 @@ function _samples() {
LEFT OUTER JOIN autoprocscaling_has_int aph ON aph.autoprocintegrationid = ap.autoprocintegrationid
LEFT OUTER JOIN autoprocscalingstatistics apss ON apss.autoprocscalingid = aph.autoprocscalingid
LEFT OUTER JOIN autoprocprogram app ON app.autoprocprogramid = ap.autoprocprogramid AND app.processingstatus = 1
+ LEFT OUTER JOIN autoprocscaling aps ON aph.autoprocscalingid = aps.autoprocscalingid
+ LEFT OUTER JOIN autoproc a ON aps.autoprocid = a.autoprocid
LEFT OUTER JOIN blsampleimage si ON b.blsampleid = si.blsampleid
@@ -1039,14 +1070,15 @@ function _update_sample_full() {
if (!sizeof($samp)) $this->_error('No such sample');
else $samp = $samp[0];
- $this->db->pq("UPDATE blsample set name=:1,comments=:2,code=:3,volume=:4,packingfraction=:5,dimension1=:6,dimension2=:7,dimension3=:8,shape=:9,looptype=:10 WHERE blsampleid=:11",
- array($a['NAME'],$a['COMMENTS'],$a['CODE'],$a['VOLUME'],$a['PACKINGFRACTION'],$a['DIMENSION1'],$a['DIMENSION2'],$a['DIMENSION3'],$a['SHAPE'],$a['LOOPTYPE'],$this->arg('sid')));
+ $this->db->pq("UPDATE blsample set name=:1,comments=:2,code=:3,volume=:4,packingfraction=:5,dimension1=:6,dimension2=:7,dimension3=:8,shape=:9,looptype=:10,staffcomments=:11 WHERE blsampleid=:12",
+ array($a['NAME'],$a['COMMENTS'],$a['CODE'],$a['VOLUME'],$a['PACKINGFRACTION'],$a['DIMENSION1'],$a['DIMENSION2'],$a['DIMENSION3'],$a['SHAPE'],$a['LOOPTYPE'],$a['STAFFCOMMENTS'],$this->arg('sid')));
if (array_key_exists('PROTEINID', $a)) {
$this->db->pq("UPDATE crystal set spacegroup=:1,proteinid=:2,cell_a=:3,cell_b=:4,cell_c=:5,cell_alpha=:6,cell_beta=:7,cell_gamma=:8,theoreticaldensity=:9 WHERE crystalid=:10",
array($a['SPACEGROUP'], $a['PROTEINID'], $a['CELL_A'], $a['CELL_B'], $a['CELL_C'], $a['CELL_ALPHA'], $a['CELL_BETA'], $a['CELL_GAMMA'], $a['THEORETICALDENSITY'], $samp['CRYSTALID']));
- $this->db->pq("UPDATE diffractionplan set anomalousscatterer=:1,requiredresolution=:2, experimentkind=:3, centringmethod=:4, radiationsensitivity=:5, energy=:6, userpath=:7 WHERE diffractionplanid=:8",
- array($a['ANOMALOUSSCATTERER'], $a['REQUIREDRESOLUTION'], $a['EXPERIMENTKIND'], $a['CENTRINGMETHOD'], $a['RADIATIONSENSITIVITY'], $a['ENERGY'], $a['USERPATH'], $samp['DIFFRACTIONPLANID']));
+ $this->db->pq("UPDATE diffractionplan set anomalousscatterer=:1,requiredresolution=:2, experimentkind=:3, centringmethod=:4, radiationsensitivity=:5, energy=:6, userpath=:7, aimedresolution=:8, preferredbeamsizex=:9, preferredbeamsizey=:10, exposuretime=:11, axisstart=:12, axisrange=:13, numberofimages=:14, transmission=:15, collectionmode=:16, priority=:17 WHERE diffractionplanid=:18",
+ array($a['ANOMALOUSSCATTERER'], $a['REQUIREDRESOLUTION'], $a['EXPERIMENTKIND'], $a['CENTRINGMETHOD'], $a['RADIATIONSENSITIVITY'], $a['ENERGY'], $a['USERPATH'], $a['AIMEDRESOLUTION'], $a['PREFERREDBEAMSIZEX'], $a['PREFERREDBEAMSIZEY'], $a['EXPOSURETIME'], $a['AXISSTART'], $a['AXISRANGE'], $a['NUMBEROFIMAGES'], $a['TRANSMISSION'], $a['COLLECTIONMODE'], $a['PRIORITY'],
+ $samp['DIFFRACTIONPLANID']));
}
$init_comps = explode(',', $samp['COMPONENTIDS']);
@@ -1071,6 +1103,35 @@ function _update_sample_components($initial, $final, $amounts, $crystalid) {
}
}
+ // Manually update the status of a sample in the queue
+ function _update_sample_queue() {
+ $statuses = array('completed', 'skipped', 'reinspect', 'failed');
+
+ if (!$this->staff) $this->_error('No access');
+ if (!$this->has_arg('prop')) $this->_error('No proposal specified');
+ if (!$this->has_arg('CONTAINERQUEUESAMPLEID')) $this->_error('No sample container queue id specified');
+ if (!$this->has_arg('QUEUESTATUS') || !in_array($this->arg('QUEUESTATUS'), $statuses)) $this->_error('No status specified');
+
+ $chk = $this->db->pq("SELECT s.blsampleid
+ FROM blsample s
+ INNER JOIN containerqueuesample cqs ON cqs.blsampleid = s.blsampleid
+ INNER JOIN container c ON c.containerid = s.containerid
+ INNER JOIN dewar d ON d.dewarid = c.dewarid
+ INNER JOIN shipping sh ON sh.shippingid = d.shippingid
+ WHERE sh.proposalid=:1 AND cqs.containerqueuesampleid=:2",
+ array($this->proposalid, $this->arg('CONTAINERQUEUESAMPLEID')));
+
+ if (!sizeof($chk)) $this->_error('Sample not queued');
+
+ $this->db->pq('UPDATE containerqueuesample SET endtime=CURRENT_TIMESTAMP, status=:1 WHERE containerqueuesampleid=:2',
+ array($this->arg('QUEUESTATUS'), $this->arg('CONTAINERQUEUESAMPLEID')));
+
+ $this->_output(array(
+ 'CONTAINERQUEUESTATUSID' => $this->arg('CONTAINERQUEUESAMPLEID'),
+ 'QUEUESTATUS' => $this->arg('QUEUESTATUS')
+ ));
+ }
+
function _add_sample() {
if (!$this->has_arg('prop')) $this->_error('No proposal specified');
@@ -1131,12 +1192,12 @@ function _prepare_sample_args($s=null) {
if (!$haskey) $this->_error('One or more fields is missing');
- foreach (array('COMMENTS', 'SPACEGROUP', 'CODE', 'ANOMALOUSSCATTERER') as $f) {
+ foreach (array('COMMENTS', 'STAFFCOMMENTS', 'SPACEGROUP', 'CODE', 'ANOMALOUSSCATTERER', 'COLLECTIONMODE') as $f) {
if ($s) $a[$f] = array_key_exists($f, $s) ? $s[$f] : '';
else $a[$f] = $this->has_arg($f) ? $this->arg($f) : '';
}
- foreach (array('CENTRINGMETHOD', 'EXPERIMENTKIND', 'RADIATIONSENSITIVITY', 'SCREENCOMPONENTGROUPID', 'BLSUBSAMPLEID', 'COMPONENTIDS', 'COMPONENTAMOUNTS', 'REQUIREDRESOLUTION', 'CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA', 'VOLUME', 'ABUNDANCE', 'PACKINGFRACTION', 'DIMENSION1', 'DIMENSION2', 'DIMENSION3', 'SHAPE', 'THEORETICALDENSITY', 'LOOPTYPE', 'ENERGY', 'USERPATH') as $f) {
+ foreach (array('CENTRINGMETHOD', 'EXPERIMENTKIND', 'RADIATIONSENSITIVITY', 'SCREENCOMPONENTGROUPID', 'BLSUBSAMPLEID', 'COMPONENTIDS', 'COMPONENTAMOUNTS', 'REQUIREDRESOLUTION', 'AIMEDRESOLUTION', 'CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA', 'VOLUME', 'ABUNDANCE', 'PACKINGFRACTION', 'DIMENSION1', 'DIMENSION2', 'DIMENSION3', 'SHAPE', 'THEORETICALDENSITY', 'LOOPTYPE', 'ENERGY', 'USERPATH', 'PRIORITY', 'PREFERREDBEAMSIZEX', 'PREFERREDBEAMSIZEY', 'EXPOSURETIME', 'AXISSTART', 'AXISRANGE', 'NUMBEROFIMAGES', 'TRANSMISSION') as $f) {
if ($s) $a[$f] = array_key_exists($f, $s) ? $s[$f] : null;
else $a[$f] = $this->has_arg($f) ? $this->arg($f) : null;
}
@@ -1146,8 +1207,8 @@ function _prepare_sample_args($s=null) {
function _do_add_sample($a) {
- $this->db->pq("INSERT INTO diffractionplan (diffractionplanid, requiredresolution, anomalousscatterer, centringmethod, experimentkind, radiationsensitivity, energy, userpath) VALUES (s_diffractionplan.nextval, :1, :2, :3, :4, :5, :6, :7) RETURNING diffractionplanid INTO :id",
- array($a['REQUIREDRESOLUTION'], $a['ANOMALOUSSCATTERER'], $a['CENTRINGMETHOD'], $a['EXPERIMENTKIND'], $a['RADIATIONSENSITIVITY'], $a['ENERGY'], $a['USERPATH']));
+ $this->db->pq("INSERT INTO diffractionplan (diffractionplanid, requiredresolution, anomalousscatterer, centringmethod, experimentkind, radiationsensitivity, energy, userpath, aimedresolution, preferredbeamsizex, preferredbeamsizey, exposuretime, axisstart, axisrange, numberofimages, transmission, collectionmode, priority) VALUES (s_diffractionplan.nextval, :1, :2, :3, :4, :5, :6, :7, :8, :9, :10, :11, :12, :13, :14, :15, :16, :17) RETURNING diffractionplanid INTO :id",
+ array($a['REQUIREDRESOLUTION'], $a['ANOMALOUSSCATTERER'], $a['CENTRINGMETHOD'], $a['EXPERIMENTKIND'], $a['RADIATIONSENSITIVITY'], $a['ENERGY'], $a['USERPATH'], $a['AIMEDRESOLUTION'], $a['PREFERREDBEAMSIZEX'], $a['PREFERREDBEAMSIZEY'], $a['EXPOSURETIME'], $a['AXISSTART'], $a['AXISRANGE'], $a['NUMBEROFIMAGES'], $a['TRANSMISSION'], $a['COLLECTIONMODE'], $a['PRIORITY']));
$did = $this->db->id();
if (!array_key_exists('CRYSTALID', $a)) {
@@ -1429,7 +1490,7 @@ function _update_sample() {
$maxLocation = $this->_get_current_max_dcp_plan_order($this->args['CONTAINERID']);
$maxLocation = sizeof($maxLocation) ? $maxLocation : -1;
- $sfields = array('CODE', 'NAME', 'COMMENTS', 'VOLUME', 'PACKINGFRACTION', 'DIMENSION1', 'DIMENSION2', 'DIMENSION3', 'SHAPE', 'POSITION', 'CONTAINERID', 'LOOPTYPE', 'LOCATION');
+ $sfields = array('CODE', 'NAME', 'COMMENTS', 'STAFFCOMMENTS', 'VOLUME', 'PACKINGFRACTION', 'DIMENSION1', 'DIMENSION2', 'DIMENSION3', 'SHAPE', 'POSITION', 'CONTAINERID', 'LOOPTYPE', 'LOCATION');
foreach ($sfields as $f) {
if ($this->has_arg($f)) {
$this->db->pq("UPDATE blsample SET $f=:1 WHERE blsampleid=:2", array($this->arg($f), $samp['BLSAMPLEID']));
@@ -1451,7 +1512,7 @@ function _update_sample() {
}
}
- $dfields = array('REQUIREDRESOLUTION', 'ANOMALOUSSCATTERER', 'CENTRINGMETHOD', 'EXPERIMENTKIND', 'RADIATIONSENSITIVITY', 'ENERGY', 'USERPATH');
+ $dfields = array('REQUIREDRESOLUTION', 'ANOMALOUSSCATTERER', 'CENTRINGMETHOD', 'EXPERIMENTKIND', 'RADIATIONSENSITIVITY', 'ENERGY', 'USERPATH', 'AIMEDRESOLUTION', 'PREFERREDBEAMSIZEX', 'PREFERREDBEAMSIZEY', 'EXPOSURETIME', 'AXISSTART', 'AXISRANGE', 'NUMBEROFIMAGES', 'TRANSMISSION', 'COLLECTIONMODE', 'PRIORITY');
foreach ($dfields as $f) {
if ($this->has_arg($f)) {
$this->db->pq("UPDATE diffractionplan SET $f=:1 WHERE diffractionplanid=:2", array($this->arg($f), $samp['DIFFRACTIONPLANID']));
@@ -1459,7 +1520,20 @@ function _update_sample() {
}
}
- if($this->has_arg('PLANORDER')) {
+ // Deal with special case (xpdf?)
+ // This method is used to update/patch samples so it does not necessarily have a containerid
+ // Isolate the requirement for a container id here because its only important when updating collection plans
+ $maxLocation = null;
+
+ if($this->has_arg('CONTAINERID') && $this->arg('CONTAINERID') == 0) {
+ $defaultContainerLocation = $this->_get_default_sample_container();
+ $this->args['CONTAINERID'] = $defaultContainerLocation['CONTAINERID'];
+ $this->args['LOCATION'] = $defaultContainerLocation['LOCATION'];
+ $maxLocation = $this->_get_current_max_dcp_plan_order($this->args['CONTAINERID']);
+ $maxLocation = sizeof($maxLocation) ? $maxLocation : -1;
+ }
+
+ if($this->has_arg('PLANORDER') && $maxLocation != null) {
// If we're moving a BLSample to a new container we need to adjust the DCP plan order not to clash with existing plans for samples in the new container
$dcps = $this->db->pq("SELECT dataCollectionPlanId FROM BLSample_has_DataCollectionPlan
WHERE blSampleId = :1", array($this->arg('sid')));
diff --git a/api/src/Page/Shipment.php b/api/src/Page/Shipment.php
index 39eb681e9..1549d2a30 100644
--- a/api/src/Page/Shipment.php
+++ b/api/src/Page/Shipment.php
@@ -87,6 +87,7 @@ class Shipment extends Page
'unassigned' => '[\w-]+',
// Container fields
+ 'REGISTRY' => '([\w-])+',
'DEWARID' => '\d+',
'CAPACITY' => '\d+',
'CONTAINERTYPE' => '\w+',
@@ -124,6 +125,8 @@ class Shipment extends Page
'manifest' => '\d',
'currentuser' => '\d',
+ 'PROPOSALCODE' => '\w+',
+ 'CONTAINERQUEUEID' => '\d+'
);
@@ -131,6 +134,7 @@ class Shipment extends Page
array('/shipments', 'post', '_add_shipment'),
array('/shipments/:sid', 'patch', '_update_shipment'),
array('/send/:sid', 'get', '_send_shipment'),
+ array('/return/:sid', 'get', '_return_shipment'),
array('/countries', 'get', '_get_countries'),
@@ -167,6 +171,7 @@ class Shipment extends Page
array('/containers/:cid', 'patch', '_update_container'),
array('/containers/move', 'get', '_move_container'),
array('/containers/queue', 'get', '_queue_container'),
+ array('/containers/queue/:CONTAINERQUEUEID', 'post', '_update_container_queue'),
array('/containers/barcode/:BARCODE', 'get', '_check_container'),
@@ -1232,6 +1237,41 @@ function _send_shipment() {
$this->_output(1);
}
+
+ function _return_shipment() {
+ if (!$this->has_arg('prop')) $this->_error('No proposal specified');
+ if (!$this->has_arg('sid')) $this->_error('No shipping id specified');
+
+ $ship = $this->db->pq("SELECT s.shippingid
+ FROM shipping s
+ INNER JOIN proposal p ON s.proposalid = p.proposalid
+ WHERE p.proposalid = :1 AND s.shippingid = :2", array($this->proposalid,$this->arg('sid')));
+
+ if (!sizeof($ship)) $this->_error('No such shipment');
+ $ship = $ship[0];
+
+ $containers = $this->db->pq("SELECT c.containerid
+ FROM container c
+ INNER JOIN dewar d ON d.dewarid = c.dewarid
+ INNER JOIN containerqueue cq ON cq.containerid = c.containerid AND cq.completedtimestamp IS NULL
+ WHERE d.shippingid = :1", array($ship['SHIPPINGID']));
+ if (sizeof($containers)) $this->_error('Cannot return shipment, there are still uncompleted queued containers: View ');
+
+ $this->db->pq("UPDATE shipping SET shippingstatus='returned' where shippingid=:1", array($ship['SHIPPINGID']));
+ $this->db->pq("UPDATE dewar SET dewarstatus='returned' where shippingid=:1", array($ship['SHIPPINGID']));
+
+ $dewars = $this->db->pq("SELECT d.dewarid, s.visit_number as vn, s.beamlinename as bl, TO_CHAR(s.startdate, 'DD-MM-YYYY HH24:MI') as startdate
+ FROM dewar d
+ LEFT OUTER JOIN blsession s ON s.sessionid = d.firstexperimentid
+ WHERE d.shippingid=:1", array($ship['SHIPPINGID']));
+ foreach ($dewars as $d) {
+ $this->db->pq("INSERT INTO dewartransporthistory (dewartransporthistoryid,dewarid,dewarstatus,arrivaldate)
+ VALUES (s_dewartransporthistory.nextval,:1,'returned',CURRENT_TIMESTAMP)",
+ array($d['DEWARID']));
+ }
+
+ $this->_output(1);
+ }
# Show and accept terms to use diamonds shipping account
@@ -1327,11 +1367,20 @@ function _get_all_containers() {
}
}
+ if ($this->has_arg('PROPOSALCODE')) {
+ $where .= " AND p.proposalcode LIKE :".(sizeof($args)+1);
+ array_push($args, $this->arg('PROPOSALCODE'));
+ }
if ($this->has_arg('PUCK')) {
$where .= " AND c.containertype LIKE '%puck'";
}
+ # For a specific shipment
+ if ($this->has_arg('SHIPPINGID')) {
+ $where .= ' AND sh.shippingid=:'.(sizeof($args)+1);
+ array_push($args, $this->arg('SHIPPINGID'));
+ }
if ($this->has_arg('did')) {
$where .= ' AND d.dewarid=:'.(sizeof($args)+1);
@@ -1381,11 +1430,24 @@ function _get_all_containers() {
array_push($args, $this->arg('CONTAINERREGISTRYID'));
}
+ if ($this->has_arg('REGISTRY')) {
+ $where .= ' AND reg.barcode = :'.(sizeof($args)+1);
+ array_push($args, $this->arg('REGISTRY'));
+ }
+
if ($this->has_arg('currentuser')) {
$where .= ' AND c.ownerid = :'.(sizeof($args)+1);
array_push($args, $this->user->personid);
}
+ if ($this->has_arg('s')) {
+ $st = sizeof($args) + 1;
+ $where .= " AND (lower(c.code) LIKE lower(CONCAT(CONCAT('%',:".$st."), '%')) OR lower(c.barcode) LIKE lower(CONCAT(CONCAT('%',:".($st+1)."), '%')) OR lower(pe.login) LIKE lower(CONCAT(CONCAT('%',:".($st+2)."), '%')))";
+ array_push($args, $this->arg('s'));
+ array_push($args, $this->arg('s'));
+ array_push($args, $this->arg('s'));
+ }
+
$tot = $this->db->pq("SELECT count(distinct c.containerid) as tot
FROM container c
INNER JOIN dewar d ON d.dewarid = c.dewarid
@@ -1398,18 +1460,12 @@ function _get_all_containers() {
LEFT OUTER JOIN containerinspection ci ON ci.containerid = c.containerid AND ci.state = 'Completed'
LEFT OUTER JOIN containerqueue cq ON cq.containerid = c.containerid AND cq.completedtimestamp IS NULL
LEFT OUTER JOIN containerqueue cq2 ON cq2.containerid = c.containerid AND cq2.completedtimestamp IS NOT NULL
+ LEFT OUTER JOIN containerregistry reg ON reg.containerregistryid = c.containerregistryid
+ LEFT OUTER JOIN person pe ON c.ownerid = pe.personid
$join
WHERE $where
$having", $args);
- $tot = sizeof($tot) ? intval($tot[0]['TOT']) : 0;
-
- if ($this->has_arg('s')) {
- $st = sizeof($args) + 1;
- $where .= " AND (lower(c.code) LIKE lower(CONCAT(CONCAT('%',:".$st."), '%')) OR lower(c.barcode) LIKE lower(CONCAT(CONCAT('%',:".($st+1)."), '%')))";
- array_push($args, $this->arg('s'));
- array_push($args, $this->arg('s'));
- }
-
+ $tot = sizeof($tot) ? intval($tot[0]['TOT']) : 0;
$pp = $this->has_arg('per_page') ? $this->arg('per_page') : 15;
$pg = $this->has_arg('page') ? $this->arg('page')-1 : 0;
@@ -1421,7 +1477,7 @@ function _get_all_containers() {
array_push($args, $start);
array_push($args, $end);
- $order = 'c.bltimestamp DESC';
+ $order = 'c.containerid DESC';
if ($this->has_arg('ty')) {
if ($this->arg('ty') == 'todispose') {
@@ -1436,7 +1492,8 @@ function _get_all_containers() {
if ($this->has_arg('sort_by')) {
$cols = array('NAME' => 'c.code', 'DEWAR' => 'd.code', 'SHIPMENT' => 'sh.shippingname', 'SAMPLES' => 'count(s.blsampleid)', 'SHIPPINGID' =>'sh.shippingid', 'LASTINSPECTION' => 'max(ci.bltimestamp)', 'INSPECTIONS' => 'count(ci.containerinspectionid)',
'DCCOUNT' => 'COUNT(distinct dc.datacollectionid)', 'SUBSAMPLES' => 'count(distinct ss.blsubsampleid)',
- 'LASTQUEUECOMPLETED' => 'max(cq2.completedtimestamp)', 'QUEUEDTIMESTAMP' => 'max(cq.createdtimestamp)'
+ 'LASTQUEUECOMPLETED' => 'max(cq2.completedtimestamp)', 'QUEUEDTIMESTAMP' => 'max(cq.createdtimestamp)',
+ 'BLTIMESTAMP' => 'c.bltimestamp'
);
$dir = $this->has_arg('order') ? ($this->arg('order') == 'asc' ? 'ASC' : 'DESC') : 'ASC';
if (array_key_exists($this->arg('sort_by'), $cols)) $order = $cols[$this->arg('sort_by')].' '.$dir;
@@ -1447,7 +1504,9 @@ function _get_all_containers() {
ses3.beamlinename as firstexperimentbeamline,
pp.name as pipeline,
TO_CHAR(max(cq2.completedtimestamp), 'HH24:MI DD-MM-YYYY') as lastqueuecompleted, TIMESTAMPDIFF('MINUTE', max(cq2.completedtimestamp), max(cq2.createdtimestamp)) as lastqueuedwell,
- c.ownerid, CONCAT(pe.givenname, ' ', pe.familyname) as owner
+ c.ownerid, concat_ws(' ', pe.givenname, pe.familyname) as owner,
+ CONCAT(SUM(IF(dp.collectionmode = 'auto', 1, 0)), 'A, ', SUM(IF(dp.collectionmode = 'manual', 1, 0)), 'M') as modes,
+ lc.cardname
FROM container c
INNER JOIN dewar d ON d.dewarid = c.dewarid
LEFT OUTER JOIN blsession ses3 ON d.firstexperimentid = ses3.sessionid
@@ -1470,6 +1529,9 @@ function _get_all_containers() {
LEFT OUTER JOIN blsession ses ON c.sessionid = ses.sessionid
LEFT OUTER JOIN processingpipeline pp ON c.prioritypipelineid = pp.processingpipelineid
LEFT OUTER JOIN person pe ON c.ownerid = pe.personid
+ LEFT OUTER JOIN diffractionplan dp ON dp.diffractionplanid = s.diffractionplanid
+
+ LEFT OUTER JOIN labcontact lc ON sh.sendinglabcontactid = lc.labcontactid
$join
WHERE $where
@@ -1520,7 +1582,15 @@ function _queue_container() {
$cqid = $chkq[0]['CONTAINERQUEUEID'];
- $this->db->pq("UPDATE containerqueuesample SET containerqueueid = NULL WHERE containerqueueid=:1", array($cqid));
+ // For pucks delete the containerqueuesample items
+ if (stripos($chkc[0]['CONTAINERTYPE'], 'puck') !== false) {
+ $this->db->pq("DELETE FROM containerqueuesample WHERE containerqueueid=:1", array($cqid));
+
+ // For plates we have a pre queued "sample is ready to queue", where containerqueueid is null
+ // so just unset containerqueueid in containerqueuesample
+ } else {
+ $this->db->pq("UPDATE containerqueuesample SET containerqueueid = NULL WHERE containerqueueid=:1", array($cqid));
+ }
$this->db->pq("DELETE FROM containerqueue WHERE containerqueueid=:1", array($cqid));
$this->_output();
@@ -1531,23 +1601,60 @@ function _queue_container() {
$this->db->pq("INSERT INTO containerqueue (containerid, personid) VALUES (:1, :2)", array($this->arg('CONTAINERID'), $this->user->personid));
$qid = $this->db->id();
- $samples = $this->db->pq("SELECT ss.blsubsampleid, cqs.containerqueuesampleid FROM blsubsample ss
- INNER JOIN blsample s ON s.blsampleid = ss.blsampleid
- INNER JOIN container c ON c.containerid = s.containerid
- INNER JOIN dewar d ON d.dewarid = c.dewarid
- INNER JOIN shipping sh ON sh.shippingid = d.shippingid
- INNER JOIN proposal p ON p.proposalid = sh.proposalid
- INNER JOIN containerqueuesample cqs ON cqs.blsubsampleid = ss.blsubsampleid
- WHERE p.proposalid=:1 AND c.containerid=:2 AND cqs.containerqueueid IS NULL AND ss.source='manual'", array($this->proposalid, $this->arg('CONTAINERID')));
-
- foreach ($samples as $s) {
- $this->db->pq("UPDATE containerqueuesample SET containerqueueid=:1 WHERE containerqueuesampleid=:2", array($qid, $s['CONTAINERQUEUESAMPLEID']));
+ // For pucks samples are queued
+ if (stripos($chkc[0]['CONTAINERTYPE'], 'puck') !== false) {
+ $this->_queue_samples($this->arg('CONTAINERID'), $qid);
+
+ // For plates subsamples are queued
+ } else {
+ $subsamples = $this->db->pq("SELECT ss.blsubsampleid, cqs.containerqueuesampleid FROM blsubsample ss
+ INNER JOIN blsample s ON s.blsampleid = ss.blsampleid
+ INNER JOIN container c ON c.containerid = s.containerid
+ INNER JOIN dewar d ON d.dewarid = c.dewarid
+ INNER JOIN shipping sh ON sh.shippingid = d.shippingid
+ INNER JOIN proposal p ON p.proposalid = sh.proposalid
+ INNER JOIN containerqueuesample cqs ON cqs.blsubsampleid = ss.blsubsampleid
+ WHERE p.proposalid=:1 AND c.containerid=:2 AND cqs.containerqueueid IS NULL AND ss.source='manual'", array($this->proposalid, $this->arg('CONTAINERID')));
+
+ foreach ($subsamples as $s) {
+ $this->db->pq("UPDATE containerqueuesample SET containerqueueid=:1 WHERE containerqueuesampleid=:2", array($qid, $s['CONTAINERQUEUESAMPLEID']));
+ }
}
$this->_output(array('CONTAINERQUEUEID' => $qid));
}
}
+ function _queue_samples($cid, $qid) {
+ $samples = $this->db->pq("SELECT s.blsampleid
+ FROM blsample s
+ INNER JOIN container c ON c.containerid = s.containerid
+ INNER JOIN dewar d ON d.dewarid = c.dewarid
+ INNER JOIN shipping sh ON sh.shippingid = d.shippingid
+ INNER JOIN proposal p ON p.proposalid = sh.proposalid
+ WHERE p.proposalid=:1 AND c.containerid=:2",
+ array($this->proposalid, $cid));
+
+ foreach ($samples as $s) {
+ $this->db->pq("INSERT INTO containerqueuesample (blsampleid, containerqueueid) VALUES (:1, :2)", array($s['BLSAMPLEID'], $qid));
+ }
+ }
+
+ # Manually update a container queue status to completed
+ function _update_container_queue() {
+ if (!$this->staff) $this->_error("No access");
+
+ $cq = $this->db->pq("SELECT containerqueueid
+ FROM containerqueue WHERE containerqueueid=:1",
+ array($this->arg('CONTAINERQUEUEID')));
+
+ if (!sizeof($cq)) $this->_error("No such container queue");
+
+ $this->db->pq("UPDATE containerqueue SET completedtimestamp=CURRENT_TIMESTAMP WHERE containerqueueid=:1",
+ array($this->arg('CONTAINERQUEUEID')));
+
+ $this->_output(1);
+ }
# Move Container
function _move_container() {
@@ -1636,6 +1743,11 @@ function _add_container() {
if ($this->has_arg('AUTOMATED')) {
$this->db->pq("INSERT INTO containerqueue (containerid, personid) VALUES (:1, :2)", array($cid, $this->user->personid));
+ $qid = $this->db->id();
+
+ if (stripos($this->arg('CONTAINERTYPE'), 'puck') !== false) {
+ $this->_queue_samples($cid, $qid);
+ }
}
$this->_output(array('CONTAINERID' => $cid));
@@ -1787,10 +1899,11 @@ function _container_registry() {
}
- $tot = $this->db->pq("SELECT count(r.containerregistryid) as tot
+ $tot = $this->db->pq("SELECT count(distinct r.containerregistryid) as tot
FROM containerregistry r
LEFT OUTER JOIN containerregistry_has_proposal rhp on rhp.containerregistryid = r.containerregistryid
LEFT OUTER JOIN proposal p ON p.proposalid = rhp.proposalid
+ LEFT OUTER JOIN container c ON c.containerregistryid = r.containerregistryid
WHERE $where", $args);
$tot = intval($tot[0]['TOT']);
@@ -1816,7 +1929,7 @@ function _container_registry() {
}
$rows = $this->db->paginate("SELECT r.containerregistryid, r.barcode, GROUP_CONCAT(distinct CONCAT(p.proposalcode,p.proposalnumber) SEPARATOR ', ') as proposals, count(distinct c.containerid) as instances, TO_CHAR(r.recordtimestamp, 'DD-MM-YYYY') as recordtimestamp,
- TO_CHAR(max(c.bltimestamp),'DD-MM-YYYY') as lastuse, max(CONCAT(p.proposalcode,p.proposalnumber)) as prop, r.comments, COUNT(distinct cr.containerreportid) as reports
+ TO_CHAR(max(c.bltimestamp),'DD-MM-YYYY') as lastuse, max(CONCAT(p.proposalcode,p.proposalnumber)) as prop, r.comments, COUNT(distinct cr.containerreportid) as reports, c.code as lastname
FROM containerregistry r
LEFT OUTER JOIN containerregistry_has_proposal rhp on rhp.containerregistryid = r.containerregistryid
LEFT OUTER JOIN proposal p ON p.proposalid = rhp.proposalid
diff --git a/client/package.json b/client/package.json
index 666c76b85..a6a35cf6e 100644
--- a/client/package.json
+++ b/client/package.json
@@ -78,6 +78,7 @@
"jquery.flot.tooltip": "^0.9.0",
"luxon": "^1.25.0",
"markdown": "^0.5.0",
+ "papaparse": "^5.2.0",
"plotly.js": "^1.52.2",
"promise": "^8.0.3",
"tailwindcss": "^1.9.5",
diff --git a/client/src/css/partials/_content.scss b/client/src/css/partials/_content.scss
index 2241a7611..2f2e14edb 100644
--- a/client/src/css/partials/_content.scss
+++ b/client/src/css/partials/_content.scss
@@ -603,12 +603,45 @@ ul.status {
&.COMP {
&:before {
- content: "Completed";
+ content: 'Completed'
}
background-color: #87ceeb;
}
+ // Queue statuses
+ &.skipped {
+ &:before {
+ content: 'Skipped'
+ }
+
+ background-color: #fdfd96;
+ }
+
+ &.reinspect {
+ &:before {
+ content: 'Re-inspect'
+ }
+
+ background-color: #ffb347;
+ }
+
+ &.completed {
+ &:before {
+ content: 'Completed'
+ }
+
+ background-color: #77dd77;
+ }
+
+ &.failed {
+ &:before {
+ content: 'Failed'
+ }
+
+ background-color: #ff6961;
+ }
+
&.XS {
&:before {
content: "XRF Spectrum";
@@ -2026,3 +2059,19 @@ ul.messages {
}
}
}
+
+
+.dropimage {
+ color: $content-search-background;
+ padding: 20px;
+ border: 2px dashed $content-search-background;
+ margin: 2% 0;
+ text-align: center;
+ border-radius: 5px;
+
+ &.active {
+ color: $content-header-color;
+ background: $content-dark-background;
+ text-decoration: italic;
+ }
+}
\ No newline at end of file
diff --git a/client/src/css/partials/_tables.scss b/client/src/css/partials/_tables.scss
index fbd9cdbff..04fb97a00 100644
--- a/client/src/css/partials/_tables.scss
+++ b/client/src/css/partials/_tables.scss
@@ -66,7 +66,7 @@ They can be overriden by specific classes below
padding: 5px;
}
- td.extra, th.extra, th.xtal, td.xtal, th.non-xtal, td.non-xtal, th.auto, td.auto {
+ td.extra, th.extra, th.xtal, td.xtal, th.non-xtal, td.non-xtal, th.auto, td.auto, th.dp, td.dp {
display: none;
&.show {
@@ -75,7 +75,7 @@ They can be overriden by specific classes below
}
@media (max-width: $breakpoint-vsmall) {
- td.extra, th.extra,, th.xtal, td.xtal, th.non-xtal, td.non-xtal, th.auto, td.auto {
+ td.extra, th.extra,, th.xtal, td.xtal, th.non-xtal, td.non-xtal, th.auto, td.auto, th.dp, td.dp {
&.show {
display: block;
}
diff --git a/client/src/js/csv/imca.js b/client/src/js/csv/imca.js
new file mode 100644
index 000000000..8abdbbb16
--- /dev/null
+++ b/client/src/js/csv/imca.js
@@ -0,0 +1,92 @@
+define([], function() {
+
+ return {
+ // The csv column names
+ headers: ['Puck', 'Pin', 'Project', 'Priority', 'Mode', 'Notes to Staff', 'Collection strategy', 'Contact person', 'Expected space group', 'Expected Cell Dimensions', 'Expected Resolution', 'Minimum Resolution Required to Collect', 'Recipe', 'Exposure time', 'Image Width', 'Phi', 'Attenuation', 'Aperture', 'Detector Distance', 'Prefix for frames', 'Observed Resolution', 'Comments From Staff', 'Status'],
+
+ // ... and their ISPyB table mapping
+ mapping: ['CONTAINER', 'LOCATION', 'ACRONYM', 'PRIORITY', 'COLLECTIONMODE', 'COMMENTS', 'COMMENTS', 'OWNER', 'SPACEGROUP', 'CELL', 'AIMEDRESOLUTION', 'REQUIREDRESOLUTION', 'RECIPE', 'EXPOSURETIME', 'AXISRANGE', 'AXISROTATION', 'TRANSMISSION', 'PREFERREDBEAMSIZEX', 'DETECTORDISTANCE', 'PREFIX', 'DCRESOLUTION', 'STAFFCOMMENTS', 'STATUS'],
+
+ // Columns to show on the import page
+ columns: {
+ LOCATION: 'Location',
+ PROTEINID: 'Protein',
+ NAME: 'Sample',
+ PRIORITY: 'Priority',
+ COLLECTIONMODE: 'Mode',
+ COMMENTS: 'Comments',
+ SPACEGROUP: 'Spacegroup',
+ CELL: 'Cell',
+ AIMEDRESOLUTION: 'Aimed Res',
+ REQUIREDRESOLUTION: 'Required Res',
+ EXPOSURETIME: 'Exposure (s)',
+ AXISRANGE: 'Axis Osc',
+ NUMBEROFIMAGES: 'No. Images',
+ TRANSMISSION: 'Transmission',
+ PREFERREDBEAMSIZEX: 'Beamsize',
+ },
+
+ // Import transforms
+ transforms: {
+ CELL: function(v, m) {
+ var comps = v.split(/\s+/)
+ _.each(['CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA'], function(ax, i) {
+ if (comps.length > i) m[ax] = comps[i].replace(',', '')
+ })
+ },
+ AXISROTATION: function(v, m) {
+ if (m.AXISRANGE) m.NUMBEROFIMAGES = m.AXISROTATION / m.AXISRANGE
+ },
+ SPACEGROUP: function(v, m) {
+ m.SPACEGROUP = v.replace(/[\(\)]/g, '')
+ },
+ LOCATION: function(v, m) {
+ if (!this.xcount) this.xcount = 1
+ m.NAME = 'x'+(this.xcount++)
+ },
+ COLLECTIONMODE: function(v, m) {
+ m.COLLECTIONMODE = v.toLowerCase()
+ }
+ },
+
+ // Export transforms
+ export: {
+ CELL: function(m) {
+ return `${m.CELL_A}, ${m.CELL_B}, ${m.CELL_C}, ${m.CELL_ALPHA}, ${m.CELL_BETA}, ${m.CELL_GAMMA}`.trim()
+ },
+
+ STATUS: function(m) {
+ var status = 'skipped'
+ if (m.QUEUEDTIMESTAMP) status = 'queued';
+ if (m.R > 0) status = 'received'
+ if (m.DC > 0) status = 'collected'
+
+ return status
+ },
+
+ AXISROTATION: function(m) {
+ return m.AXISRANGE * m.NUMBEROFIMAGES
+ },
+
+ COMMENTS: function(m, h) {
+ var comments = m.COMMENTS.split(' | ')
+ return comments.length > 1 && h == 'Collection strategy' ? comments[1] : comments[0]
+ }
+ },
+
+ exampleCSV: `Puck,Pin,Project,Priority,Mode,Notes to Staff,Collection strategy,Contact person,Expected space group,Expected Cell Dimensions,Expected Resolution,Minimum Resolution Required to Collect,Recipe,Exposure time,Image Width,Phi,Attenuation,Aperture,Detector Distance,Prefix for frames,Observed Resolution,Comments From Staff,Status
+Blue53,1,a,1,Manual,Tricky,Do best you can,Luke,C2,"143.734, 67.095, 76.899, 90, 110.45, 90",1.9-3.5,4,luke-360.rcp,,,,,,,,,,
+Blue53,2,a,1,Manual,Very tricky,New crystals,Luke,C2,140 65 75 90 110 90,1.8-2.4,3.5,,0.1,0.25,,95,5,250,image_,,,
+Blue53,2,a,1,Manual,Very tricky,New crystals,Luke,C2,140 65 75 90 110 90,1.8-2.4,3.5,,0.1,0.25,,95,5,250,image_,,,
+Blue53,3,b,3,Auto,Routine,SeMet,Luke,P2,52.4 39.8 65.0 108.5,1.5,1.7,,0.04,0.25,360,,10,300,,,,
+Blue53,4,c,3,Auto,Rods,Native,Luke,P21,39 69.2 60 90 105.3,1.5,1.7,,0.04,0.25,360,95,20,,image_,,,
+Blue53,5,d,8,,Plates,,Luke,C222,280 45 112 102 90,1.5,1.7,,0.04,0.25,360,95,50,300,image_,,,
+Blue54,1,e,,Auto,,,,P212121,67 82 276,2.1,2.5,,,0.25,180,,10,350,image_,,,
+Blue54,2,e,4,,,,Luke,P2(1)2(1)2(1),67 82 276,,1.7,luke-180.rcp,,,,,,,,,,
+Blue54,3,f,,Auto,,,,P222,,2.1,,,0.04,,180,95,,350,image_,,,
+Blue54,4,g,4,Auto,,,Luke,,,2.1,2.5,,0.04,0.25,180,75,,350,image_,,,
+Blue54,5,h,99,Auto,,,Luke,P222,,2.2,2.5,,0.04,0.25,180,95,,400,image_,,,
+ `
+ }
+
+})
diff --git a/client/src/js/models/sample.js b/client/src/js/models/sample.js
index 850e51c4f..cc6cfa262 100644
--- a/client/src/js/models/sample.js
+++ b/client/src/js/models/sample.js
@@ -60,6 +60,15 @@ define(['backbone', 'collections/components',
DIMENSION2: '',
DIMENSION3: '',
SHAPE: '',
+ AIMEDRESOLUTION: '',
+ COLLECTIONMODE: '',
+ PRIORITY: '',
+ EXPOSURETIME: '',
+ AXISSTART: '',
+ AXISRANGE: '',
+ NUMBEROFIMAGES: '',
+ TRANSMISSION: '',
+ PREFERREDBEAMSIZEX: '',
},
validation: {
@@ -157,6 +166,51 @@ define(['backbone', 'collections/components',
maxLength: 40,
},
+ AIMEDRESOLUTION: {
+ required: false,
+ pattern: 'number',
+ },
+
+ COLLECTIONMODE: {
+ required: false,
+ pattern: 'word',
+ },
+
+ PRIORITY: {
+ required: false,
+ pattern: 'number',
+ },
+
+ EXPOSURETIME: {
+ required: false,
+ pattern: 'number',
+ },
+
+ AXISSTART: {
+ required: false,
+ pattern: 'number',
+ },
+
+ AXISRANGE: {
+ required: false,
+ pattern: 'number',
+ },
+
+ NUMBEROFIMAGES: {
+ required: false,
+ pattern: 'number',
+ },
+
+ TRANSMISSION: {
+ required: false,
+ pattern: 'number',
+ },
+
+ PREFERREDBEAMSIZEX: {
+ required: false,
+ pattern: 'number',
+ },
+
COMPONENTAMOUNTS: function(from_ui, attr, all_values) {
var values = all_values.components.pluck('ABUNDANCE')
diff --git a/client/src/js/modules/assign/controller.js b/client/src/js/modules/assign/controller.js
index a5a5e6a9f..4a97ac323 100644
--- a/client/src/js/modules/assign/controller.js
+++ b/client/src/js/modules/assign/controller.js
@@ -4,7 +4,8 @@ define(['marionette',
'modules/assign/views/selectvisit',
'modules/assign/views/assign',
- ], function(Marionette, Visit, Visits, SelectVisitView, AssignView) {
+ 'modules/assign/views/scanassign',
+ ], function(Marionette, Visit, Visits, SelectVisitView, AssignView, ScanAssignView) {
var bc = { title: 'Assign Containers', url: '/assign' }
@@ -16,7 +17,7 @@ define(['marionette',
var visits = new Visits(null, { queryParams: { next: 1 } })
visits.fetch({
success: function() {
- app.bc.reset([bc]),
+ app.bc.reset([bc])
app.content.show(new SelectVisitView({ collection: visits }))
},
error: function() {
@@ -45,7 +46,18 @@ define(['marionette',
app.message({ title: 'No such visit', message: 'The specified visit doesnt exist' })
}
})
- }
+ },
+
+ // A simple assign page by scanning barcodes
+ scanAssign: function(bl) {
+ if (!app.staff) {
+ app.message({ title: 'No access', message: 'You do not have access to that page' })
+ return
+ }
+
+ app.bc.reset([{ title: 'Assign Containers' }, { title: 'Barcode Scan'}, { title: bl }])
+ app.content.show(new ScanAssignView({ bl: bl }))
+ },
}
app.addInitializer(function() {
diff --git a/client/src/js/modules/assign/router.js b/client/src/js/modules/assign/router.js
index 4b61df45e..d7576902f 100644
--- a/client/src/js/modules/assign/router.js
+++ b/client/src/js/modules/assign/router.js
@@ -6,6 +6,7 @@ define(['marionette', 'modules/assign/controller'], function(Marionette, c) {
appRoutes: {
'assign': 'selectVisit',
'assign/visit/:visit(/page/:page)': 'assignVisit',
+ 'assign/scan/:bl': 'scanAssign',
},
loadEvents: ['assign:visit'],
diff --git a/client/src/js/modules/assign/routes.js b/client/src/js/modules/assign/routes.js
index c8f55e558..869e6c98e 100644
--- a/client/src/js/modules/assign/routes.js
+++ b/client/src/js/modules/assign/routes.js
@@ -7,6 +7,7 @@ import Visits from 'collections/visits'
import SelectVisitView from 'modules/assign/views/selectvisit'
import AssignView from 'modules/assign/views/assign'
+import ScanAssignView from 'modules/assign/views/scanassign'
let bc = { title: 'Assign Containers', url: '/assign' }
@@ -93,7 +94,22 @@ const routes = [
// In either case we can stop the loading animation
app.loading(false)
})
- }
+ },
+ },
+ {
+ path: 'scan/:bl([a-zA-Z0-9_-]+)',
+ name: 'assign-scan',
+ meta: {
+ permission: 'scan_assign'
+ },
+ component: MarionetteView,
+ props: route => ({
+ mview: ScanAssignView,
+ options: {
+ bl: route.params.bl
+ },
+ breadcrumbs: [bc,{ title: 'Assign Containers' }, { title: 'Barcode Scan'}, { title: route.params.bl }]
+ }),
}
]
}
diff --git a/client/src/js/modules/assign/views/scanassign.js b/client/src/js/modules/assign/views/scanassign.js
new file mode 100644
index 000000000..02b8f0641
--- /dev/null
+++ b/client/src/js/modules/assign/views/scanassign.js
@@ -0,0 +1,257 @@
+define(['marionette', 'backbone',
+ 'views/pages',
+ 'collections/containers',
+ 'modules/assign/collections/pucknames',
+ 'modules/shipment/models/containerregistry',
+ 'utils',
+ 'templates/assign/scanassign.html',
+ 'backbone-validation'
+ ], function(Marionette,
+ Backbone,
+ Pages,
+ Containers,
+ PuckNames,
+ ContainerRegistry,
+ utils,
+ template) {
+
+ var ValidatedContainerRegistry = ContainerRegistry.extend({})
+ _.extend(ValidatedContainerRegistry.prototype, Backbone.Validation.mixin);
+
+ var ContainerView = Marionette.CompositeView.extend({
+ template: _.template(' View Container
<%-PROP%>: <%-NAME%> '),
+ className: 'container assigned',
+
+ events: {
+ click: 'unassignContainer'
+ },
+
+ // Unassign Containers
+ unassignContainer: function(e, options) {
+ if ($(e.target).is('a') || $(e.target).is('i')) return;
+
+ console.log('this.beal', this.getOption('bl'))
+ utils.confirm({
+ title: 'Confirm Container Unassignment',
+ content: 'Are you sure you want to unassign "'+this.model.get('NAME')+'" from sample changer position "'+this.model.get('SAMPLECHANGERLOCATION')+'"?',
+ callback: this.doUnAssign.bind(this, options)
+ })
+ },
+
+ doUnAssign: function() {
+ Backbone.ajax({
+ url: app.apiurl+'/assign/unassign',
+ data: {
+ nodup: 1,
+ prop: this.model.get('PROP'), cid: this.model.get('CONTAINERID'), bl: this.getOption('bl')
+ },
+ success: this.unassignUpdateGUI.bind(this),
+ error: function() {
+ app.alert({ message: 'Something went wrong unassigning this container' })
+
+ },
+ })
+ },
+
+ unassignUpdateGUI: function() {
+ this.model.set({ SAMPLECHANGERLOCATION: null })
+ }
+
+ })
+
+
+ // Sample Changer Positions
+ var PositionView = Marionette.CompositeView.extend({
+ template: _.template('<%-id%>
'),
+ className: 'bl_puck',
+
+ childView: ContainerView,
+ childViewContainer: '.ac',
+ childViewOptions: function() {
+ return {
+ bl: this.getOption('bl')
+ }
+ },
+
+ ui: {
+ name: '.name',
+ barcode: 'input[name=barcode]',
+ },
+
+ events: {
+ click: 'focusInput',
+ 'change @ui.barcode': 'findContainer',
+ 'keyup @ui.barcode': 'findContainer',
+ },
+
+ collectionEvents: {
+ 'change reset': 'render',
+ },
+
+ focusInput: function() {
+ this.ui.barcode.focus()
+ },
+
+ findContainer: function() {
+ if (this.ui.barcode.val() && this.validate()) {
+ this.containers.fetch().done(this.assignContainer.bind(this))
+ }
+ },
+
+ validate: function() {
+ var error = this.registryModel.preValidate('BARCODE', this.ui.barcode.val())
+
+ if (error) this.ui.barcode.addClass('ferror').removeClass('fvalid')
+ else this.ui.barcode.removeClass('ferror').addClass('fvalid')
+
+ return error ? false : true
+ },
+
+ assignContainer: function() {
+ if (this.containers.length) {
+ var container = this.containers.at(0)
+
+ utils.confirm({
+ title: 'Confirm Assign Container',
+ content: 'Barcode matched "'+container.get('PROP')+': '+container.get('NAME')+'" from dewar "'+container.get('DEWAR')+'" with owner "'+container.get('OWNER')+'". Do you want to assign this to sample changer position "'+this.model.get('id')+'"?',
+ callback: this.doAssignContainer.bind(this)
+ })
+ } else {
+ app.alert({ message: 'No containers found for barcode: '+this.ui.barcode.val() })
+ }
+
+ },
+
+ doAssignContainer: function() {
+ var container = this.containers.at(0)
+ Backbone.ajax({
+ url: app.apiurl+'/assign/assign',
+ data: {
+ prop: container.get('PROP'),
+ cid: container.get('CONTAINERID'),
+ pos: this.model.get('id'),
+ bl: this.getOption('bl')
+ },
+ success: this.assignUpdateGUI.bind(this),
+ error: function() {
+ app.alert({ message: 'Something went wrong assigning this container' })
+ },
+ })
+ },
+
+ assignUpdateGUI: function() {
+ var container = this.containers.at(0)
+ container.set({ SAMPLECHANGERLOCATION: this.model.get('id').toString() })
+ this.assigned.add(container)
+ },
+
+ getBarcode: function() {
+ return this.ui.barcode.val()
+ },
+
+ initialize: function(options) {
+ this.collection = new Containers()
+ this.assigned = options.assigned
+ this.listenTo(this.assigned, 'change:SAMPLECHANGERLOCATION change sync add remove', this.updateCollection, this)
+ this.updateCollection()
+
+ this.listenTo(this.getOption('pucknames'), 'sync', this.getNameModel)
+
+ this.findContainer = _.debounce(this.findContainer.bind(this), 500)
+
+ this.containers = new Containers()
+ this.containers.queryParams.all = 1
+ this.containers.queryParams.REGISTRY = this.getBarcode.bind(this)
+
+ this.registryModel = new ValidatedContainerRegistry()
+ },
+
+ getNameModel: function() {
+ this.name = this.getOption('pucknames').findWhere({ id: this.model.get('id') })
+ if (this.name) {
+ this.listenTo(this.name, 'change update', this.updateName)
+ this.updateName()
+ }
+ },
+
+ updateName: function() {
+ if (this.name && this.name.get('name')) this.ui.name.text(' - '+this.name.get('name'))
+ },
+
+ updateCollection: function() {
+ this.collection.reset(this.assigned.findWhere({ SAMPLECHANGERLOCATION: this.model.get('id').toString() }))
+ },
+
+ onRender: function() {
+ this.updateName()
+
+ if (this.collection.length > 0) {
+ this.ui.barcode.hide()
+ }
+ },
+
+
+ })
+
+
+ var SampleChangerView = Marionette.CollectionView.extend({
+ className: 'clearfix',
+ childView: PositionView,
+ childViewOptions: function() {
+ return {
+ assigned: this.getOption('assigned'),
+ pucknames: this.getOption('pucknames'),
+ bl: this.getOption('bl'),
+ }
+ }
+ })
+
+
+
+ return Marionette.LayoutView.extend({
+ template: template,
+ className: 'content',
+
+ regions: {
+ rassigned: '.rassigned'
+ },
+
+ templateHelpers: function() {
+ return {
+ bl: this.getOption('bl'),
+ }
+ },
+
+ refresh: function() {
+ this.assigned.fetch()
+ },
+
+ initialize: function() {
+ this.assigned = new Containers(null, { queryParams: { assigned: 1, bl: this.getOption('bl'), all: 1 }, state: { pageSize: 9999 } })
+ this.assigned.fetch()
+
+ this.pucknames = new PuckNames()
+ this.pucknames.state.pageSize = 100
+ this.pucknames.queryParams.bl = this.getOption('bl')
+ this.pucknames.fetch()
+ },
+
+ onShow: function() {
+ var pucks = this.getOption('bl') in app.config.pucks ? app.config.pucks[this.getOption('bl')] : 10
+
+ var positions = new Backbone.Collection(_.map(_.range(1,pucks+1), function(i) { return { id: i } }))
+ this.rassigned.show(new SampleChangerView({
+ collection: positions,
+ assigned: this.assigned,
+ pucknames: this.pucknames,
+ bl: this.getOption('bl')
+ }))
+
+ },
+
+ onDestroy: function() {
+ this.pucknames.stop()
+ },
+ })
+
+})
diff --git a/client/src/js/modules/samples/views/view.js b/client/src/js/modules/samples/views/view.js
index 945810f86..fdb0f0a76 100644
--- a/client/src/js/modules/samples/views/view.js
+++ b/client/src/js/modules/samples/views/view.js
@@ -53,8 +53,16 @@ define(['marionette',
ui: {
comp: 'input[name=COMPONENTID]',
},
+
+ templateHelpers: function() {
+ return {
+ AUTO_LABEL: this.automated_label
+ }
+ },
initialize: function(options) {
+ this.automated_label = app.config.auto_collect_label || 'Automated'
+
Backbone.Validation.bind(this);
this.dcs = new DCCol(null, { queryParams: { sid: this.model.get('BLSAMPLEID'), pp: 5 } })
diff --git a/client/src/js/modules/shipment/controller.js b/client/src/js/modules/shipment/controller.js
index 945391477..521c725d9 100644
--- a/client/src/js/modules/shipment/controller.js
+++ b/client/src/js/modules/shipment/controller.js
@@ -7,7 +7,8 @@ define(['backbone',
'modules/shipment/views/shipments',
'modules/shipment/views/shipment',
'modules/shipment/views/shipmentadd',
-
+ 'modules/shipment/views/fromcsv',
+
'models/container',
'collections/containers',
'modules/shipment/views/container',
@@ -16,6 +17,9 @@ define(['backbone',
'modules/shipment/views/containers',
'modules/imaging/views/queuecontainer',
+ 'modules/shipment/views/queuedcontainers',
+ 'modules/shipment/views/containerreview',
+
'modules/shipment/models/containerregistry',
'modules/shipment/collections/containerregistry',
'modules/shipment/views/containerregistry',
@@ -46,9 +50,10 @@ define(['backbone',
], function(Backbone,
GetView,
- Dewar, Shipment, Shipments,
- ShipmentsView, ShipmentView, ShipmentAddView,
- Container, Containers, ContainerView, ContainerPlateView, /*ContainerAddView,*/ ContainersView, QueueContainerView,
+ Dewar, Shipment, Shipments,
+ ShipmentsView, ShipmentView, ShipmentAddView, ImportFromCSV,
+ Container, Containers, ContainerView, ContainerPlateView, /*ContainerAddView,*/ ContainersView, QueueContainerView,
+ QueuedContainers, ReviewContainer,
ContainerRegistry, ContainersRegistry, ContainerRegistryView, RegisteredContainer,
RegisteredDewar, DewarRegistry, DewarRegView, RegDewarView, RegDewarAddView, DewarRegistryView,
DispatchView, TransferView, Dewars, DewarOverview, ManifestView, DewarStats, CreateAWBView, RebookPickupView,
@@ -116,6 +121,37 @@ define(['backbone',
}
},
+ // Import csv based on selected profile
+ import_csv: function(sid) {
+ if (!app.config.csv_profile) {
+ app.message({ title: 'CSV Import Not Enabled', message: 'Shipment CSV import is not currently enabled'})
+ return
+ }
+
+ var lookup = new ProposalLookup({ field: 'SHIPPINGID', value: sid })
+ lookup.find({
+ success: function() {
+ var shipment = new Shipment({ SHIPPINGID: sid })
+ shipment.fetch({
+ success: function() {
+ app.bc.reset([bc, { title: shipment.get('SHIPPINGNAME') }, { title: 'Import from CSV' }])
+ app.content.show(new ImportFromCSV({ model: shipment, format: 'imca' }))
+ },
+ error: function() {
+ app.bc.reset([bc])
+ app.message({ title: 'No such shipment', message: 'The specified shipment could not be found'})
+ },
+ })
+ },
+
+ error: function() {
+ app.bc.reset([bc, { title: 'No such shipment' }])
+ app.message({ title: 'No such shipment', message: 'The specified shipment could not be found'})
+ }
+ })
+ },
+
+
create_awb: function(sid) {
var shipment = new Shipment({ SHIPPINGID: sid })
shipment.fetch({
@@ -325,6 +361,40 @@ define(['backbone',
})
},
+ queued_containers: function(s, ty, pt, bl, sid, page) {
+ if (!app.staff) {
+ app.message({ title: 'No access', message: 'You do not have access to that page'})
+ return
+ }
+
+ app.bc.reset([bc, { title: 'Queued Containers' }])
+ app.content.show(new QueuedContainers({ params: { s: s, ty: ty, pt: pt, bl: bl, sid: sid, page: page }}))
+ },
+
+ container_review: function(cid) {
+ var lookup = new ProposalLookup({ field: 'CONTAINERID', value: cid })
+ lookup.find({
+ success: function() {
+ var container = new Container({ CONTAINERID: cid })
+ container.fetch({
+ success: function() {
+ app.bc.reset([bc, { title: container.get('SHIPMENT'), url: '/shipments/sid/'+container.get('SHIPPINGID') }, { title: 'Containers' }, { title: 'Review' }, { title: container.get('NAME') }])
+ app.content.show(new ReviewContainer({ model: container }))
+ },
+ error: function() {
+ app.bc.reset([bc, { title: 'No such container' }])
+ app.message({ title: 'No such container', message: 'The specified container could not be found'})
+ },
+ })
+ },
+
+ error: function() {
+ app.bc.reset([bc, { title: 'No such container' }])
+ app.message({ title: 'No such container', message: 'The specified container could not be found'})
+ }
+ })
+ },
+
dewar_registry: function(ty, s, page) {
app.loading()
@@ -488,6 +558,11 @@ define(['backbone',
controller.view_container(cid, iid, sid)
})
+ app.on('container:review', function(cid) {
+ app.navigate('containers/review/'+cid)
+ controller.container_review(cid)
+ })
+
app.on('rdewar:show', function(fc) {
app.navigate('dewars/registry/'+fc)
controller.view_dewar(fc)
diff --git a/client/src/js/modules/shipment/router.js b/client/src/js/modules/shipment/router.js
index 99fc5c71c..3009b7db0 100644
--- a/client/src/js/modules/shipment/router.js
+++ b/client/src/js/modules/shipment/router.js
@@ -9,6 +9,8 @@ define(['utils/lazyrouter'], function(LazyRouter) {
'shipments/awb/sid/:sid': 'create_awb',
'shipments/pickup/sid/:sid': 'rebook_pickup',
+ 'shipments/csv/:sid': 'import_csv',
+
'containers/cid/:cid(/iid/:iid)(/sid/:sid)': 'view_container',
'containers/queue/:cid': 'queue_container',
'containers/add/did/:did': 'add_container',
@@ -17,6 +19,9 @@ define(['utils/lazyrouter'], function(LazyRouter) {
'containers/registry(/ty/:ty)(/s/:s)(/page/:page)': 'container_registry',
'containers/registry/:crid': 'view_rcontainer',
+ 'containers/queued(/s/:s)(/ty/:ty)(/pt/:pt)(/bl/:bl)(/sid/:sid)(/page/:page)': 'queued_containers',
+ 'containers/review/:cid': 'container_review',
+
'dewars(/s/:s)(/page/:page)': 'dewar_list',
'dewars/dispatch/:did': 'dispatch_dewar',
'dewars/transfer/:did': 'transfer_dewar',
@@ -32,7 +37,7 @@ define(['utils/lazyrouter'], function(LazyRouter) {
'migrate': 'migrate',
},
- loadEvents: ['shipments:show', 'shipment:show', 'rcontainer:show', 'rdewar:show'],
+ loadEvents: ['shipments:show', 'shipment:show', 'rcontainer:show', 'rdewar:show', 'container:review'],
loadModule: function(loadedCallback) {
import(/* webpackChunkName: "shipping" */ 'modules/shipment/controller').then(controller => {
diff --git a/client/src/js/modules/shipment/routes.js b/client/src/js/modules/shipment/routes.js
index e96564423..586dfb91e 100644
--- a/client/src/js/modules/shipment/routes.js
+++ b/client/src/js/modules/shipment/routes.js
@@ -11,6 +11,7 @@ import Backbone from 'backbone'
import Shipments from 'collections/shipments.js'
import Shipment from 'models/shipment.js'
+import Container from 'models/container.js'
import Containers from 'collections/containers.js'
import ContainerRegistry from 'modules/shipment/models/containerregistry'
import ContainersRegistry from 'modules/shipment/collections/containerregistry'
@@ -35,6 +36,7 @@ const ManifestView = import(/* webpackChunkName: "shipment" */ 'modules/shipment
const DewarStats = import(/* webpackChunkName: "shipment-stats" */ 'modules/shipment/views/dewarstats')
const DispatchView = import(/*webpackChunkName: "shipment" */ 'modules/shipment/views/dispatch')
const TransferView = import(/*webpackChunkName: "shipment" */ 'modules/shipment/views/transfer')
+const ImportFromCSV = import(/*webpackChunkName: "shipment" */ 'modules/shipment/views/fromcsv')
// In future may want to move these into wrapper components
// Similar approach was used for samples with a samples-map to determine the correct view
@@ -44,6 +46,8 @@ const XpdfContainersView = import(/* webpackChunkName: "shipment" */ 'modules/ty
const ContainerRegistryView = import(/* webpackChunkName: "shipment" */ 'modules/shipment/views/containerregistry')
const RegisteredContainer = import(/* webpackChunkName: "shipment" */ 'modules/shipment/views/registeredcontainer')
+const QueuedContainers = import(/* webpackChunkName: "shipment" */ 'modules/shipment/views/queuedcontainers')
+const ReviewContainer = import(/* webpackChunkName: "shipment" */ 'modules/shipment/views/containerreview')
const MigrateView = import(/* webpackChunkName: "shipment" */ 'modules/shipment/views/migrate')
@@ -77,6 +81,10 @@ app.addInitializer(function() {
application.on('rcontainer:show', function(crid) {
application.navigate('/containers/registry/'+crid)
})
+
+ application.on('container:review', function(cid) {
+ application.navigate('/containers/review/'+cid)
+ })
})
let bc = { title: 'Shipments', url: '/shipments' }
@@ -310,6 +318,31 @@ const routes = [
breadcrumbs: [bc, { title: 'Dewar Stats' }]
}
},
+ {
+ path: 'csv/:sid',
+ name: 'shipment-import-csv',
+ component: MarionetteView,
+ props: route => ({
+ mview: ImportFromCSV,
+ breadcrumbs: [bc, { title: 'Import from CSV' }],
+ breadcrumb_tags: ['SHIPPINGNAME'], // Append shipment model name to the bc
+ options: {
+ model: new Shipment({ SHIPPINGID: route.params.sid })
+ }
+ }),
+ beforeEnter: (to, from, next) => {
+ // Call the loading state here because we are finding the proposal based on this shipment id
+ // Prop lookup sets the proposal and type via set application.cookie method which we mapped to the store
+ store.dispatch('proposal/proposalLookup', { field: 'SHIPPINGID', value: to.params.sid } )
+ .then( () => {
+ console.log("Calling next - Success, shipment model will be prefetched in marionette view")
+ next()
+ }, () => {
+ console.log("Error, no proposal found from the shipment id")
+ next('/notfound')
+ })
+ }
+ },
],
},
//
@@ -373,6 +406,47 @@ const routes = [
cid: +route.params.cid,
}),
},
+ {
+ path: '/containers/queued(/ty/)?:ty([a-zA-Z0-9]+)?(/s/)?:s([a-zA-Z0-9_-]+)?(/pt/)?:pt([a-zA-Z0-9_-]+)?(/bl/)?:bl([a-zA-Z0-9_-]+)?(/sid/)?:sid([0-9]+)?(/page/)?:page([0-9]+)?',
+ name: 'containers-queued',
+ meta: {
+ permission: 'auto_dash'
+ },
+ component: MarionetteView,
+ props: route => ({
+ mview: QueuedContainers,
+ options: {
+ params: {
+ s: route.params.s, ty: route.params.ty, pt: route.params.pt,
+ bl: route.params.bl, sid: route.params.sid, page: route.params.page
+ }
+ },
+ breadcrumbs: [bc, { title: 'Queued Containers' }]
+ }),
+ },
+ {
+ path: '/containers/review/:cid([0-9]+)',
+ name: 'container-review',
+ component: MarionetteView,
+ props: route => ({
+ mview: ReviewContainer,
+ options: {
+ model: new Container({ CONTAINERID: route.params.cid })
+ },
+ breadcrumbs: [bc, { title: 'Containers' }, { title: 'Review' }],
+ breadcrumb_tags: ['SHIPMENT', 'NAME'],
+ beforeEnter: (to, from, next) => {
+ store.dispatch('proposal/proposalLookup', { field: 'CONTAINERID', value: to.params.cid } )
+ .then( () => {
+ console.log("Calling next - Success. model will be prefetched in marionette view")
+ next()
+ }, () => {
+ console.log("Calling next - Error, no container found")
+ next('/notfound')
+ })
+ }
+ }),
+ },
{
path: '/containers/plan/:cid([0-9]+)',
component: ContainerPlanWrapper,
diff --git a/client/src/js/modules/shipment/views/container.js b/client/src/js/modules/shipment/views/container.js
index 66ad313c6..7cfa3ac9e 100644
--- a/client/src/js/modules/shipment/views/container.js
+++ b/client/src/js/modules/shipment/views/container.js
@@ -58,10 +58,12 @@ define(['marionette',
ext: '.extrainfo',
auto: '.auto',
extrastate: '.extra-state',
+ dpstate: '.dp-state',
},
events: {
'click @ui.ext': 'toggleExtra',
+ 'click a.dpinfo': 'toggleDP',
'click a.queue': 'confirmQueueContainer',
'click a.unqueue': 'confirmUnqueueContainer',
},
@@ -69,6 +71,8 @@ define(['marionette',
templateHelpers: function() {
return {
IS_STAFF: app.staff,
+ ENABLE_EXP_PLAN: app.config.enable_exp_plan,
+ AUTO_LABEL: this.automated_label
}
},
@@ -79,11 +83,20 @@ define(['marionette',
: this.ui.extrastate.addClass('fa-plus').removeClass('fa-minus')
},
+ toggleDP: function(e) {
+ e.preventDefault()
+ this.table.currentView.toggleDP()
+ this.table.currentView.dpState() ? this.ui.dpstate.addClass('fa-minus').removeClass('fa-plus')
+ : this.ui.dpstate.addClass('fa-plus').removeClass('fa-minus')
+ },
+
createSamples: function() {
this.samples = new Samples(null, { state: { pageSize: 9999 } })
},
initialize: function(options) {
+ this.automated_label = app.config.auto_collect_label || 'Automated'
+
var self = this
this.createSamples()
this.samples.queryParams.cid = options.model.get('CONTAINERID')
@@ -160,10 +173,10 @@ define(['marionette',
updateAutoCollection: function() {
if (this.model.get('CONTAINERQUEUEID')) {
- this.ui.auto.html('This container was queued for auto collection on '+this.model.escape('QUEUEDTIMESTAMP'))
+ this.ui.auto.html('This container was queued for '+this.automated_label.toLowerCase()+' collection on '+this.model.escape('QUEUEDTIMESTAMP'))
this.ui.auto.append(' Unqueue ')
} else {
- this.ui.auto.html(' Queue this container for Auto Collect')
+ this.ui.auto.html(' Queue this container for '+this.automated_label.toLowerCase()+' collection')
}
},
@@ -171,7 +184,7 @@ define(['marionette',
e.preventDefault()
utils.confirm({
title: 'Queue Container?',
- content: 'Are you sure you want to queue this container for auto collection?',
+ content: 'Are you sure you want to queue this container for '+this.automated_label.toLowerCase()+' collection?',
callback: this.doQueueContainer.bind(this)
})
},
diff --git a/client/src/js/modules/shipment/views/containeradd.js b/client/src/js/modules/shipment/views/containeradd.js
index 00997d9a8..5eebca004 100644
--- a/client/src/js/modules/shipment/views/containeradd.js
+++ b/client/src/js/modules/shipment/views/containeradd.js
@@ -95,6 +95,8 @@ define(['backbone',
SHIPPINGID: this.dewar.get('SHIPPINGID'),
SHIPMENT: this.dewar.get('SHIPPINGNAME'),
DEWAR: this.dewar.get('CODE'),
+ ENABLE_EXP_PLAN: app.config.enable_exp_plan,
+ AUTO_LABEL: this.automated_label
}
},
@@ -115,6 +117,7 @@ define(['backbone',
auto: 'input[name=AUTOMATED]',
extrastate: '.extra-state',
spacegroups: 'input[name=SPACEGROUPS]',
+ dp: '.dp-state',
},
@@ -135,6 +138,7 @@ define(['backbone',
'change @ui.type': 'setType',
'click @ui.ext': 'toggleExtra',
+ 'click a.dpinfo': 'toggleDP',
'keypress .ui-combobox input': 'excelNavigate',
'keypress input.sname': 'excelNavigate',
@@ -290,6 +294,13 @@ define(['backbone',
}
},
+ toggleDP: function(e) {
+ e.preventDefault()
+ this.table.currentView.toggleDP()
+ this.table.currentView.dpState() ? this.ui.dpstate.addClass('fa-minus').removeClass('fa-plus')
+ : this.ui.dpstate.addClass('fa-plus').removeClass('fa-minus')
+ },
+
isForImager: function() {
return !(!this.ui.imager.val())
},
@@ -530,6 +541,8 @@ define(['backbone',
},
initialize: function(options) {
+ this.automated_label = app.config.auto_collect_label || 'Automated'
+
this.ready = []
this.dewar = options.dewar
diff --git a/client/src/js/modules/shipment/views/containerreview.js b/client/src/js/modules/shipment/views/containerreview.js
new file mode 100644
index 000000000..d95c72afb
--- /dev/null
+++ b/client/src/js/modules/shipment/views/containerreview.js
@@ -0,0 +1,157 @@
+define(['backbone',
+ 'marionette',
+ 'backgrid',
+ 'views/table',
+ 'utils/table',
+ 'collections/samples',
+ 'templates/shipment/containerreview.html'
+], function(Backbone,
+ Marionette,
+ Backgrid,
+ TableView,
+ table,
+ Samples,
+ template) {
+
+ var QueueStatusCell = Backgrid.Cell.extend({
+ render: function() {
+ var st = this.model.escape('LASTQUEUESTATUS')
+ if (st) this.$el.html('')
+
+ return this
+ }
+ })
+
+ var UCTemplate = '\
+ \
+ \
+ \
+ A \
+ B \
+ C \
+ α \
+ β \
+ γ \
+ \
+ \
+ \
+ \
+ <%-CELL_A%> \
+ <%-CELL_B%> \
+ <%-CELL_C%> \
+ <%-CELL_ALPHA%> \
+ <%-CELL_BETA%> \
+ <%-CELL_GAMMA%> \
+ \
+ \
+
'
+
+ var ActionCell = Backgrid.Cell.extend({
+ className: 'nowrap',
+
+ events: {
+ 'click a.reinspect': 'markReinspect',
+ 'click a.skip': 'markSkip',
+ 'click a.completed': 'markCompleted'
+ },
+
+ render: function() {
+ if (app.staff && this.model.get('CONTAINERQUEUESAMPLEID')) {
+ var cs = this.model.get('LASTQUEUESTATUS')
+ if (cs != 'reinspect') this.$el.html(' ')
+ if (cs != 'skipped') this.$el.append(' ')
+ if (cs != 'completed') this.$el.append(' ')
+ }
+
+ this.$el.append(' View Sample ')
+
+ return this
+ },
+
+ markReinspect: function(e) {
+ e.preventDefault()
+ this.doMarkSample('reinspect')
+ },
+
+ markSkip: function(e) {
+ e.preventDefault()
+ this.doMarkSample('skipped')
+ },
+
+ markCompleted: function(e) {
+ e.preventDefault()
+ this.doMarkSample('completed')
+ },
+
+ doMarkSample: function(status) {
+ var self = this
+ Backbone.ajax({
+ url: app.apiurl+'/sample/queue/'+this.model.get('CONTAINERQUEUESAMPLEID'),
+ data: JSON.stringify({
+ prop: app.prop,
+ QUEUESTATUS: status
+ }),
+ type: 'PATCH',
+ success: function() {
+ app.alert({ className: 'message notify', message: 'Sample queue status upated', scrollTo: false })
+ self.model.collection.fetch()
+ },
+ error: function() {
+ app.alert({ message: 'Something went wrong updating this samples queue status, please try again' })
+ },
+
+ })
+ },
+ })
+
+ return Marionette.LayoutView.extend({
+ className: 'content',
+ template: template,
+
+ regions: {
+ rsamples: '.rsamples'
+ },
+
+ initialize: function(options) {
+ this.samples = new Samples(null, { state: { pageSize: 9999 } })
+ this.samples.queryParams.cid = options.model.get('CONTAINERID')
+ this.samples.fetch()
+ },
+
+ onRender: function() {
+ var columns = [
+ { name: 'LOCATION', label: 'Location', cell: 'string', editable: false },
+ { name: 'NAME', label: 'Name', cell: 'string', editable: false },
+ { name: 'ACRONYM', label: 'Protein', cell: 'string', editable: false },
+ { name: 'COMMENTS', label: 'Comment', cell: 'string', editable: false },
+ { name: 'SPACEGROUP', label: 'SG', cell: 'string', editable: false },
+ { label: 'Unit Cell', cell: table.TemplateCell, template: UCTemplate, editable: false },
+ { name: 'REQUIREDRESOLUTION', label: 'Required Res', cell: 'string', editable: false },
+ { name: 'AIMEDRESOLUTION', label: 'Aimed Res', cell: 'string', editable: false },
+ { name: 'COLLECTIONMODE', label: 'Mode', cell: 'string', editable: false },
+ { name: 'PRIORITY', label: 'Priority', cell: 'string', editable: false },
+ { name: 'EXPOSURETIME', label: 'Exposure (s)', cell: 'string', editable: false },
+ { name: 'AXISRANGE', label: 'Axis Range', cell: 'string', editable: false },
+ { name: 'NUMBEROFIMAGES', label: 'No. Images', cell: 'string', editable: false },
+ { name: 'TRASMISSION', label: 'Transmission', cell: 'string', editable: false },
+ { name: 'DCRESOLUTION', label: 'Observed Res', cell: 'string', editable: false },
+ { name: 'DCSPACEGROUP', label: 'Observed SG', cell: 'string', editable: false },
+ { name: 'STAFFCOMMENTS', label: 'Staff Comments', cell: 'string', editable: app.staff },
+ { label: 'Status', cell: table.StatusCell, editable: false },
+ { label: 'Queue', cell: QueueStatusCell, editable: false },
+ { label: '', cell: ActionCell, editable: false },
+ ]
+
+ this.rsamples.show(new TableView({
+ collection: this.samples,
+ columns: columns
+ }))
+
+ this.listenTo(this.samples, 'change:STAFFCOMMENTS', this.saveStaffComment, this)
+ },
+
+ saveStaffComment: function(m, v) {
+ m.save(m.changedAttributes(), { patch: true })
+ },
+ })
+})
diff --git a/client/src/js/modules/shipment/views/fromcsv.js b/client/src/js/modules/shipment/views/fromcsv.js
new file mode 100644
index 000000000..6d7a3c8e4
--- /dev/null
+++ b/client/src/js/modules/shipment/views/fromcsv.js
@@ -0,0 +1,568 @@
+define(['backbone',
+ 'marionette',
+ 'papaparse',
+ 'models/protein',
+ 'collections/proteins',
+ 'models/sample',
+ 'collections/samples',
+ 'models/container',
+ 'collections/containers',
+ 'collections/dewars',
+ 'views/validatedrow',
+ 'views/form',
+ 'modules/shipment/collections/platetypes',
+ 'modules/shipment/collections/containerregistry',
+ 'collections/users',
+ 'modules/shipment/collections/distinctproteins',
+
+ 'utils/sgs',
+ 'utils/collectionmode',
+
+ 'templates/shipment/fromcsv.html',
+ 'templates/shipment/fromcsvtable.html',
+ 'templates/shipment/fromcsvcontainer.html'
+ ], function(
+ Backbone,
+ Marionette,
+ Papa,
+ Protein,
+ Proteins,
+ Sample,
+ Samples,
+ Container,
+ Containers,
+ Dewars,
+ ValidatedRow,
+ FormView,
+ PlateTypes,
+ ContainerRegistry,
+ Users,
+ DistinctProteins,
+ SG,
+ COLM,
+ template, table, container) {
+
+
+ var GridRow = ValidatedRow.extend({
+ template: false,
+ tagName: 'tr',
+
+ columnTypes: {
+ LOCATION: function(v) {
+ return ''+v+' '
+ },
+
+ CELL: function(v, model) {
+ return `
+
+
+
+
+
+
+
+ `
+ },
+ COLLECTIONMODE: function() {
+ return ''+COLM.opts()+' '
+ },
+ PROTEINID: function(v, m) {
+ var newProtein = v == 0 ? 'active' : ''
+ return ''+m.escape('ACRONYM')+' '
+ },
+ SPACEGROUP: function(v, m) {
+ return ''+SG.opts()+' '
+ }
+ },
+
+ onRender: function() {
+ var cts = this.getOption('columnTypes')
+ var columns = _.map(this.getOption('profile').columns, function(c, k) {
+ var val = this.model.get(k) || ''
+ return cts[k] ? cts[k](val, this.model, this) : ' '
+ }, this)
+
+ this.$el.html(columns.join(''))
+ this.$el.find('select[name=SPACEGROUP]').val(this.model.get('SPACEGROUP'))
+ this.$el.find('select[name=COLLECTIONMODE]').val(this.model.get('COLLECTIONMODE'))
+ this.model.validate()
+ },
+ })
+
+ var TableView = Marionette.CompositeView.extend({
+ tagName: "table",
+ className: 'samples reflow',
+ template: table,
+ childView: GridRow,
+ childViewOptions: function() {
+ return {
+ profile: this.getOption('profile'),
+ }
+ },
+
+ ui: {
+ tr: 'thead tr',
+ },
+
+ onRender: function() {
+ var headers = _.map(this.getOption('profile').columns, function(c, k) {
+ return ''+c+' '
+ })
+
+ this.ui.tr.html(headers.join(''))
+ },
+
+ })
+
+ var ContainerView = Marionette.LayoutView.extend({
+ template: _.template('<%-NAME%>
'),
+
+ regions: {
+ rsamples: '.rsamples',
+ rcontainer: '.rcontainer',
+ },
+
+ initialize: function(options) {
+ this.samples = new Samples()
+ this.listenTo(options.samples, 'sync reset', this.generateSamples)
+ this.generateSamples()
+ },
+
+ generateSamples: function() {
+ this.samples.reset(this.getOption('samples').where({ CONTAINER: this.model.get('NAME') }))
+ },
+
+ onRender: function() {
+ this.rsamples.show(new TableView({
+ collection: this.samples,
+ profile: this.getOption('profile'),
+ }))
+ this.rcontainer.show(new ModifyContainerView({
+ model: this.model,
+ platetypes: this.getOption('platetypes'),
+ users: this.getOption('users'),
+ containerregistry: this.getOption('containerregistry'),
+ }))
+ }
+ })
+
+ var ModifyContainerView = FormView.extend({
+ template: container,
+ ui: {
+ containertype: '[name=CONTAINERTYPE]',
+ registry: '[name=CONTAINERREGISTRYID]',
+ person: '[name=PERSONID]',
+ },
+
+ events: {
+ 'change select': 'updateModel',
+ 'change input': 'updateModel',
+ },
+
+ createModel: function() {
+
+ },
+
+ updateModel: function(e) {
+ var attr = $(e.target).attr('name')
+ console.log('updateModel', attr, e.target.value)
+ if (attr == 'CONTAINERTYPE') {
+ this.updateContainerType()
+ } else {
+ this.model.set({ [attr]: e.target.value })
+ }
+ },
+
+ updateContainerType: function() {
+ var containerType = this.getOption('platetypes').findWhere({ name: this.ui.containertype.val() })
+ this.model.set({ CONTAINERTYPE: this.ui.containertype.val(), CAPACITY: containerType.get('capacity') })
+ },
+
+ onRender: function() {
+ this.ui.containertype.html(this.getOption('platetypes').opts())
+ this.ui.registry.html('Please select one '+this.getOption('containerregistry').opts({ empty: true }))
+ this.ui.person.html(this.getOption('users').opts()).val(this.model.get('OWNERID') || app.personid)
+
+ if (!this.model.get('CONTAINERTYPE')) {
+ this.updateContainerType()
+ }
+
+ if (!this.model.get('CONTAINERREGISTRYID')) {
+ var reg = this.getOption('containerregistry')
+ var nearest = reg.findWhere({ LASTNAME: this.model.get('NAME') })
+ this.model.set({ CONTAINERREGISTRYID: nearest ? nearest.get('CONTAINERREGISTRYID') : '!' })
+ }
+ this.ui.registry.val(this.model.get('CONTAINERREGISTRYID'))
+
+ this.model.isValid(true)
+ }
+ })
+
+ var ContainersView = Marionette.CollectionView.extend({
+ childView: ContainerView,
+ childViewOptions: function() {
+ return {
+ samples: this.getOption('samples'),
+ profile: this.getOption('profile'),
+ platetypes: this.getOption('platetypes'),
+ containerregistry: this.getOption('containerregistry'),
+ users: this.getOption('users'),
+ proteins: this.getOption('proteins'),
+ }
+ }
+ })
+
+
+ var MessageView = Marionette.ItemView.extend({
+ tagName: 'li',
+ template: _.template('<%-message%>')
+ })
+
+ var MessagesView = Marionette.CollectionView.extend({
+ tagName: 'ul',
+ childView: MessageView
+ })
+
+ var Message = Backbone.Model.extend({
+
+ })
+
+ return Marionette.LayoutView.extend({
+ template: template,
+ className: 'content',
+
+ regions: {
+ rcontainers: '.rcontainers',
+ rmessages: '.rmessages',
+ },
+
+ ui: {
+ drop: '.dropimage',
+ pnew: '.pnew',
+ },
+
+ events: {
+ 'dragover @ui.drop': 'dragHover',
+ 'dragleave @ui.drop': 'dragHover',
+ 'drop @ui.drop': 'uploadFile',
+ 'click .submit': 'import',
+ 'click a.export': 'export',
+ },
+
+ addMessage: function(options) {
+ this.messages.add(new Message({ message: options.message }))
+ },
+
+ import: function(e) {
+ e.preventDefault()
+ this.messages.reset()
+
+ if (!this.containers.length && !this.samples.length) {
+ app.alert({ message: 'No containers and samples found' })
+ return
+ }
+
+ var valid = true
+ this.containers.each(function(c) {
+ var cValid = c.isValid(true)
+ console.log(c.get('CODE'), c)
+ if (!cValid) {
+ valid = false
+ this.addMessage({ message: `Container ${c.get('NAME')} is invalid` })
+ }
+ }, this)
+
+ this.samples.each(function(s) {
+ var sValid = s.isValid(true)
+ console.log(s.get('NAME'), s)
+ if (!sValid) {
+ valid = false
+ this.addMessage({ message: `Sample ${s.get('NAME')} is invalid` })
+ }
+ }, this)
+
+ var pos = this.$el.find('.top').offset().top
+ $('html, body').animate({scrollTop: pos}, 300);
+
+ if (!valid) {
+ app.alert({ message: 'Shipment is not valid' })
+ return
+ }
+
+ this.messages.reset()
+
+ var self = this
+ var pp = []
+ this.newProteins.each(function(p) {
+ pp.push(p.save({}, {
+ success: function() {
+ self.addMessage({ message: 'Created component: '+p.get('ACRONYM') })
+ },
+ error: function(xhr, status, error) {
+ self.addMessage({ messages: 'Error creating component: '+error})
+ }
+ }))
+ }, this)
+
+ $.when.apply($, pp).done(function() {
+ var cp = []
+ self.containers.each(function(c) {
+ if (c.isNew()) {
+ cp.push(c.save({}, {
+ success: function() {
+ self.addMessage({ message: 'Created container: '+c.get('NAME') })
+ },
+ error: function(xhr, status, error) {
+ self.addMessage({ messages: 'Error creating container: '+error})
+ }
+ }))
+ }
+ })
+
+ $.when.apply($, cp).done(function() {
+ var news = new Samples(self.samples.filter(function(s) {
+ return s.isNew()
+ }))
+
+ if (news.length == 0) return
+
+ news.each(function(s) {
+ if (!s.get('CONTAINERID')) {
+ var c = self.containers.findWhere({ NAME: s.get('CONTAINER')})
+ s.set({ CONTAINERID: c.get('CONTAINERID') }, { silent: true })
+ }
+
+ if (s.get('PROTEINID') == 0) {
+ var p = self.newProteins.findWhere({ ACRONYM: s.get('ACRONYM')})
+ s.set({ PROTEINID: p.get('PROTEINID') }, { silent: true })
+ }
+ })
+
+ news.save({
+ success: function() {
+ app.alert({ message: 'Shipment contents imported, Click here to view it', persist: 'simport'+self.model.escape('SHIPPINGID'), className: 'message notify' })
+ self.addMessage({ messages: 'Samples created' })
+ },
+ error: function(xhr, status, error) {
+ self.addMessage({ messages: 'Error creating samples: '+error})
+ }
+ })
+ })
+
+ })
+ },
+
+ export: function() {
+ var rows = []
+ rows.push(this.csvProfile.headers)
+ this.samples.each(function(s) {
+ var row = []
+ _.each(this.csvProfile.mapping, function(k, i) {
+ if (k in this.csvProfile.export) {
+ row.push(this.csvProfile.export[k](s.toJSON(), this.csvProfile.headers[i]))
+ } else {
+ row.push(s.get(k))
+ }
+ }, this)
+
+ rows.push(row)
+ }, this)
+
+ var csv = Papa.unparse(rows)
+ var a = document.createElement('a')
+ var file = new Blob([csv], {type: 'application/octet-stream'})
+
+ a.href= URL.createObjectURL(file)
+ a.download = this.model.get('SHIPPINGNAME') + '.csv'
+ a.click()
+
+ URL.revokeObjectURL(a.href);
+
+ console.log(csv)
+ },
+
+ initialize: function(options) {
+ this.messages = new Backbone.Collection()
+
+ this.samples = new Samples(null, { state: { pageSize: 9999 }})
+ this.samples.queryParams.SHIPPINGID = this.model.get('SHIPPINGID')
+ this.samples.fetch()
+
+ this.containers = new Containers()
+ this.containers.queryParams.SHIPPINGID = this.model.get('SHIPPINGID')
+ this.containers.setSorting('BLTIMESTAMP', 0)
+ this.containers.fetch()
+
+ this.platetypes = new PlateTypes()
+
+ this.ready = []
+ this.containerregistry = new ContainerRegistry(null, { state: { pageSize: 9999 }})
+ this.ready.push(this.containerregistry.fetch())
+
+ this.users = new Users(null, { state: { pageSize: 9999 }})
+ this.users.queryParams.all = 1
+ this.users.queryParams.pid = app.proposal.get('PROPOSALID')
+ this.ready.push(this.users.fetch())
+
+ this.newProteins = new Proteins()
+ this.proteins = new DistinctProteins()
+ if (app.valid_samples) {
+ this.proteins.queryParams.external = 1
+ }
+ this.ready.push(this.proteins.fetch())
+
+ this.dewars = new Dewars()
+ this.dewars.queryParams.sid = this.model.get('SHIPPINGID')
+ this.ready.push(this.dewars.fetch())
+
+ this.csvProfile = require('csv/'+app.config.csv_profile+'.js')
+ console.log('initialize', this.csvProfile)
+ },
+
+
+ dragHover: function(e) {
+ e.stopPropagation()
+ e.preventDefault()
+ if (e.type == 'dragover') this.ui.drop.addClass('active')
+ else this.ui.drop.removeClass('active')
+ },
+
+ uploadFile: function(e) {
+ this.dragHover(e)
+ var files = e.originalEvent.dataTransfer.files
+ var f = files[0]
+
+ if (f.name.endsWith('csv')) {
+ var reader = new FileReader()
+ var self = this
+ reader.onload = function(e) {
+ self.parseCSVContents(e.target.result)
+ }
+ reader.readAsText(f)
+ } else {
+ app.alert({ message: 'Cannot import file "'+f.name+'" is not a csv file' })
+ }
+ },
+
+ createObjects: function(raw) {
+ var parsed = Papa.parse(raw)
+
+ if (parsed.errors.length) {
+ var errs = []
+ _.each(parsed.errors, function(e) {
+ errs.push({ message: e.code + ': ' + e.message + ' at row ' + e.row })
+ })
+ this.messages.reset(errs)
+ app.alert({ message: 'Error parsing csv file, see messages below' })
+ return
+ }
+
+ var objects = parsed.data
+ var headers = this.csvProfile.mapping
+ var transforms = this.csvProfile.transforms
+ objects.splice(0, 1)
+
+ var newProteins = []
+ var populatedObject = []
+ _.each(objects, function(item){
+ if (!item.length || (item.length == 1 && !item[0].trim())) return
+ var obj = {}
+ _.each(item, function(v, i) {
+ var key = headers[i]
+ if (v) obj[key] ? obj[key] += ' | '+v : obj[key] = v
+ })
+
+ _.each(obj, function(v, k) {
+ if (k in transforms) {
+ transforms[k](v, obj)
+ }
+ }, this)
+
+ if (!obj.PROTEINID) {
+ var protein = this.proteins.findWhere({ ACRONYM: obj.ACRONYM })
+ if (protein) {
+ obj.PROTEINID = protein.get('PROTEINID')
+ } else {
+ obj.PROTEINID = 0
+ var newp = _.findWhere(newProteins, { ACRONYM: obj.ACRONYM })
+ if (!newp) {
+ newProteins.push({
+ ACRONYM: obj.ACRONYM,
+ NAME: obj.ACRONYM,
+ })
+ }
+ }
+ }
+
+ populatedObject.push(obj)
+ }, this)
+
+ this.newProteins.reset(newProteins)
+ return populatedObject
+ },
+
+ parseCSVContents: function(raw) {
+ var samples = this.createObjects(raw)
+ console.log('parseCSVContents', samples)
+
+ var valid = true
+ var existingContainers = this.containers.pluck('NAME')
+ _.each(_.unique(_.pluck(samples, 'CONTAINER')), function(name) {
+ if (existingContainers.indexOf(name) > -1) {
+ app.alert({ message: 'Container ' + name + ' already exists' })
+ valid = false
+ }
+ })
+
+ var existingSamples = this.samples.pluck('NAME')
+ _.each(samples, function(sample) {
+ if (existingSamples.indexOf(sample.NAME) > -1) {
+ app.alert({ message: 'Sample ' + sample.NAME + ' already exists' })
+ valid = false
+ }
+ })
+
+ if (!valid) {
+ app.alert({ message: 'Duplicate containers and/or samples, aborting' })
+ return
+ }
+
+ this.samples.reset(samples)
+
+ this.containers.reset(_.map(_.unique(_.pluck(samples, 'CONTAINER')), function(name) {
+ var sample = this.samples.findWhere({ CONTAINER: name })
+
+ var ownerid = null
+ if (sample) {
+ var oid = this.users.findWhere({ FULLNAME: sample.get('OWNER') })
+ if (oid) ownerid = oid.get('PERSONID')
+ }
+
+ return { NAME: name, DEWARID: this.dewars.at(0).get('DEWARID'), OWNERID: ownerid }
+ }, this))
+
+ if (this.newProteins.length) this.ui.pnew.text('Need to create '+this.newProteins.length+' components: '+this.newProteins.pluck('ACRONYM').join(', '))
+ },
+
+ onRender: function() {
+ $.when.apply($, this.ready).done(this.doOnRender.bind(this))
+ this.rmessages.show(new MessagesView({ collection: this.messages }))
+ },
+
+ doOnRender: function() {
+ // this.parseCSVContents(this.csvProfile.exampleCSV)
+
+ this.rcontainers.show(new ContainersView({
+ collection: this.containers,
+ samples: this.samples,
+ profile: this.csvProfile,
+ platetypes: this.platetypes,
+ containerregistry: this.containerregistry,
+ users: this.users,
+ proteins: this.proteins,
+ }))
+ },
+
+ })
+
+})
diff --git a/client/src/js/modules/shipment/views/queuedcontainers.js b/client/src/js/modules/shipment/views/queuedcontainers.js
new file mode 100644
index 000000000..8f431f1ed
--- /dev/null
+++ b/client/src/js/modules/shipment/views/queuedcontainers.js
@@ -0,0 +1,217 @@
+define(['backbone', 'marionette',
+ 'backgrid',
+ 'views/table',
+ 'views/filter',
+ 'utils/table',
+ 'utils',
+ 'collections/proposaltypes',
+ 'collections/bls',
+ 'collections/containers'], function(Backbone, Marionette, Backgrid, TableView, FilterView, table, utils,
+ ProposalTypes,
+ Beamlines,
+ Containers) {
+
+
+ var ClickableRow = table.ClickableRow.extend({
+ event: 'container:review',
+ argument: 'CONTAINERID',
+ cookie: true,
+ })
+
+ var ActionCell = Backgrid.Cell.extend({
+ events: {
+ 'click a.completed': 'markCompleted'
+ },
+
+ markCompleted: function(e) {
+ e.preventDefault()
+ utils.confirm({
+ title: 'Confirm Mark Completed',
+ content: 'Are you sure you want to mark "'+this.model.get('NAME')+'" completed?',
+ callback: this.doMarkCompleted.bind(this)
+ })
+ },
+
+ doMarkCompleted: function() {
+ var self = this
+ Backbone.ajax({
+ url: app.apiurl+'/shipment/containers/queue/'+this.model.get('CONTAINERQUEUEID'),
+ method: 'POST',
+ success: function() {
+ app.alert({ className: 'message notify', message: 'Container queue successfully marked as completed' })
+ self.model.collection.fetch()
+ },
+ error: function() {
+ app.alert({ message: 'Something went wrong marking this container queue as completed, please try again' })
+ },
+
+ })
+ },
+
+ render: function() {
+ if (app.staff && this.model.get('CONTAINERQUEUEID')) {
+ this.$el.html(' ')
+ }
+
+ return this
+ }
+ })
+
+ var LocationCell = Backgrid.Cell.extend({
+ render: function() {
+ this.$el.html(this.model.escape('BEAMLINELOCATION'))
+ if (this.model.get('SAMPLECHANGERLOCATION')) {
+ this.$el.append(' - ' + this.model.escape('SAMPLECHANGERLOCATION'))
+ }
+ return this
+ }
+ })
+
+ var FilterWithDefault = FilterView.extend({
+ default: null,
+
+ selected: function() {
+ var selected = this.collection.findWhere({ isSelected: true })
+ if (!selected) {
+ var selected = this.collection.findWhere({ id: this.getOption('default')})
+ selected.set({isSelected: true})
+ }
+ return selected ? selected.get('id') : null
+ },
+ })
+
+ return Marionette.LayoutView.extend({
+ className: 'content',
+ template: '',
+ regions: {
+ wrap: '.wrapper',
+ type: '.type', type2: '.type2', typeas: '.typeas', typebl: '.typebl'
+ },
+
+
+ hiddenColumns: [3,4,5,7,9,10,11],
+
+ columns: [
+ { name: 'NAME', label: 'Name', cell: 'string', editable: false },
+ { name: 'PROP', label: 'Proposal', cell: 'string', editable: false },
+ { name: 'OWNER', label: 'Owner', cell: 'string', editable: false },
+ { name: 'CARDNAME', label: 'Contact', cell: 'string', editable: false },
+ { name: 'SHIPMENT', label: 'Shipment', cell: 'string', editable: false },
+ { name: 'DEWAR', label: 'Dewar', cell: 'string', editable: false },
+ { name: 'SAMPLES', label: '# Samples', cell: 'string', editable: false },
+ { name: 'MODES', label: 'Modes', cell: 'string', editable: false },
+ { name: 'CONTAINERSTATUS', label: 'Status', cell: 'string', editable: false },
+ { name: 'COMMENTS', label: 'Comments', cell: 'string', editable: false },
+ { name: 'QUEUEDTIMESTAMP', label: 'Queued', cell: 'string', editable: false },
+ { name: 'LASTQUEUECOMPLETED', label: 'Completed', cell: 'string', editable: false },
+ { label: 'SC', cell: LocationCell, editable: false },
+ { label: '', cell: ActionCell, editable: false },
+ ],
+
+ showFilter: true,
+ filters: [
+ { id: 'queued', name: 'Queued'},
+ { id: 'completed', name: 'Completed'},
+ ],
+
+ ui: {
+ total: 'span.total',
+ },
+
+ refresh: function() {
+ this.collection.fetch()
+ },
+
+ updateTotal: function() {
+ this.ui.total.text(this.collection.state.totalRecords)
+ },
+
+ initialize: function(options) {
+ this.types = new ProposalTypes()
+ this.ready = []
+ this.ready.push(this.types.fetch())
+
+ this.beamlines = new Beamlines(null, { ty: app.type })
+ this.ready.push(this.beamlines.fetch())
+
+ this.collection = new Containers()
+ this.collection.queryParams.all = 1
+ this.collection.queryParams.PUCK = 1
+ this.collection.queryParams.ty = 'queued'
+ if (options.params.sid) this.collection.queryParams.SHIPPINGID = options.params.sid
+ if (options.params.page) this.collection.state.currentPage = parseInt(options.params.page)
+ this.listenTo(this.collection, 'sync', this.updateTotal)
+
+ var filters = this.getOption('filters').slice(0)
+ var columns = this.getOption('columns').slice(0)
+
+ if (app.mobile()) {
+ _.each(this.getOption('hiddenColumns'), function(v) {
+ columns[v].renderable = false
+ })
+ }
+
+ this.table = new TableView({
+ collection: this.collection,
+ columns: columns,
+ tableClass: 'containers', filter: 's', search: options.params.s, loading: true,
+ backgrid: { row: ClickableRow, emptyText: 'No containers found' } })
+
+ this.ty = new FilterWithDefault({
+ default: 'queued',
+ collection: this.collection,
+ value: options.params && options.params.ty,
+ name: 'ty',
+ filters: filters
+ })
+
+ this.assigned = new FilterView({
+ collection: this.collection,
+ name: 'assigned',
+ filters: { id: 1, name: 'Assigned'},
+ })
+ },
+
+ onRender: function() {
+ this.wrap.show(this.table)
+ this.type.show(this.ty)
+ this.typeas.show(this.assigned)
+
+ $.when.apply($, this.ready).then(this.doOnRender.bind(this))
+ },
+
+ doOnRender: function() {
+ this.showProposalFilter()
+ this.showBeamlineFilter()
+ this.refresh()
+ },
+
+ showProposalFilter: function() {
+ this.ty2 = new FilterView({
+ collection: this.collection,
+ name: 'PROPOSALCODE',
+ urlFragment: 'pt',
+ value: this.getOption('params') && this.getOption('params').pt,
+ filters: this.types.map(function(b) { return { id: b.get('PROPOSALCODE'), name: b.get('PROPOSALCODE') } }),
+ })
+ this.type2.show(this.ty2)
+ },
+
+ updateFilter2: function(selected) {
+ this.collection.queryParams.proposalcode = selected
+ this.refresh()
+ },
+
+ showBeamlineFilter: function() {
+ this.tybl = new FilterView({
+ collection: this.collection,
+ name: 'bl',
+ urlFragment: 'bl',
+ value: this.getOption('params') && this.getOption('params').bl,
+ filters: this.beamlines.map(function(b) { return { id: b.get('BEAMLINE'), name: b.get('BEAMLINE') } }),
+ })
+ this.typebl.show(this.tybl)
+ },
+ })
+
+})
diff --git a/client/src/js/modules/shipment/views/sampletable.js b/client/src/js/modules/shipment/views/sampletable.js
index 64955987b..44cf1cda8 100644
--- a/client/src/js/modules/shipment/views/sampletable.js
+++ b/client/src/js/modules/shipment/views/sampletable.js
@@ -12,11 +12,11 @@ define(['marionette',
'collections/spacegroups',
'utils/forms',
- 'utils/sgs',
'utils/anoms',
'utils/centringmethods',
'utils/experimentkinds',
'utils/radiationsensitivity',
+ 'utils/collectionmode',
'utils',
'utils/safetylevel',
@@ -24,7 +24,7 @@ define(['marionette',
'jquery',
], function(Marionette, Protein, Proteins, ValidatedRow, DistinctProteins, ComponentsView,
sampletable, sampletablerow, sampletablerowedit, SpaceGroups,
- forms, SG, Anom, CM, EXP, RS, utils, safetyLevel, $) {
+ forms, Anom, CM, EXP, RS, COLM, utils, safetyLevel, $) {
// A Sample Row
@@ -74,13 +74,15 @@ define(['marionette',
cancelEditSample: function(e) {
this.editing = false
e.preventDefault()
+ if (this.model.get('PROTEINID') > -1 && this.model.isNew()) this.model.set({ PROTEINID: -1, CRYSTALID: -1 })
this.template = this.getOption('rowTemplate')
this.render()
},
setData: function() {
var data = {}
- _.each(['CODE', 'PROTEINID', 'CRYSTALID', 'NAME', 'COMMENTS', 'SPACEGROUP', 'VOLUME', 'ABUNDANCE', 'PACKINGFRACTION', 'LOOPTYPE', 'CENTRINGMETHOD', 'EXPERIMENTKIND', 'ENERGY', 'RADIATIONSENSITIVITY', 'USERPATH'], function(f) {
+ _.each(['CODE', 'PROTEINID', 'CRYSTALID', 'NAME', 'COMMENTS', 'SPACEGROUP', 'VOLUME', 'ABUNDANCE', 'PACKINGFRACTION', 'LOOPTYPE', 'CENTRINGMETHOD', 'EXPERIMENTKIND', 'ENERGY', 'RADIATIONSENSITIVITY', 'USERPATH',
+ 'AIMEDRESOLUTION', 'COLLECTIONMODE', 'PRIORITY', 'EXPOSURETIME', 'AXISSTART', 'AXISRANGE', 'NUMBEROFIMAGES', 'TRANSMISSION', 'PREFERREDBEAMSIZEX'], function(f) {
var el = this.$el.find('[name='+f+']')
if (el.length) data[f] = el.attr('type') == 'checkbox'? (el.is(':checked')?1:null) : el.val()
}, this)
@@ -149,6 +151,7 @@ define(['marionette',
CELL_A: '', CELL_B: '', CELL_C: '', CELL_ALPHA: '', CELL_BETA: '', CELL_GAMMA: '', REQUIREDRESOLUTION: '', ANOM_NO: '', ANOMALOUSSCATTERER: '',
CRYSTALID: -1, PACKINGFRACTION: '', LOOPTYPE: '',
DIMENSION1: '', DIMENSION2: '', DIMENSION3: '', SHAPE: '', CENTRINGMETHOD: '', EXPERIMENTKIND: '', ENERGY: '', RADIATIONSENSITIVITY: '', USERPATH: '',
+ AIMEDRESOLUTION: '', COLLECTIONMODE: '', PRIORITY: '', EXPOSURETIME: '', AXISSTART: '', AXISRANGE: '', NUMBEROFIMAGES: '', TRANSMISSION: '', PREFERREDBEAMSIZEX: ''
})
this.model.get('components').reset()
this.render()
@@ -210,13 +213,15 @@ define(['marionette',
//if (this.model.get('CODE')) this.$el.find('input[name=CODE]').val(this.model.get('CODE'))
//if (this.model.get('COMMENTS')) this.$el.find('input[name=COMMENTS]').val(this.model.get('COMMENTS'))
- _.each(['NAME', 'CODE', 'COMMENTS', 'CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA', 'REQUIREDRESOLUTION', 'ANOM_NO', 'VOLUME', 'PACKINGFRACTION', 'USERPATH'], function(f, i) {
+ _.each(['NAME', 'CODE', 'COMMENTS', 'CELL_A', 'CELL_B', 'CELL_C', 'CELL_ALPHA', 'CELL_BETA', 'CELL_GAMMA', 'REQUIREDRESOLUTION', 'ANOM_NO', 'VOLUME', 'PACKINGFRACTION', 'USERPATH',
+ 'AIMEDRESOLUTION', 'COLLECTIONMODE', 'PRIORITY', 'EXPOSURETIME', 'AXISSTART', 'AXISRANGE', 'NUMBEROFIMAGES', 'TRANSMISSION', 'PREFERREDBEAMSIZEX'], function(f, i) {
if (this.model.get(f)) this.$el.find('input[name='+f+']').val(this.model.get(f))
}, this)
this.ui.symbol.text(this.model.get('SYMBOL') ? this.model.get('SYMBOL') : '')
if (this.getOption('extra').show) this.$el.find('.extra').addClass('show')
+ if (this.getOption('dp').show) this.$el.find('.dp').addClass('show')
if (this.getOption('type') == 'non-xtal') {
this.$el.find('.non-xtal').addClass('show')
@@ -237,6 +242,7 @@ define(['marionette',
this.$el.find('[name=EXPERIMENTKIND]').html(EXP.opts()).val(this.model.get('EXPERIMENTKIND'))
this.$el.find('[name=ENERGY]').val(this.model.get('ENERGY'))
this.$el.find('[name=RADIATIONSENSITIVITY]').html(RS.opts()).val(this.model.get('RADIATIONSENSITIVITY'))
+ this.$el.find('[name=COLLECTIONMODE]').html(COLM.opts()).val(this.model.get('COLLECTIONMODE'))
this.compview = new ComponentsView({ collection: this.model.get('components'), editable: this.editing || this.model.get('new') })
this.ui.comps.append(this.compview.render().$el)
@@ -384,12 +390,15 @@ define(['marionette',
if (options.childEditTemplate) this.options.childViewOptions.editTemplate = options.childEditTemplate
this.extra = { show: false }
+ this.dp = { show: false }
this.auto = { show: options.auto == true ? true : false }
this.all_spacegroups = { show: options.spacegroups == true ? true : false }
this.options.childViewOptions.extra = this.extra
this.options.childViewOptions.auto = this.auto
+ this.options.childViewOptions.dp = this.dp
this.options.childViewOptions.all_spacegroups = this.all_spacegroups
+
this.options.childViewOptions.type = this.getOption('type')
},
@@ -414,6 +423,10 @@ define(['marionette',
return this.extra.show
},
+ dpState: function() {
+ return this.dp.show
+ },
+
toggleExtra: function() {
this.extra.show = !this.extra.show
@@ -421,6 +434,12 @@ define(['marionette',
else this.$el.find('.extra').removeClass('show')
},
+ toggleDP: function() {
+ this.dp.show = !this.dp.show
+
+ if (this.dp.show) this.$el.find('.dp').addClass('show')
+ else this.$el.find('.dp').removeClass('show')
+ },
toggleAuto: function(val) {
this.auto.show = val
diff --git a/client/src/js/modules/shipment/views/shipment.js b/client/src/js/modules/shipment/views/shipment.js
index d9c9051cf..3e6440ad4 100644
--- a/client/src/js/modules/shipment/views/shipment.js
+++ b/client/src/js/modules/shipment/views/shipment.js
@@ -43,14 +43,19 @@ define(['marionette',
APIURL: app.apiurl,
PROP: app.prop,
DHL_ENABLE: app.options.get('dhl_enable'),
+ IS_STAFF: app.staff,
+ QUEUE_SHIPMENT: app.config.queue_shipment,
+ AUTO_LABEL: app.config.auto_collect_label || 'Automated'
}
},
events: {
'click #add_dewar': 'addDewar',
'click a.send': 'sendShipment',
+ 'click a.return': 'returnShipment',
'click a.pdf': utils.signHandler,
'click a.cancel_pickup': 'cancelPickup',
+ 'click a.queue': 'queueShipment',
},
ui: {
@@ -101,7 +106,7 @@ define(['marionette',
Backbone.ajax({
url: app.apiurl+'/shipment/send/'+this.model.get('SHIPPINGID'),
success: function() {
- self.model.set({ SHIPPINGSTATUS: 'send to DLS' })
+ self.model.set({ SHIPPINGSTATUS: 'sent to facility' })
app.alert({ className: 'message notify', message: 'Shipment successfully marked as sent' })
self.render()
},
@@ -111,7 +116,83 @@ define(['marionette',
})
},
-
+
+ returnShipment: function(e) {
+ e.preventDefault()
+ var self = this
+ Backbone.ajax({
+ url: app.apiurl+'/shipment/return/'+this.model.get('SHIPPINGID'),
+ success: function() {
+ self.model.set({ SHIPPINGSTATUS: 'returned' })
+ self.render()
+ setTimeout(function() {
+ app.alert({ className: 'message notify', message: 'Shipment successfully marked as returned to user' })
+ }, 500)
+
+ },
+ error: function(xhr) {
+ var json = {};
+ if (xhr.responseText) {
+ try {
+ json = JSON.parse(xhr.responseText)
+ } catch(err) {
+
+ }
+ }
+
+ if (json.message) {
+ app.alert({ message: json.message })
+ } else {
+ app.alert({ message: 'Something went wrong marking this shipment returned, please try again' })
+ }
+ },
+
+ })
+ },
+
+ queueShipment: function(e) {
+ e.preventDefault()
+
+ var containers = new Containers()
+ // make sure to return all containers in the shipment
+ containers.state.pageSize = 100
+ containers.queryParams.SHIPPINGID = this.model.get('SHIPPINGID')
+ containers.fetch().done(function () {
+ var promises = []
+ var success = 0
+ var failure = 0
+
+ containers.each(function(c) {
+ promises.push(Backbone.ajax({
+ url: app.apiurl+'/shipment/containers/queue',
+ data: {
+ CONTAINERID: c.get('CONTAINERID')
+ },
+ success: function() {
+ success++
+ },
+ error: function(xhr) {
+ var json = {};
+ if (xhr.responseText) {
+ try {
+ json = JSON.parse(xhr.responseText)
+ } catch(err) {
+
+ }
+ }
+ app.alert({ message: c.get('CONTAINERID') + ': ' + json.message })
+ failure++
+ }
+ }))
+ })
+
+ $.when.apply($, promises).then(function() {
+ app.alert({ message: success+ ' Container(s) Successfully Queued, ' + failure + ' Failed' })
+ }).fail(function() {
+ app.alert({ message: success+ ' Container(s) Successfully Queued, ' + failure + ' Failed' })
+ })
+ })
+ },
addDewar: function(e) {
e.preventDefault()
diff --git a/client/src/js/modules/types/mx/menu.js b/client/src/js/modules/types/mx/menu.js
index d1e37b516..bbff750bc 100644
--- a/client/src/js/modules/types/mx/menu.js
+++ b/client/src/js/modules/types/mx/menu.js
@@ -23,6 +23,7 @@ define([], function() {
},
admin: {
+ 'containers/queued': { title: 'Queue', icon: 'fa-database', permission: 'auto_dash' },
'runs/overview': { title: 'Run Overview', icon: 'fa-bar-chart', permission: 'all_breakdown' },
'stats/overview/beamlines': { title: 'Reporting', icon: 'fa-line-chart', permission: 'all_prop_stats' },
'admin/imaging': { title: 'Imaging', icon: 'fa-image', permission: 'imaging_dash' },
diff --git a/client/src/js/templates/assign/scanassign.html b/client/src/js/templates/assign/scanassign.html
new file mode 100644
index 000000000..75de719b5
--- /dev/null
+++ b/client/src/js/templates/assign/scanassign.html
@@ -0,0 +1,7 @@
+Container Allocation for <%-bl%>
+
+This page is designed to allocate containers via barcode scanning. Click on a sample change position and scan a barcode to assign it to that position. You will be presented with a list of matching containers.
+
+
diff --git a/client/src/js/templates/samples/sample.html b/client/src/js/templates/samples/sample.html
index 59bb555ab..7f17cbb9e 100644
--- a/client/src/js/templates/samples/sample.html
+++ b/client/src/js/templates/samples/sample.html
@@ -134,7 +134,7 @@ Sample Details
<% if (CONTAINERQUEUEID) { %>
- Auto Collect Queued
+ <%-AUTO_LABEL%> Collection Queued
<%-QUEUEDTIMESTAMP%>
diff --git a/client/src/js/templates/shipment/container.html b/client/src/js/templates/shipment/container.html
index 39090755b..10c12e93d 100644
--- a/client/src/js/templates/shipment/container.html
+++ b/client/src/js/templates/shipment/container.html
@@ -7,6 +7,10 @@ Container: <%-NAME%>
This container is currently assigned and in use on a beamline sample changer. Unassign it to make it editable
<% } %>
+
+
+<% if (ENABLE_EXP_PLAN) { %>
+
Plan Fields
+<% } %>
\ No newline at end of file
diff --git a/client/src/js/templates/shipment/containeradd.html b/client/src/js/templates/shipment/containeradd.html
index 74e63b08d..6567999c7 100644
--- a/client/src/js/templates/shipment/containeradd.html
+++ b/client/src/js/templates/shipment/containeradd.html
@@ -45,7 +45,7 @@
Add New Container
- Automated Collection
+ <%-AUTO_LABEL%> Collection
@@ -136,6 +136,10 @@
Add New Container
Clone from First Sample
Clear Puck
+
+ <% if (ENABLE_EXP_PLAN) { %>
+
Plan Fields
+ <% } %>
diff --git a/client/src/js/templates/shipment/containerreview.html b/client/src/js/templates/shipment/containerreview.html
new file mode 100644
index 000000000..9dc21bd16
--- /dev/null
+++ b/client/src/js/templates/shipment/containerreview.html
@@ -0,0 +1,42 @@
+Review: <%-NAME%>
+
+
+This page shows the data collection and sample status of the selected container.
+
+
+
+
+
+
diff --git a/client/src/js/templates/shipment/fromcsv.html b/client/src/js/templates/shipment/fromcsv.html
new file mode 100644
index 000000000..84ae03a6c
--- /dev/null
+++ b/client/src/js/templates/shipment/fromcsv.html
@@ -0,0 +1,19 @@
+
+ Export to CSV
+
+
+ Drop CSV File Here
+
+
+Messages
+
+
+
+
+
+
+
+
+ Import Shipment
+
+
diff --git a/client/src/js/templates/shipment/fromcsvcontainer.html b/client/src/js/templates/shipment/fromcsvcontainer.html
new file mode 100644
index 000000000..eea78f63b
--- /dev/null
+++ b/client/src/js/templates/shipment/fromcsvcontainer.html
@@ -0,0 +1,5 @@
+
diff --git a/client/src/js/templates/shipment/fromcsvtable.html b/client/src/js/templates/shipment/fromcsvtable.html
new file mode 100644
index 000000000..696e046e5
--- /dev/null
+++ b/client/src/js/templates/shipment/fromcsvtable.html
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/client/src/js/templates/shipment/sampletable.html b/client/src/js/templates/shipment/sampletable.html
index e61e0ada6..d78885c6a 100644
--- a/client/src/js/templates/shipment/sampletable.html
+++ b/client/src/js/templates/shipment/sampletable.html
@@ -21,6 +21,16 @@
+
+ Aimed Res
+ Mode
+ Priority
+ Exposure
+ Axis Start
+ Axis Range
+ No. Images
+ Transmission
+ Beamsize
Status
diff --git a/client/src/js/templates/shipment/sampletablenew.html b/client/src/js/templates/shipment/sampletablenew.html
index f253cbbd9..df23ae1b9 100644
--- a/client/src/js/templates/shipment/sampletablenew.html
+++ b/client/src/js/templates/shipment/sampletablenew.html
@@ -23,6 +23,16 @@
+ Aimed Res
+ Mode
+ Priority
+ Exposure
+ Axis Start
+ Axis Range
+ No. Images
+ Transmission
+ Beamsize
+
diff --git a/client/src/js/templates/shipment/sampletablerow.html b/client/src/js/templates/shipment/sampletablerow.html
index bf4d40e61..e63012469 100644
--- a/client/src/js/templates/shipment/sampletablerow.html
+++ b/client/src/js/templates/shipment/sampletablerow.html
@@ -7,6 +7,7 @@
+
<% } else { %>
@@ -59,6 +60,16 @@
+ <%-AIMEDRESOLUTION%>
+ <%-COLLECTIONMODE%>
+ <%-PRIORITY%>
+ <%-EXPOSURETIME%>
+ <%-AXISSTART%>
+ <%-AXISRANGE%>
+ <%-NUMBEROFIMAGES%>
+ <%-TRANSMISSION%>
+ <%-PREFERREDBEAMSIZEX%>
+
<% if (BLSAMPLEID && STATUS) { %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Save Changes
diff --git a/client/src/js/templates/shipment/sampletablerownew.html b/client/src/js/templates/shipment/sampletablerownew.html
index 736a01166..bae0b7c14 100644
--- a/client/src/js/templates/shipment/sampletablerownew.html
+++ b/client/src/js/templates/shipment/sampletablerownew.html
@@ -32,6 +32,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Clone
Clear
diff --git a/client/src/js/templates/shipment/shipment.html b/client/src/js/templates/shipment/shipment.html
index 4ff46e623..07457d67c 100644
--- a/client/src/js/templates/shipment/shipment.html
+++ b/client/src/js/templates/shipment/shipment.html
@@ -13,10 +13,18 @@ Shipment: <%-SHIPPINGNAME%>
<% if (LCOUT && LCRET) { %>
<% } %>
diff --git a/client/src/js/utils/collectionmode.js b/client/src/js/utils/collectionmode.js
new file mode 100644
index 000000000..15ce438c3
--- /dev/null
+++ b/client/src/js/utils/collectionmode.js
@@ -0,0 +1,22 @@
+define([], function() {
+
+ return {
+ opts: function() {
+ return _.map(this.list, function(v,s) { return ''+s+' ' }).join()
+ },
+ obj: function() {
+ return _.invert(this.list)
+ },
+
+ key: function(value) {
+ return _.invert(this.list)[value]
+ },
+
+ list: {
+ 'auto': 'auto',
+ 'manual': 'manual',
+ }
+
+ }
+
+})