diff --git a/api/src/Page/Processing.php b/api/src/Page/Processing.php index 378e78d9b..41413728f 100644 --- a/api/src/Page/Processing.php +++ b/api/src/Page/Processing.php @@ -9,7 +9,7 @@ class Processing extends Page { public static $dispatch = array( array('/:id(/dcg/:dcg)', 'get', '_results'), - array('/visit/:visit(/csv/:csv)', 'get', '_results_for_visit'), + array('/summary(/group/:sgid)(/protein/:pid)(/visit/:visit)(/csv/:csv)', 'get', '_summary'), array('/status', 'post', '_statuses'), array('/messages/status', 'post', '_ap_message_status'), @@ -27,6 +27,8 @@ class Processing extends Page { 'id' => '\d+', 'ids' => '\d+', 'dcg' => '\d+', + 'pid' => '\d+', + 'sgid' => '\d+', 'visit' => '\w+\d+-\d+', 'map' => '\d+', 'n' => '\d+', @@ -382,24 +384,56 @@ function _statuses() { $this->_output($out); } - function _results_for_visit() { - if (!($this->has_arg('visit'))) { - $this->_error('No visit specified'); - } - $pattern = '/([A-z]+)(\d+)-(\d+)/'; - preg_match($pattern, $this->arg('visit'), $matches); - if (!sizeof($matches)) - $this->_error('No such visit'); + function _summary() { + $where = 'dc.overlap = 0 AND dc.axisrange > 0 AND app.processingstatus = 1'; + $join = ''; + if ($this->has_arg('visit')) { + $pattern = '/([A-z]+)(\d+)-(\d+)/'; + preg_match($pattern, $this->arg('visit'), $matches); + if (!sizeof($matches)) + $this->_error('No such visit'); + + $info = $this->db->pq("SELECT s.sessionid FROM blsession s + INNER JOIN proposal p ON (p.proposalid = s.proposalid) + WHERE p.proposalid=:1 AND p.proposalcode=:2 AND p.proposalnumber=:3 AND s.visit_number=:4", + array($this->proposalid, $matches[1], $matches[2], $matches[3]) + ); - $info = $this->db->pq("SELECT s.sessionid FROM blsession s INNER JOIN proposal p ON (p.proposalid = s.proposalid) WHERE p.proposalcode=:1 AND p.proposalnumber=:2 AND s.visit_number=:3", array($matches[1], $matches[2], $matches[3])); + if (!sizeof($info)) { + $this->_error('No such visit'); + } - if (!sizeof($info)) { - $this->_error('No such visit'); - } + $args = array($info[0]['SESSIONID']); + $where .= ' AND dc.sessionid=:1'; + } else if ($this->has_arg('pid')) { + $info = $this->db->pq("SELECT pr.proteinid FROM protein pr + WHERE pr.proposalid=:1 AND pr.proteinid=:2", + array($this->proposalid, $this->arg('pid')) + ); + + if (!sizeof($info)) { + $this->_error('No such protein'); + } + + $args = array($info[0]['PROTEINID']); + $where .= ' AND c.proteinid=:1'; + $join = 'INNER JOIN crystal c on smp.crystalid=c.crystalid'; + } else if ($this->has_arg('sgid')) { + $info = $this->db->pq("SELECT blsg.blsamplegroupid FROM blsamplegroup blsg + WHERE blsg.proposalid=:1 AND blsg.blsamplegroupid=:2", + array($this->proposalid, $this->arg('sgid')) + ); - $args = array($info[0]['SESSIONID']); + if (!sizeof($info)) { + $this->_error('No such sample group'); + } - $where = 'dc.sessionid=:1 AND dc.overlap = 0 AND dc.axisrange > 0 AND app.processingstatus = 1'; + $args = array($info[0]['BLSAMPLEGROUPID']); + $where .= ' AND bhb.blsamplegroupid=:1'; + $join = 'INNER JOIN BLSampleGroup_has_BLSample bhb on smp.blsampleid=bhb.blsampleid'; + } else { + $this->_error('No visit, protein or group specified'); + } if ($this->has_arg('pipeline')) { $st = sizeof($args); @@ -489,6 +523,7 @@ function _results_for_visit() { $jobs = $this->db->pq( "SELECT dc.datacollectionid as id, CONCAT(dc.imageprefix, '_', dc.datacollectionnumber) as prefix, + CONCAT(p.proposalcode, p.proposalnumber, '-', s.visit_number) as visit, smp.name as sample, smp.blsampleid, ".self::EVTOA."/dc.wavelength as energy, @@ -518,7 +553,10 @@ function _results_for_visit() { apssinner.ccanomalous as innerccanom, app.autoprocprogramid as aid FROM datacollection dc + INNER JOIN blsession s ON dc.sessionid = s.sessionid + INNER JOIN proposal p ON s.proposalid = p.proposalid LEFT OUTER JOIN blsample smp ON dc.blsampleid = smp.blsampleid + $join INNER JOIN processingjob pj ON dc.datacollectionid = pj.datacollectionid INNER JOIN autoprocprogram app ON pj.processingjobid = app.processingjobid INNER JOIN autoproc ap ON app.autoprocprogramid=ap.autoprocprogramid @@ -583,7 +621,14 @@ function _results_for_visit() { if ($this->has_arg('csv')) { $this->app->response->headers->set("Content-type", "text/csv"); - Utils::setDispositionAttachment($this->app->response, $this->arg('visit') . "_summary.csv"); + if ($this->has_arg('visit')) { + $filename = 'visit_' . $this->arg('visit') . "_summary.csv"; + } else if ($this->has_arg('pid')) { + $filename = 'protein_' . $this->arg('pid') . "_summary.csv"; + } else { + $filename = 'group_' . $this->arg('sgid') . "_summary.csv"; + } + Utils::setDispositionAttachment($this->app->response, $filename); if (!empty($data)) { print implode(',', array_keys($data[0])) . "\n"; } diff --git a/api/src/Page/Sample.php b/api/src/Page/Sample.php index 87c7eac06..0f49e9b6d 100644 --- a/api/src/Page/Sample.php +++ b/api/src/Page/Sample.php @@ -2010,7 +2010,7 @@ function _distinct_proteins() LEFT OUTER JOIN concentrationtype ct ON ct.concentrationtypeid = pr.concentrationtypeid WHERE pr.acronym is not null AND $where GROUP BY ct.symbol, pr.acronym, pr.name, pr.global - ORDER BY lower(pr.acronym)", $args); + ORDER BY lower(pr.acronym), pr.proteinid", $args); $this->_output($rows); } diff --git a/client/src/css/partials/_content.scss b/client/src/css/partials/_content.scss index d1309b8ed..24017adf7 100644 --- a/client/src/css/partials/_content.scss +++ b/client/src/css/partials/_content.scss @@ -33,6 +33,7 @@ &.nou { border-bottom: 0; + margin-bottom: 0; } &.center { diff --git a/client/src/css/partials/_utility.scss b/client/src/css/partials/_utility.scss index 5ed296780..b29ace198 100644 --- a/client/src/css/partials/_utility.scss +++ b/client/src/css/partials/_utility.scss @@ -386,11 +386,19 @@ ul.ui-autocomplete { @apply tw-flex; @apply tw-flex-row; @apply tw-gap-2; - @apply tw-items-center; + @apply tw-items-baseline; border-bottom: 1px solid grey; h1 { padding: 0 !important; @apply tw-flex-grow; + + &.no-grow { + @apply tw-flex-grow-0; + } + } + + .xl { + @apply tw-text-xl; } } diff --git a/client/src/js/app/views/marionette/marionette-wrapper.vue b/client/src/js/app/views/marionette/marionette-wrapper.vue index a360c4a64..3c6d1af7d 100644 --- a/client/src/js/app/views/marionette/marionette-wrapper.vue +++ b/client/src/js/app/views/marionette/marionette-wrapper.vue @@ -18,7 +18,9 @@ export default { // If this component is used directly from another vue component this will not be called // Instead the parent vue component will need to load the model/collection data as required beforeRouteEnter: function(to, from, next) { - next(vm => vm.prefetchData()) + next(vm => { + if (!vm.fetchOnLoad) vm.prefetchData() + }) }, props: { 'mview': [Function, Promise], // The marionette view could be lazy loaded or static import diff --git a/client/src/js/collections/datacollectionsforvisit.js b/client/src/js/collections/datacollectionsforvisit.js index 711a2cc2b..caabf17f9 100644 --- a/client/src/js/collections/datacollectionsforvisit.js +++ b/client/src/js/collections/datacollectionsforvisit.js @@ -4,25 +4,41 @@ define(['backbone.paginator', 'models/datacollectionsforvisit', 'utils/kvcollect model: DCs, mode: 'server', visit: null, - url: function() { return '/processing/visit/'+this.visit }, + pid: null, + sgid: null, + url: function() { + if (this.pid) { + return '/processing/summary/protein/'+this.pid + } else if (this.visit) { + return '/processing/summary/visit/'+this.visit + } else { + return '/processing/summary/group/'+this.sgid + } + }, initialize(collection, options) { if (options && options.queryParams && options.queryParams.visit) { this.visit = options.queryParams.visit } + if (options && options.queryParams && options.queryParams.pid) { + this.pid = options.queryParams.pid + } + if (options && options.queryParams && options.queryParams.sgid) { + this.sgid = options.queryParams.sgid + } }, - + state: { pageSize: 15, }, - + parseState: function(r, q, state, options) { return { totalRecords: r.total } }, - + parseRecords: function(r, options) { return r.data }, - + })) }) diff --git a/client/src/js/collections/samplegroups.js b/client/src/js/collections/samplegroups.js index 345bfc1b2..6f5d7197a 100644 --- a/client/src/js/collections/samplegroups.js +++ b/client/src/js/collections/samplegroups.js @@ -5,8 +5,9 @@ define(['backbone', 'backbone.paginator', 'models/samplegroup', -], function(Backbone, PageableCollection, SampleGroup) { - return PageableCollection.extend({ + 'utils/kvcollection', +], function(Backbone, PageableCollection, SampleGroup, KVCollection) { + return PageableCollection.extend(_.extend({}, KVCollection, { model: SampleGroup, url: '/sample/groups', @@ -23,5 +24,8 @@ define(['backbone', parseState: function(r) { return { totalRecords: r.total } }, - }) + + keyAttribute: 'NAME', // What the user sees in the dropdown list + valueAttribute: 'BLSAMPLEGROUPID', // The hidden ID submitted when selected + })) }) diff --git a/client/src/js/modules/dc/routes.js b/client/src/js/modules/dc/routes.js index 16440dfcd..481886b22 100644 --- a/client/src/js/modules/dc/routes.js +++ b/client/src/js/modules/dc/routes.js @@ -42,6 +42,18 @@ application.addInitializer(function() { application.navigate('/dc/'+(visit ? ('visit/'+visit) : '') + '/ty/'+type+'/id/'+id) // controller.dc_list(visit, null, null, null, type, id) }) + + application.on('visitsummary:show', function(visit) { + application.navigate('/dc/summary/visit/'+visit) + }) + + application.on('proteinsummary:show', function(pid) { + application.navigate('/dc/summary/protein/'+pid) + }) + + application.on('groupsummary:show', function(pid) { + application.navigate('/dc/summary/group/'+pid) + }) }) // appRoutes: { @@ -110,12 +122,45 @@ let routes = [ id: +route.params.id || null, }), }, + { + path: '/dc/summary/protein/:pid([0-9]+)', + name: 'dc-summary-protein', + component: MarionetteView, + props: route => ({ + mview: Summary, + fetchOnLoad: true, + pid: route.params.pid || '', + options: { + model: new Visit(), + collection: new DCVisit(null, { + queryParams: { pid: route.params.pid } + }) + } + }), + }, + { + path: '/dc/summary/group/:sgid([0-9]+)', + name: 'dc-summary-group', + component: MarionetteView, + props: route => ({ + mview: Summary, + fetchOnLoad: true, + sgid: route.params.sgid || '', + options: { + model: new Visit(), + collection: new DCVisit(null, { + queryParams: { sgid: route.params.sgid } + }) + } + }), + }, { path: '/dc/summary/visit/:visit([a-zA-Z]{2}[0-9]+-[0-9]+)', name: 'dc-summary', component: MarionetteView, props: route => ({ mview: Summary, + fetchOnLoad: true, visit: route.params.visit || '', options: { model: visitModel, diff --git a/client/src/js/modules/dc/views/summary.js b/client/src/js/modules/dc/views/summary.js index 70693afd6..0729365ef 100644 --- a/client/src/js/modules/dc/views/summary.js +++ b/client/src/js/modules/dc/views/summary.js @@ -7,6 +7,9 @@ define(['backbone', 'utils/kvcollection', 'collections/spacegroups', 'collections/processingpipelines', + 'collections/visits', + 'modules/shipment/collections/distinctproteins', + 'collections/samplegroups', 'templates/dc/summary.html'], function(Backbone, Marionette, Backgrid, TableView, utils, @@ -14,6 +17,9 @@ define(['backbone', KVCollection, Spacegroups, ProcessingPipelines, + Visits, + DistinctProteins, + SampleGroups, template) { var Pipelines = Backbone.Collection.extend(_.extend({ @@ -28,6 +34,8 @@ define(['backbone', templateHelpers: function() { return { APIURL: app.apiurl, + PROTEINID: this.protein, + SAMPLEGROUPID: this.group, } }, @@ -38,6 +46,10 @@ define(['backbone', events: { 'click a.dll': utils.signHandler, 'click a.csv': 'downloadCSV', + 'change @ui.typeselect': 'changeType', + 'change @ui.visitselect': 'changeVisit', + 'change @ui.proteinselect': 'changeProtein', + 'change @ui.groupselect': 'changeGroup', 'change @ui.pipeline': 'changePipeline', 'change @ui.sg': 'changeSpaceGroup', 'change @ui.minres': 'changeResolution', @@ -50,6 +62,10 @@ define(['backbone', }, ui: { + typeselect: 'select[name=type-select]', + visitselect: 'select[name=visit-select]', + proteinselect: 'select[name=protein-select]', + groupselect: 'select[name=group-select]', pipeline: 'select[name=pipeline]', sg: 'select[name=SG]', minres: 'input[name=minres]', @@ -62,7 +78,82 @@ define(['backbone', }, initialize: function(options) { - this.visit = options.model.get('VISIT') + this.visit = options.collection.queryParams.visit + this.protein = options.collection.queryParams.pid + this.group = options.collection.queryParams.sgid + this.visits = null + this.proteins = null + this.groups = null + }, + + setType: function() { + if (this.visit) this.ui.typeselect.val('visit') + if (this.protein) this.ui.typeselect.val('protein') + if (this.group) this.ui.typeselect.val('group') + this.changeType() + }, + + changeType: function() { + if (this.ui.typeselect.val() == 'visit') { + this.ui.visitselect.show() + this.ui.proteinselect.hide() + this.ui.groupselect.hide() + if (!this.visits) { + this.visits = new Visits(null, { state: { pageSize: 9999 } }) + this.visits.fetch().done(this.updateVisits.bind(this)) + } + } else if (this.ui.typeselect.val() == 'protein') { + this.ui.visitselect.hide() + this.ui.proteinselect.show() + this.ui.groupselect.hide() + if (!this.proteins) { + this.proteins = new DistinctProteins(null, { state: { pageSize: 9999 } }) + this.proteins.fetch().done(this.updateProteins.bind(this)) + } + } else { + // sample group + this.ui.visitselect.hide() + this.ui.proteinselect.hide() + this.ui.groupselect.show() + if (!this.groups) { + this.groups = new SampleGroups(null, { state: { pageSize: 9999 } }) + this.groups.queryParams.groupSamplesType = 'BLSAMPLEGROUPID' + this.groups.fetch().done(this.updateGroups.bind(this)) + } + } + }, + + changeVisit: function() { + const selectedModel = this.visits.findWhere({ SESSIONID: this.ui.visitselect.val() }); + if (selectedModel) { + app.trigger('visitsummary:show', selectedModel.get('VISIT')); + } + }, + + changeProtein: function() { + app.trigger('proteinsummary:show', this.ui.proteinselect.val()); + }, + + changeGroup: function() { + app.trigger('groupsummary:show', this.ui.groupselect.val()); + }, + + updateVisits: function() { + this.ui.visitselect.html(''+this.visits.opts()) + const model = this.visits.findWhere({ VISIT: this.visit }) + if (model) { + this.ui.visitselect.val(model.get('SESSIONID')) + } + }, + + updateProteins: function() { + this.ui.proteinselect.html(''+this.proteins.opts()) + this.ui.proteinselect.val(this.protein) + }, + + updateGroups: function() { + this.ui.groupselect.html(''+this.groups.opts()) + this.ui.groupselect.val(this.group) }, downloadCSV: function(e) { @@ -194,6 +285,7 @@ define(['backbone', onRender: function() { this.showSpaceGroups() + this.setType() this.processing_pipelines = new ProcessingPipelines() this.processing_pipelines.fetch({ @@ -204,7 +296,7 @@ define(['backbone', }).done(this.updatePipelines.bind(this)); var columns = [ - { label: '', cell: table.TemplateCell, editable: false, template: ' View Data Collection' }, + { label: '', cell: table.TemplateCell, editable: false, template: ' View Data Collection' }, { name: 'PREFIX', label: 'Prefix', cell: 'string', editable: false }, { name: 'SAMPLE', label: 'Sample', cell: table.TemplateCell, template: '<%-SAMPLE%>', editable: false }, { name: 'ENERGY', label: 'Energy (eV)', cell: 'string', editable: false }, diff --git a/client/src/js/modules/samples/components/sample-groups.vue b/client/src/js/modules/samples/components/sample-groups.vue index 6ef9b5ab3..39712d394 100644 --- a/client/src/js/modules/samples/components/sample-groups.vue +++ b/client/src/js/modules/samples/components/sample-groups.vue @@ -48,6 +48,7 @@