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

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