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

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