diff --git a/api/src/Page/DC.php b/api/src/Page/DC.php index b3e0d4d22..4ab4da7d2 100644 --- a/api/src/Page/DC.php +++ b/api/src/Page/DC.php @@ -1106,8 +1106,13 @@ function _chk_image() return; } + if (!$this->has_arg('pjid')) { + array_push($ids, $this->proposalid); + $where .= ' AND p.proposalid=:'.sizeof($ids); + } + $dct = $this->db->pq("SELECT CONCAT(p.proposalcode, p.proposalnumber, '-', s.visit_number) as vis, dc.datacollectionid as id, dc.startimagenumber, dc.filetemplate, dc.xtalsnapshotfullpath1 as x1, dc.xtalsnapshotfullpath2 as x2, dc.xtalsnapshotfullpath3 as x3, dc.xtalsnapshotfullpath4 as x4,dc.imageprefix as imp, dc.datacollectionnumber as run, dc.imagedirectory as dir, s.visit_number - FROM datacollection dc + FROM datacollection dc INNER JOIN datacollectiongroup dcg ON dcg.datacollectiongroupid = dc.datacollectiongroupid INNER JOIN blsession s ON s.sessionid = dcg.sessionid INNER JOIN proposal p ON p.proposalid = s.proposalid WHERE $where", $ids); diff --git a/api/src/Page/Download.php b/api/src/Page/Download.php index 5072f83bd..5839f8c46 100644 --- a/api/src/Page/Download.php +++ b/api/src/Page/Download.php @@ -89,7 +89,7 @@ function _download_visit() if ($filesystem->exists($data)) { $response = new BinaryFileResponse($data); $response->headers->set("Content-Type", "application/octet-stream"); - $this->_set_disposition_attachment($response, $this->arg('visit') . '_download.zip'); + Utils::setDispositionAttachment($response, $this->arg('visit') . '_download.zip'); $response->send(); } else $this->_error('There doesnt seem to be a data archive available for this visit'); @@ -320,7 +320,7 @@ function _csv_report() WHERE dcg.sessionid=:1 ORDER BY dc.starttime", array($vis['SESSIONID'])); $this->app->response->headers->set("Content-type", "application/vnd.ms-excel"); - $this->_set_disposition_attachment($this->app->response, $vis['ST'] . "_" . $vis['BEAMLINENAME'] . "_" . $this->arg('visit') . ".csv"); + Utils::setDispositionAttachment($this->app->response, $vis['ST'] . "_" . $vis['BEAMLINENAME'] . "_" . $this->arg('visit') . ".csv"); print "Image prefix,Beamline,Run no,Start Time,Sample Name,Protein Acronym,# images, Wavelength (angstrom), Distance (mm), Exp. Time (sec), Phi start (deg), Phi range (deg), Xbeam (mm), Ybeam (mm), Detector resol. (angstrom), Comments\n"; foreach ($rows as $r) { $r['COMMENTS'] = '"' . $r['COMMENTS'] . '"'; @@ -356,7 +356,7 @@ function _dispensing_csv() } $dropsPerWell = $plate["CAPACITY"] / (count($rowNames) * $plate["WELLPERROW"]); $this->app->response->headers->set("Content-type", "text/csv"); - $this->_set_disposition_attachment($this->app->response, $plate["CODE"] . "_targets.csv"); + Utils::setDispositionAttachment($this->app->response, $plate["CODE"] . "_targets.csv"); list($width, $height, $type, $attr) = getimagesize($plate['IMAGEFULLPATH']); foreach ($rows as $r) { if (!isset($r["POSX"]) || !isset($r["POSY"])) { @@ -618,25 +618,12 @@ function set_mime_content($response, $filename, $prefix = null) $response->headers->set("Content-Type", "application/octet-stream"); } if ($this->has_arg('download') && $this->arg('download') < 3) { - $this->_set_disposition_inline($response); + Utils::setDispositionInline($response); } else { - $this->_set_disposition_attachment($response, $saved_filename); + Utils::setDispositionAttachment($response, $saved_filename); } } - function _set_disposition_attachment($response, $filename) { - $response->headers->set("Content-Disposition", - (new ResponseHeaderBag())->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename) - ); - } - - function _set_disposition_inline($response) { - $response->headers->set("Content-Disposition", - (new ResponseHeaderBag())->makeDisposition(ResponseHeaderBag::DISPOSITION_INLINE, '') - ); - } - - # ------------------------------------------------------------------------ # Download Data function _download() @@ -666,7 +653,7 @@ function _download() if ($filesystem->exists($data)) { $response = new BinaryFileResponse($data); $response->headers->set("Content-Type", "application/octet-stream"); - $this->_set_disposition_attachment($response, $this->arg('id') . '_download.zip'); + Utils::setDispositionAttachment($response, $this->arg('id') . '_download.zip'); $response->send(); } else { error_log("Download file " . $data . " not found"); diff --git a/api/src/Page/Processing.php b/api/src/Page/Processing.php index 4abff179b..67e0ec777 100644 --- a/api/src/Page/Processing.php +++ b/api/src/Page/Processing.php @@ -4,11 +4,12 @@ use SynchWeb\Page; use SynchWeb\Downstream\DownstreamProcessing; +use SynchWeb\Utils; class Processing extends Page { public static $dispatch = array( array('/:id', 'get', '_results'), - array('/visit/:visit', 'get', '_results_for_visit'), + array('/visit/:visit(/csv/:csv)', 'get', '_results_for_visit'), array('/status', 'post', '_statuses'), array('/messages/status', 'post', '_ap_message_status'), @@ -43,6 +44,7 @@ class Processing extends Page { 'cloudrunid' => '(\w|\-)+', 'AUTOPROCPROGRAMATTACHMENTID' => '\d+', 'DATACOLLECTIONFILEATTACHMENTID' => '\d+', + 'csv' => '\d+', ); /** @@ -476,12 +478,11 @@ function _results_for_visit() { $jobs = $this->db->pq( "SELECT dc.datacollectionid as id, CONCAT(dc.imageprefix, '_', dc.datacollectionnumber) as prefix, - ".self::EVTOA."/dc.wavelength as energy, - dc.resolution, smp.name as sample, smp.blsampleid, + ".self::EVTOA."/dc.wavelength as energy, + dc.resolution, app.processingprograms as pipeline, - app.autoprocprogramid as aid, REPLACE(ap.spacegroup,' ','') as sg, ap.refinedcell_a as cell_a, ap.refinedcell_b as cell_b, @@ -489,12 +490,12 @@ function _results_for_visit() { ap.refinedcell_alpha as cell_al, ap.refinedcell_beta as cell_be, ap.refinedcell_gamma as cell_ga, - apssover.resolutionlimithigh as overallrhigh, apssover.resolutionlimitlow as overallrlow, - apssinner.resolutionlimithigh as innerrhigh, + apssover.resolutionlimithigh as overallrhigh, apssinner.resolutionlimitlow as innerrlow, - apssouter.resolutionlimithigh as outerrhigh, + apssinner.resolutionlimithigh as innerrhigh, apssouter.resolutionlimitlow as outerrlow, + apssouter.resolutionlimithigh as outerrhigh, apssover.completeness as overallcompleteness, apssinner.completeness as innercompleteness, apssouter.completeness as outercompleteness, @@ -503,7 +504,8 @@ function _results_for_visit() { apssouter.anomalouscompleteness as anomoutercompleteness, apssinner.rmeasalliplusiminus as innerrmeas, apssouter.cchalf as outercchalf, - apssinner.ccanomalous as innerccanom + apssinner.ccanomalous as innerccanom, + app.autoprocprogramid as aid FROM datacollection dc LEFT OUTER JOIN blsample smp ON dc.blsampleid = smp.blsampleid INNER JOIN processingjob pj ON dc.datacollectionid = pj.datacollectionid @@ -534,12 +536,14 @@ function _results_for_visit() { $data = array_slice($data, $start, $pp); // Add classes to highlight fields in red/yellow/green - foreach ($data as &$d) { - foreach (array('OVERALL', 'INNER', 'OUTER') as $s) { - $c = $d[$s.'COMPLETENESS']; - $d[$s.'COMPLETENESSCLASS'] = $c > 95 ? 'active' : ($c > 80 ? 'minor' : 'inactive'); - $c = $d['ANOM'.$s.'COMPLETENESS']; - $d['ANOM'.$s.'COMPLETENESSCLASS'] = $c > 95 ? 'active' : ($c > 80 ? 'minor' : 'inactive'); + if (!$this->has_arg('csv')) { + foreach ($data as &$d) { + foreach (array('OVERALL', 'INNER', 'OUTER') as $s) { + $c = $d[$s.'COMPLETENESS']; + $d[$s.'COMPLETENESSCLASS'] = $c > 95 ? 'active' : ($c > 80 ? 'minor' : 'inactive'); + $c = $d['ANOM'.$s.'COMPLETENESS']; + $d['ANOM'.$s.'COMPLETENESSCLASS'] = $c > 95 ? 'active' : ($c > 80 ? 'minor' : 'inactive'); + } } } @@ -561,13 +565,23 @@ function _results_for_visit() { foreach ($nf as $nff => $cols) { foreach ($cols as $c) { foreach ($data as &$d) { - $d[$c] = number_format($d[$c], $nff); + $d[$c] = number_format($d[$c], $nff, ".", ""); } } } - $this->_output(array('total' => $tot, 'data' => $data)); - + 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 (!empty($data)) { + print implode(',', array_keys($data[0])) . "\n"; + } + foreach ($data as $r) { + print implode(',', array_values($r)) . "\n"; + } + } else { + $this->_output(array('total' => $tot, 'data' => $data)); + } } /** diff --git a/api/src/Utils.php b/api/src/Utils.php index 2fe30b5a8..1bafae4f7 100644 --- a/api/src/Utils.php +++ b/api/src/Utils.php @@ -2,6 +2,7 @@ namespace SynchWeb; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; use InvalidArgumentException; class Utils @@ -52,4 +53,17 @@ public static function filterParamFromUrl($url, $param): string $redirect_url = preg_replace('/(&|\?)'.preg_quote($param).'=[^&]*$/', '', $url); return preg_replace('/(&|\?)'.preg_quote($param).'=[^&]*&/', '$1', $redirect_url); } + + public static function setDispositionAttachment($response, $filename) + { + $response->headers->set('Content-Disposition', + (new ResponseHeaderBag())->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename) + ); + } + + public static function setDispositionInline($response) { + $response->headers->set("Content-Disposition", + (new ResponseHeaderBag())->makeDisposition(ResponseHeaderBag::DISPOSITION_INLINE, '') + ); + } } diff --git a/client/src/js/app/components/navbar.vue b/client/src/js/app/components/navbar.vue index a1558927b..c0ac3dfb5 100644 --- a/client/src/js/app/components/navbar.vue +++ b/client/src/js/app/components/navbar.vue @@ -21,9 +21,9 @@

diff --git a/client/src/js/modules/dc/dclist.js b/client/src/js/modules/dc/dclist.js index 320f003ef..19ca15a94 100644 --- a/client/src/js/modules/dc/dclist.js +++ b/client/src/js/modules/dc/dclist.js @@ -1,6 +1,4 @@ define(['marionette', - //'modules/dc/views/imagestatuscollection', - //'modules/dc/views/apstatuscollection', 'modules/dc/collections/imagestatuses', 'modules/dc/collections/apstatuses', 'modules/dc/collections/apmessagestatuses', @@ -68,8 +66,16 @@ function(Marionette, }, _onSync: function() { - var ids = this.collection.pluck('ID') - this.imagestatuses.fetch({ data: { ids: ids }, type: 'POST' }) + var ids = this.collection + .filter(m => m.get('TYPE') === 'data' || m.get('TYPE') === 'grid') + .map(m => m.get('ID')); + var data = { ids: ids } + var params = this.getOption('params') || {}; + if (params.pjid != null) { + data.pjid = params.pjid + } + if (!ids.length) return + this.imagestatuses.fetch({ data: data, type: 'POST' }) if (this.getOption('apStatus')) this.apstatuses.fetch({ data: { ids: ids }, type: 'POST' }) if (this.getOption('apMessageStatus')) this.apmessagestatuses.fetch({ data: { ids: ids }, type: 'POST' }) }, diff --git a/client/src/js/modules/dc/views/summary.js b/client/src/js/modules/dc/views/summary.js index 193ecec5f..00867d6d4 100644 --- a/client/src/js/modules/dc/views/summary.js +++ b/client/src/js/modules/dc/views/summary.js @@ -23,12 +23,19 @@ define(['backbone', className: 'content', template: template, + templateHelpers: function() { + return { + APIURL: app.apiurl, + } + }, + regions: { wrap: '.wrapper', }, events: { 'click a.dll': utils.signHandler, + 'click a.csv': 'downloadCSV', 'change @ui.pipeline': 'changePipeline', 'change @ui.sg': 'changeSpaceGroup', 'change @ui.minres': 'changeResolution', @@ -56,6 +63,37 @@ define(['backbone', this.visit = options.model.get('VISIT') }, + downloadCSV: function(e) { + e.preventDefault() + + let a = e.target + while (a && a.tagName !== 'A') a = a.parentNode + if (!a) return + + const originalHref = a.href + const url = new URL(originalHref) + + const skip = ['currentPage', 'pageSize', 'totalPages', 'totalRecords', 'sortKey', 'order'] + + for (const [key, val] of Object.entries(this.collection.queryParams)) { + if (skip.includes(key)) continue + if (val == null || typeof val === 'function' || typeof val === 'object') continue + url.searchParams.set(key, val) + } + + if (this.collection.state.sortKey) { + url.searchParams.set('sort_by', this.collection.state.sortKey) + url.searchParams.set('order', this.collection.state.order === 1 ? 'desc' : 'asc') + } + url.searchParams.set('per_page', 9999) + + a.href = url.pathname + url.search + + utils.signHandler(e) + + a.href = originalHref + }, + updateData: function() { this.collection.state['currentPage'] = 1 this.collection.fetch() diff --git a/client/src/js/modules/projects/views/view.js b/client/src/js/modules/projects/views/view.js index d510b29df..441da3e94 100644 --- a/client/src/js/modules/projects/views/view.js +++ b/client/src/js/modules/projects/views/view.js @@ -105,7 +105,7 @@ define(['marionette', edit.create('ACRONYM', 'text') } - this.dc.show(new DCView({ model: this.model, collection: this.dcs, params: { visit: null }, noPageUrl: true, noFilterUrl: true, noSearchUrl: true })) + this.dc.show(new DCView({ model: this.model, collection: this.dcs, params: { visit: null, pjid: this.model.get('PROJECTID') }, noPageUrl: true, noFilterUrl: true, noSearchUrl: true })) this.smp.show(new SampleList({ collection: this.samples, noPageUrl: true, noFilterUrl: true, noSearchUrl: true })) this.prt.show(new ProteinList({ collection: this.proteins, noPageUrl: true, noSearchUrl: true })) diff --git a/client/src/js/templates/dc/summary.html b/client/src/js/templates/dc/summary.html index 165e5e3ea..18626ac34 100644 --- a/client/src/js/templates/dc/summary.html +++ b/client/src/js/templates/dc/summary.html @@ -3,6 +3,7 @@

Data Collection Summary

diff --git a/client/src/js/utils.js b/client/src/js/utils.js index a469e020f..4a2994911 100644 --- a/client/src/js/utils.js +++ b/client/src/js/utils.js @@ -202,7 +202,8 @@ define(['backbone', utils.sign({ url: url, callback: function(resp) { - window.location = url+'?token='+resp.token + const separator = url.includes('?') ? '&' : '?' + window.location = url + separator + 'token=' + resp.token } }) },