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

Last change on this file since 59 was 59, checked in by robert@…, 10 years ago
  • Improved speed by using ajax calls in pagination
  • Importing taxonomy files now works by adding it to sequenceData objects
File size: 20.5 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           int total = Assay.executeQuery( "SELECT COUNT(*) FROM Assay a WHERE a.study.trashcan = false")[ 0 ]
34
35           // Which columns are shown on screen and should be retrieved from the database
36           def columns = [
37                   null,
38                   "a.name",
39                   "a.study.name",
40                   "COUNT( DISTINCT s )",
41                   "SUM( sd.numSequences ) / COUNT( DISTINCT s )",
42                   null,
43                   null,
44                   "a.study.studyToken"
45           ]
46           
47           def idColumn = 'a.id';
48           def groupColumns = [ idColumn ] + columns[1..2] + columns[ 7 ];
49
50           // Retrieve data from assaySample table
51           def from = "Assay a LEFT JOIN a.assaySamples s LEFT JOIN s.sequenceData sd"
52           def where = " EXISTS( FROM Auth auth WHERE auth.study = a.study AND auth.user = :user AND auth.canRead = true )"
53           def parameters = [ "user": session.user ] 
54           
55           // This closure determines what to do with a row that is retrieved from the database.
56           def convertClosure = {
57                   def assayId = it[ 0 ];
58                   def assayName = it[ 1 ];
59                   def studyName = it[ 2 ];
60                   def numSamples = it[ 3 ];
61                   def numSequences = it[ 4 ];
62                   def studyToken = it[ 5 ];
63                   
64                   // Create buttons in the last three columns
65                   def editButton = g.link( controller: "assay", action: "show", id: assayId, "title": "View assay"  ) { '<img src="' + fam.icon( name: 'application_form_magnify' ) + '" title="View assay" />' };
66                   def chartButton = '';
67                   
68                   if( numSequences > 0 ) {
69                           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" />' }
70                   } else {
71                           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." />'
72                   }
73                   
74                   [
75                           g.checkBox( name: "ids", value: assayId, checked: false, onClick: "updateCheckAll(this);" ),
76                           g.link( title:"View assay", controller:"assay", action:"show", id: assayId ) { assayName },  // it.name
77                           g.link( title:"View study", url: gscfService.urlViewStudy( studyToken )) { studyName },      // it.study.name
78                           numSamples > 0 ? g.formatNumber( number: numSamples, format: "###,###,##0" ) : "-",  // it.numSequences(),
79                           numSequences > 0 ? g.formatNumber( number: numSequences, format: "###,###,##0" ) : "-",      // it.numQualScores(),
80                           editButton,
81                           chartButton
82                   ]
83           }
84           
85           // Send the data to the user
86           render dataTablesService.retrieveData(
87                   params,
88                   Assay.class,
89                   convertClosure,
90                   columns,
91                   groupColumns,
92                   idColumn,
93                   from,
94                   total,
95                   where,
96                   parameters
97           ) as JSON
98   }
99
100
101        def show = {
102                def assay = getAssay( params.id );
103                if( !assay )
104                        return
105
106                // Make sure the newest data is available
107                synchronizationService.sessionToken = session.sessionToken
108                synchronizationService.synchronizeAssay( assay );
109
110                if( !assay.study || !assay.study.canRead( session.user ) ) {
111                        flash.error = "You don't have sufficient privileges to access assay " + assay.name
112                        redirect( action: "index" );
113                        return;
114                }
115               
116                // Find statistics for all assaySamples in order to improve performance
117                AssaySample.initStats( assay.assaySamples?.toList() )
118               
119                // Determine runs not used in this assay
120                def otherRuns = Run.list( sort: "name" ).findAll { !it.assays.contains( assay ) }
121
122                // Send the assay information to the view
123                [assay: assay, editable: assay.study.canWrite( session.user ), otherRuns: otherRuns]
124        }
125       
126        /**
127        * Returns JSON data for the datatable with assaysamples
128        * @see http://www.datatables.net/usage/server-side
129        * @see DataTablesService.retrieveData
130        */
131   def showSampleData = {
132           // load run with id specified by params.id
133           def assay = getAssay( params.id );
134
135           if (!assay) {
136                   response.sendError(404, "Run not found" )
137                   return
138           }
139           
140           // Determine the total number of assaysamples for this run
141           def total = (int) Run.executeQuery( "SELECT COUNT(*) FROM AssaySample s WHERE s.assay.id = :assayId", [ "assayId": assay.id ] )[ 0 ];
142
143           // Which columns are shown on screen and should be retrieved from the database
144           def columns = [
145                   null,
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                   null,
152                   null,
153                   's.assay.id',
154                   's.assay.study.id',
155           ]
156           
157           def idColumn = 's.id';
158           def groupColumns = [ idColumn ] + columns[1..3] + columns[8..9];
159
160           // Retrieve data from assaySample table
161           def from = "AssaySample s LEFT JOIN s.sequenceData as sd LEFT JOIN s.run as run"
162
163           // And filter by runId
164           def where = "s.assay.id = :assayId"
165           def parameters = [ "assayId": assay.id ];
166           
167           def canWrite = assay.study.canWrite( session.user );
168
169           // This closure determines what to do with a row that is retrieved from the database.
170           def convertClosure = {
171                   def sampleId = it[ 0 ];
172                   def assayId = it[ 6 ];
173                   def studyId = it[ 7 ];
174                   def numSequences = it[ 4 ];
175                   
176                   // Create buttons in the last three columns
177                   def editButton = '';
178                   def chartButton = '';
179                   
180                   if( canWrite ) {
181                           editButton = g.link( url: '#', title: "Edit sample", onClick: "showEditSampleDialog(" + sampleId + ", 'assay', " + assayId + ");"  ) { '<img src="' + fam.icon( name: 'pencil' ) + '" title="Edit sample" />' };
182                   } else {
183                           editButton = '<img src="' + fam.icon( name: 'pencil' ) + '" title="You can\'t edit this sample because you don\'t have sufficient privileges." class="disabled" />';
184                   }
185                   
186                   if( numSequences > 0 ) {
187                           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" />' }
188                   } else {
189                           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." />'
190                   }
191                   
192                   [
193                           g.checkBox( name: "ids", value: sampleId, checked: false, onClick: "updateCheckAll(this);" ),
194                           it[ 1 ],     // it.sample.name
195                           it[ 2 ],     // it.run.name
196                           it[ 3 ],     // it.fwMidName
197                           it[ 4 ] > 0 ? g.formatNumber( number: it[ 4 ], format: "###,###,##0" ) : "-",        // it.numSequences(),
198                           it[ 5 ] > 0 ? g.formatNumber( number: it[ 5 ], format: "###,###,##0" ) : "-",        // it.numQualScores(),
199                           editButton,
200                           chartButton
201                   ]
202           }
203           
204           // Send the data to the user
205           render dataTablesService.retrieveData(
206                   params,
207                   AssaySample.class,
208                   convertClosure,
209                   columns,
210                   groupColumns,
211                   idColumn,
212                   from,
213                   total,
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.