source: trunk/grails-app/controllers/nl/tno/massSequencing/AssayController.groovy @ 63

Last change on this file since 63 was 63, checked in by robert@…, 8 years ago

Improved querying (#40)

  • Removed continuous synchronization while importing data
  • Added action buttons to search result screen
  • Implemented authorization checks while searching
File size: 20.7 KB
Line 
1package nl.tno.massSequencing
2
3import java.util.List;
4import grails.converters.JSON
5
6import org.codehaus.groovy.grails.commons.ConfigurationHolder
7
8class AssayController {
9        def synchronizationService
10        def gscfService
11        def fuzzySearchService
12        def dataTablesService
13
14        def fileService
15        def excelService
16        def sampleExcelService
17        def fastaService
18
19        def index = {
20                // Filter studies for the ones the user is allowed to see
21                def studies = Study.list();
22                [studies: studies.findAll { it.canRead( session.user ) },
23                        gscfAddUrl: gscfService.urlAddStudy() ]
24        }
25       
26        /**
27        * Returns JSON data for the datatable with runs
28        * @see http://www.datatables.net/usage/server-side
29        * @see DataTablesService.retrieveData
30        */
31   def showAssayList = {
32           // Determine the total number of assaysamples for this run
33           def ids = Assay.executeQuery( "SELECT a.id FROM Assay a WHERE a.study.trashcan = false")
34           int total = ids.size();
35
36           // Which columns are shown on screen and should be retrieved from the database
37           def columns = [
38                   "a.id",
39                   "a.name",
40                   "a.study.name",
41                   "COUNT( DISTINCT s )",
42                   "SUM( s.numSequences ) / COUNT( DISTINCT s )",
43                   "a.study.studyToken"
44           ]
45           
46           def groupColumns = columns[0..2] + columns[ 5 ];
47           def orderByMapping = null;
48           
49           // Retrieve data from assaySample table
50           def from = "Assay a LEFT JOIN a.assaySamples s"
51           def where = " EXISTS( FROM Auth auth WHERE auth.study = a.study AND auth.user = :user AND auth.canRead = true )"
52           def parameters = [ "user": session.user ] 
53           
54           // This closure determines what to do with a row that is retrieved from the database.
55           def convertClosure = {
56                   def assayId = it[ 0 ];
57                   def assayName = it[ 1 ];
58                   def studyName = it[ 2 ];
59                   def numSamples = it[ 3 ];
60                   def numSequences = it[ 4 ];
61                   def studyToken = it[ 5 ];
62                   
63                   // Create buttons in the last three columns
64                   def editButton = g.link( controller: "assay", action: "show", id: assayId, "title": "View assay"  ) { '<img src="' + fam.icon( name: 'application_form_magnify' ) + '" title="View assay" />' };
65                   def chartButton = '';
66                   
67                   if( numSequences > 0 ) {
68                           chartButton = g.link( controller: "assay", action: "sequenceLengthHistogram", id: assayId, title: "Sequence length histogram" ) { '<img src="' + fam.icon( name: 'chart_bar' ) + '" alt="Sequence length histogram" title="Sequence length histogram" />' }
69                   } else {
70                           chartButton = '<img src="' + fam.icon( name: 'chart_bar' ) + '" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />'
71                   }
72                   
73                   [
74                           g.checkBox( name: "ids", value: assayId, checked: false, onClick: "updateCheckAll(this);" ),
75                           g.link( title:"View assay", controller:"assay", action:"show", id: assayId ) { assayName },  // it.name
76                           g.link( title:"View study", url: gscfService.urlViewStudy( studyToken )) { studyName },      // it.study.name
77                           numSamples > 0 ? g.formatNumber( number: numSamples, format: "###,###,##0" ) : "-",  // it.numSequences(),
78                           numSequences > 0 ? g.formatNumber( number: numSequences, format: "###,###,##0" ) : "-",      // it.numQualScores(),
79                           editButton,
80                           chartButton
81                   ]
82           }
83           
84           // Send the data to the user
85           render dataTablesService.retrieveData(
86                   params,
87                   Assay.class,
88                   convertClosure,
89                   columns,
90                   groupColumns,
91                   from,
92                   total,
93                   ids,
94                   orderByMapping,
95                   where,
96                   parameters
97           ) as JSON
98   }
99
100        def show = {
101                def assay = getAssay( params.id );
102                if( !assay )
103                        return
104
105                // Make sure the newest data is available
106                synchronizationService.sessionToken = session.sessionToken
107                synchronizationService.synchronizeAssay( assay );
108
109                if( !assay.study || !assay.study.canRead( session.user ) ) {
110                        flash.error = "You don't have sufficient privileges to access assay " + assay.name
111                        redirect( action: "index" );
112                        return;
113                }
114               
115                // Find statistics for all assaySamples in order to improve performance
116                AssaySample.initStats( assay.assaySamples?.toList() )
117               
118                // Determine runs not used in this assay
119                def otherRuns = Run.list( sort: "name" ).findAll { !it.assays.contains( assay ) }
120
121                // Send the assay information to the view
122                [assay: assay, editable: assay.study.canWrite( session.user ), otherRuns: otherRuns]
123        }
124       
125        /**
126        * Returns JSON data for the datatable with assaysamples
127        * @see http://www.datatables.net/usage/server-side
128        * @see DataTablesService.retrieveData
129        */
130   def showSampleData = {
131           // load run with id specified by params.id
132           def assay = getAssay( params.id );
133
134           if (!assay) {
135                   response.sendError(404, "Run not found" )
136                   return
137           }
138           
139           // Determine the total number of assaysamples for this run
140           def ids = Run.executeQuery( "SELECT s.id FROM AssaySample s WHERE s.assay.id = :assayId", [ "assayId": assay.id ] );
141           def total = ids.size();
142
143           // Which columns are shown on screen and should be retrieved from the database
144           def columns = [
145                   "s.id",
146                   "s.sample.name",
147                   "run.name",
148                   "s.fwMidName",
149                   "SUM( sd.numSequences )",
150                   "SUM( CASE WHEN sd.qualityFile IS NULL THEN 0 ELSE sd.numSequences END )",
151                   's.assay.id',
152                   's.assay.study.id',
153           ]
154           
155           def groupColumns = columns[0..3] + columns[6..7];
156           def orderByMapping = null;   // Meaning: order by column 2 on screen = order by column 1 in the table (screen starts at column 0, table starts at column 1 )
157           
158           // Retrieve data from assaySample table
159           def from = "AssaySample s LEFT JOIN s.sequenceData as sd LEFT JOIN s.run as run"
160
161           // And filter by runId
162           def where = "s.assay.id = :assayId"
163           def parameters = [ "assayId": assay.id ];
164           
165           def canWrite = assay.study.canWrite( session.user );
166
167           // This closure determines what to do with a row that is retrieved from the database.
168           def convertClosure = {
169                   def sampleId = it[ 0 ];
170                   def sampleName = it[ 1 ];
171                   def assayId = it[ 6 ];
172                   def studyId = it[ 7 ];
173                   def numSequences = it[ 4 ];
174                   
175                   // Create buttons in the last three columns
176                   def editButton = '';
177                   def chartButton = '';
178                   
179                   if( canWrite ) {
180                           editButton = g.link( url: '#', title: "Edit sample", onClick: "showEditSampleDialog(" + sampleId + ", 'assay', " + assayId + ");"  ) { '<img src="' + fam.icon( name: 'pencil' ) + '" title="Edit sample" />' };
181                   } else {
182                           editButton = '<img src="' + fam.icon( name: 'pencil' ) + '" title="You can\'t edit this sample because you don\'t have sufficient privileges." class="disabled" />';
183                   }
184                   
185                   if( numSequences > 0 ) {
186                           chartButton = g.link( controller: "assaySample", action: "sequenceLengthHistogram", id: sampleId, title: "Sequence length histogram" ) { '<img src="' + fam.icon( name: 'chart_bar' ) + '" alt="Sequence length histogram" title="Sequence length histogram" />' }
187                   } else {
188                           chartButton = '<img src="' + fam.icon( name: 'chart_bar' ) + '" class="disabled" alt="No histogram available because no sequences are uploaded." title="No histogram available because no sequences are uploaded." />'
189                   }
190                   
191                   [
192                           g.checkBox( name: "ids", value: sampleId, checked: false, onClick: "updateCheckAll(this);" ),
193                           g.link( url: "#", onClick:"showSample( " + sampleId + ", 'run' );", title: "Show sample details" ) { sampleName },   // it.sample.name
194                           it[ 2 ],     // it.run.name
195                           it[ 3 ],     // it.fwMidName
196                           it[ 4 ] > 0 ? g.formatNumber( number: it[ 4 ], format: "###,###,##0" ) : "-",        // it.numSequences(),
197                           it[ 5 ] > 0 ? g.formatNumber( number: it[ 5 ], format: "###,###,##0" ) : "-",        // it.numQualScores(),
198                           editButton,
199                           chartButton
200                   ]
201           }
202           
203           // Send the data to the user
204           render dataTablesService.retrieveData(
205                   params,
206                   AssaySample.class,
207                   convertClosure,
208                   columns,
209                   groupColumns,
210                   from,
211                   total,
212                   ids,
213                   orderByMapping,
214                   where,
215                   parameters
216           ) as JSON
217   }
218       
219        def sequenceLengthHistogram = {
220                redirect( controller: "assaySample", action: "sequenceLengthHistogramForAssay", id: params.id );
221        }
222
223
224        def showByToken = {
225                // load study with token specified by param.id
226                def assay = Assay.findByAssayToken(params.id as String)
227
228                if (!assay) {
229                        // Initialize synchronizationService
230                        synchronizationService.sessionToken = session.sessionToken
231                        synchronizationService.user = session.user
232
233                        // If the assay is not found, synchronize studies and try again
234                        synchronizationService.synchronizeStudies();
235
236                        assay = Assay.findByAssayToken(params.id as String)
237
238                        if (!assay) {
239                                // If the assay is not found, synchronize all studies and try again, since we might be out of sync
240                                synchronizationService.eager = true
241                                synchronizationService.synchronizeStudies();
242
243                                assay = Assay.findByAssayToken(params.id as String)
244
245                                // If after synchronization still no assay is found, show an error
246                                if( !assay ) {
247                                        flash.message = "No assay found with token: $params.id"
248                                        redirect(controller: 'study')
249                                }
250                        }
251                }
252
253                if (!assay.study.canRead( session.user ) ) {
254                        flash.error = "You don't have the right authorizaton to access assay " + assay.name
255                        redirect(action: 'index')
256                        return null
257                }
258               
259                redirect( action: "show", id: assay.id );
260        }
261
262        /**************************************************************************
263         *
264         * Method for handling data about samples for this assay
265         *
266         *************************************************************************/
267
268        /**
269         * Downloads an excel sheet with data about the assay samples, to enter data in excel
270         */
271        def downloadTagsExcel = {
272                // load study with id specified by param.id
273                def assay = getAssay( params.id );
274                if( !assay )
275                        return
276
277                def filename = assay.study.name + "_" + assay.name + "_tags.xls"
278                def wb = sampleExcelService.downloadSampleExcel( assay.assaySamples );
279
280                // Make file downloadable
281                log.trace( "Creation for downloading the file " + filename )
282                sampleExcelService.excelService.downloadFile( wb, filename, response )
283        }
284
285       
286        /**
287        * Downloads an example excel sheet to describe the format of a file-matching sheet. This
288        * file is used when uploading sequence files.
289        */
290   def downloadMatchExcel = {
291           def assay = getAssay( params.id );
292
293           if( !assay ) {
294                   redirect(controller: 'assay')
295                   return
296           }
297
298           def filename = "Assay " + assay.name + "_filenames.xls"
299           def wb = sampleExcelService.downloadMatchExcel( assay.assaySamples );
300
301           // Make file downloadable
302           log.trace( "Creation for downloading the file " + filename )
303           sampleExcelService.excelService.downloadFile( wb, filename, response )
304   }
305
306       
307        /**
308         * Parses an uploaded excel file and shows a form to match columns
309         */
310        def parseTagExcel = {
311                def assay = getAssay( params.id, true );
312                if( !assay )
313                        return
314
315                def filename = params.filename
316
317                // Security check to prevent accessing files in other directories
318                if( !filename || filename.contains( '..' ) ) {
319                        response.status = 500;
320                        render "Invalid filename given";
321                        return;
322                }
323
324                // Check for existence and readability
325                File file = new File( fileService.getUploadDir(), filename)
326
327                if( !file.exists() || !file.canRead() ) {
328                        response.status = 404;
329                        render "The uploaded file doesn't exist or doesn't work as expected.";
330                        return;
331                }
332
333                // Save the filename in session for later use
334                session.filename = filename;
335                def excelData;
336                try {
337                        excelData = sampleExcelService.parseTagsExcel( file );
338                } catch( Throwable e ) { // Catch a throwable here instead of an exception, since the apache poi stuff gives an Error on failure
339                        // Couldn't create a workbook from this file.
340                        response.status = 400 // Bad request
341                        render "Uploaded file is not a valid excel file: " + e.getMessage()
342                        return
343                }
344                session.possibleFields = excelData.possibleFields
345
346                [assay: assay, headers: excelData.headers, exampleData: excelData.exampleData, filename: filename, possibleFields: [ "Don't import" ] + excelData.possibleFields, bestMatches: excelData.bestMatches]
347        }
348
349        /**
350         * Updates the assay samples based on the given excel file and the column matches
351         */
352        def updateTagsByExcel = {
353                def assay = getAssay( params.id, true );
354                if( !assay ) {
355                        // Now delete the file, since we don't need it anymore
356                        _deleteUploadedFileFromSession()
357                        return;
358                }
359
360                if( !session.filename ) {
361                        // Now delete the file, since we don't need it anymore
362                        _deleteUploadedFileFromSession()
363
364                        flash.error = "No excel file found because session timed out. Please try again."
365                        redirect( action: 'show', id: params.id)
366                        return
367                }
368
369                // Determine the match-columns
370                def matchColumns = params[ 'matches'];
371
372                // Now loop through the excel sheet and update all samples with the specified data
373                File file = new File( fileService.getUploadDir(), session.filename );
374
375                if( !file.exists() || !file.canRead() ) {
376                        flash.error = "Excel file has been removed since previous step. Please try again."
377                        redirect( action: 'show', id: params.id)
378                        return
379                }
380
381                def excelData = sampleExcelService.updateTagsByExcel( matchColumns, session.possibleFields, file, assay.assaySamples );
382
383                // Return a message to the user
384                if( !excelData.success ) {
385                        flash.error = excelData.message
386                } else if( excelData.numSuccesful == 0 ) {
387                        flash.error = "None of the " + excelData.failedRows.size() + " row(s) could be imported, because none of the sample names matched. Have you provided the right excel file?"
388                } else {
389                        flash.message = excelData.numSuccesful + " samples have been updated. "
390
391                        if( excelData.failedRows.size() > 0 )
392                                flash.message += excelData.failedRows.size() + " row(s) could not be imported, because the sample names could not be found in the database."
393                }
394               
395                // Now delete the file, since we don't need it anymore
396                _deleteUploadedFileFromSession()
397
398                redirect( action: 'show', id: params.id )
399        }
400
401        /**
402         * Update the properties of the assay samples manually
403         */
404        def updateTagsManually = {
405                def assay = getAssay( params.id, true );
406                if( !assay )
407                        return
408
409                // Loop through all assay samples and set data
410                def sampleParams = params.assaySample;
411
412                if( sampleParams ) {
413                        assay.assaySamples.each { assaySample ->
414                                def assaySampleParams = sampleParams.get( assaySample.id as String );
415
416                                if( assaySampleParams ) {
417                                        sampleExcelService.variableFields.each { k, v ->
418                                                assaySample[ k ] = assaySampleParams[ k ];
419                                        }
420                                        assaySample.save()
421                                       
422                                        try {
423                                                assaySample.run = Run.get( assaySampleParams.run as Long );
424                                        } catch( Exception e ) {}
425
426                                        assaySample.save()
427                                }
428                        }
429                }
430
431                flash.message = "Data about samples is saved."
432                redirect( action: 'show', id: params.id )
433        }
434
435        /**************************************************************************
436         *
437         * Methods for handling data about runs for this assay
438         *
439         *************************************************************************/
440
441        /**
442         * Adds existing runs to this assay
443         */
444        def addExistingRuns = {
445                def assay = getAssay( params.id, true );
446                if( !assay )
447                        return
448
449                // Add checked runs to this assay
450                def runs = params.runs
451                if( runs instanceof String ) {
452                        runs = [ runs ]
453                }
454
455                def numAdded = 0;
456                runs.each { run_id ->
457                        try {
458                                def run = Run.findById( run_id as Long )
459                                if( run.assays == null || !run.assays.contains( assay ) ) {
460                                        run.addToAssays( assay );
461                                        numAdded++;
462                                }
463                        } catch( Exception e ) {}
464                }
465
466                flash.message = numAdded + " runs are added to this assay."
467                redirect( action: 'show', id: params.id)
468        }
469
470        /**
471         * Removes a run from this assay
472         */
473        def removeRun = {
474                def assay = getAssay( params.id, true );
475                if( !assay )
476                        return
477
478                if( !params.run_id ) {
479                        flash.message = "No run id given"
480                        redirect(action: 'show', id: params.id)
481                        return
482                }
483
484                def run
485
486                try {
487                        run = Run.findById( params.run_id as Long )
488                } catch( Exception e ) {
489                        throw e
490                        flash.message = "Incorrect run id given: "
491                        redirect(action: 'show', id: params.id)
492                        return
493                }
494
495                if( assay.runs.contains( run ) ) {
496                        assay.removeFromRuns( run );
497                        flash.message = "The run has been removed from this assay."
498                } else {
499                        flash.message = "The given run was not associated with this assay."
500                }
501
502                redirect( action: 'show', id: params.id)
503        }
504
505        def errorPage = {
506                render "An error has occured. $flash.message"
507        }
508
509        /**
510         * Deletes an uploaded file for which the filename is given in the session.
511         * @return
512         */
513        def _deleteUploadedFileFromSession() {
514                if( !session.filename )
515                        return
516
517                // Now delete the file, since we don't need it anymore
518                fileService.delete( session.filename  )
519                session.filename = ''
520        }
521       
522       
523        /**
524         * Deletes all sequences for a given assay
525         * @param assayId       Id of the assay we are in
526         * @param ids           List of ids of the assaysamples to delete the sequences from
527         */
528        def deleteSequenceData = {
529                // Determine the assay we are in
530                Assay assay = getAssay( params.assayId );
531               
532                if( !assay ) {
533                        redirect(controller: 'assay', action: 'index')
534                        return
535                }
536
537                // Find the selected assaysamples
538                def ids = params.list( 'ids' );
539                ids = ids.findAll { it.isLong() }.collect { Long.parseLong( it ) }
540                def assaySamples = ids.collect { AssaySample.get( it ) }.findAll { it }
541
542                if( !assaySamples ) {
543                        flash.message = "No samples selected"
544                        redirect( controller: 'assay', action: 'show', id: assay.id );
545                        return;
546                }
547               
548                def numFiles = fastaService.deleteSequenceData( assaySamples );
549               
550                flash.message = numFiles + " files have been removed from the assay.";
551                redirect( controller: 'assay', action: 'show', id: assay.id );
552        }
553
554        /**
555         * Exports data about one or more assays in fasta format
556         */
557        def exportAsFasta = {
558                def assaySamples = getAssaySamples( params );
559                def name
560
561                if( assaySamples == null ) {
562                        return
563                } else if( assaySamples*.assay.unique().size() == 1 ) {
564                        name = "Assay_" + assaySamples[0].assay?.name?.replace( ' ', '_' );
565                } else {
566                        name = "assays";
567                }
568
569                // Export the sequences and quality scores
570                response.setHeader "Content-disposition", "attachment; filename=" + name.trim() + ".zip"
571                try {
572                        fastaService.export( assaySamples.unique(), response.getOutputStream() );
573                        response.outputStream.flush();
574                } catch( Exception e ) {
575                        log.error( "Exception occurred during export of sequences. Probably the user has cancelled the download." );
576                        e.printStackTrace();
577                }
578        }
579
580        /**
581         * Export metadata of selected samples in excel format
582         */
583        def exportMetaData = {
584                def assaySamples = getAssaySamples( params );
585                def name
586
587                if( assaySamples == null ) {
588                        return
589                } else if( assaySamples*.assay.unique().size() == 1 ) {
590                        name = "Assay_" + assaySamples[0].assay?.name?.replace( ' ', '_' );
591                } else {
592                        name = "assays";
593                }
594
595                // Export the metadata
596                response.setHeader "Content-disposition", "attachment; filename=${name}.xls"
597                try {
598                        // The export functionality needs a assaySample-tag list, but it
599                        // should be empty when only exporting metadata
600                        def tags = [];
601                        assaySamples.unique().each { assaySample ->
602                                tags << [assaySampleId: assaySample.id, sampleName: assaySample.sample.name, assayName: assaySample.assay.name, studyName: assaySample.assay.study.name, tag: "-"]
603                        }
604                        sampleExcelService.exportExcelSampleData( assaySamples.unique(), tags, response.getOutputStream() );
605                        response.outputStream.flush();
606                } catch( Exception e ) {
607                        log.error( "Exception occurred during export of metadata. Probably the user has cancelled the download." );
608                        e.printStackTrace();
609                }
610        }
611
612       
613        /**
614         * Retrieves an assay from the database, based on the assay ID given
615         * @param assayId               ID of the assay
616         * @param writeAccess   True if you require write access to this assay. The system will check for sufficient privileges
617         * @return
618         */
619        protected Assay getAssay(def assayId, boolean writeAccess = false ) {
620                // load study with id specified by param.id
621                def assay
622                try {
623                        assay = Assay.get(assayId as Long)
624                } catch( Exception e ) {
625                        flash.error = "Incorrect id given: " + assayId
626                        redirect(action: 'index')
627                        return null
628                }
629
630                if (!assay) {
631                        flash.error = "No assay found with id: " + assayId
632                        redirect(action: 'index')
633                        return null
634                }
635
636                if ( !assay.study.canRead( session.user ) || ( writeAccess && !assay.study.canWrite( session.user ) ) ) {
637                        flash.error = "You don't have the right authorizaton to access assay " + assay.name
638                        redirect(action: 'index')
639                        return null
640                }
641               
642                return assay
643        }
644
645       
646        protected List getAssaySamples( params ) {
647                def tokens = params.list( 'tokens' );
648                def ids = params.list( 'ids' );
649                def name;
650
651                ids = ids.findAll { it.isLong() }.collect { Long.parseLong( it ) }
652
653                if( !tokens && !ids ) {
654                        def message = "No assay tokens or ids given"
655                        flash.error = message
656                        redirect( action: "index" );
657                        return;
658                }
659
660                def assaySamples = [];
661
662                // Determine which assaySamples to export
663                def assay;
664                tokens.each { token ->
665                        assay = Assay.findByAssayToken( token );
666                        if( assay && assay.study.canRead( session.user ) )
667                                assaySamples += assay.assaySamples
668                }
669                ids.each { id ->
670                        assay = Assay.get( id );
671                        if( assay && assay.study.canRead( session.user ) )
672                                assaySamples += assay.assaySamples
673                }
674
675                return assaySamples;
676        }
677
678
679}
Note: See TracBrowser for help on using the repository browser.