source: trunk/grails-app/controllers/nl/tno/massSequencing/RunController.groovy @ 49

Last change on this file since 49 was 49, checked in by robert@…, 8 years ago
  • Added sequence length histograms
  • Fixed bugs in files that remained on the file system after uploading (while they shouldn't)
  • Added extra options in run- and assay screens
File size: 19.2 KB
Line 
1package nl.tno.massSequencing
2
3import java.util.Date;
4
5import org.codehaus.groovy.grails.commons.ConfigurationHolder
6
7class RunController {
8        def fileService
9        def synchronizationService
10        def sampleExcelService
11        def fastaService
12
13        def index = {
14                [runs: Run.list(), user: session.user]
15        }
16
17        def show = {
18                // load run with id specified by param.id
19                def run = getRun( params.id );
20
21                if (!run) {
22                        redirect(controller: 'study', action: 'index')
23                        return
24                }
25
26                // Make sure the newest data is available
27                synchronizationService.sessionToken = session.sessionToken
28                synchronizationService.user = session.user
29                try { 
30                        synchronizationService.synchronizeStudies();
31                } catch( Exception e ) {
32                        log.error "Exception occurred during synchronization in " + params.controller + ": " + e.getMessage()
33                        redirect( url: synchronizationService.gscfService.urlAuthRemote(params, session.sessionToken) )
34                }
35
36                // Determine runs not used in this assay
37                def otherAssays = Assay.list( sort: "name" ).findAll { !it.runs.contains( run ) && it.study.canRead( session.user ) }
38
39                // Send the assay information to the view
40                [run: run, allRuns: Run.list(), otherAssays: otherAssays, editable: true]
41        }
42
43        /**
44         * Shows a form to edit the specified run in dialog mode
45         */
46        def editForm = {
47                // load run with id specified by param.id
48                Run run = getRun( params.id );
49
50                if (!run) {
51                        render flash.error
52                        return
53                }
54
55                Assay assay = null
56                if( params.assayId ) {
57                        assay = getAssay( params.assayId )
58
59                        if( !assay ) {
60                                render flash.error;
61                                return
62                        }
63                }
64
65                [assay: assay, run: run]
66        }
67
68        def create = {
69                // Retrieve the assay from the database, but don't exit with an error if no assay is found
70                Assay a = getAssay(params.id);
71                flash.error = "";
72
73                // Create run based on given parameters
74                Run run = new Run();
75
76                run.setPropertiesFromForm( params );
77
78                if( a )
79                        a.addToRuns( run );
80
81                if( !run.save() ) {
82                        flash.message = "Run could not be saved: " + run.getErrors();
83                } else {
84                        flash.message = "Run " + run.name + " has been added to the system."
85                }
86
87                if( a )
88                        redirect( controller: "assay", action: "show", id: a.id )
89                else
90                        redirect( controller: 'run' );
91        }
92
93        def update = {
94                Run run = getRun( params.id );
95
96                if( !run ) {
97                        redirect(controller: 'assay', action: 'show', id: params.assayId)
98                        return
99                }
100
101                // Set properties to the run
102                params.parameterFile = params.editParameterFile
103
104                run.setPropertiesFromForm( params );
105
106                if( run.save() ) {
107                        flash.message = "Run succesfully saved";
108                } else {
109                        flash.error = "Run could not be saved: " + run.getErrors();
110                }
111
112                Assay assay = getAssay(params.assayId);
113                flash.error = "";
114
115                if( assay ) {
116                        redirect( controller: 'assay', action: 'show', id: assay.id)
117                } else {
118                        redirect( controller: 'run', action: 'show', id: run.id )
119                }
120        }
121
122        def delete = {
123                Run run = getRun( params.id );
124
125                if( !run ) {
126                        redirect(controller: 'assay', action: 'show', id: params.assayId)
127                        return
128                }
129
130                // Don't remove runs for which data exists
131                if( run.assaySamples*.sequenceData.flatten().size() ) {
132                        flash.message = "Run could not be deleted because samples with data are associated with it.";
133                        redirect( controller: "assay", action: "show", id: params.assayId )
134                }
135
136                // Check whether the user has sufficient privileges to remove the run from all assays
137                def hasPrivileges = true;
138                run.assay.each {
139                        if( !it.study.canWrite( session.user ) ) 
140                                hasPrivileges = false
141                }
142               
143                if( !hasPrivileges ) {
144                        flash.message = "Run could not be deleted because you don't have sufficient privileges to remove the run from all assays.";
145                        redirect( controller: "assay", action: "show", id: params.assayId )
146                }
147               
148                // Remove all associations
149                def a = [] + run.assays
150                a.each {
151                        run.removeFromAssays( it );
152                }
153
154                def name = run.name
155                run.delete();
156                flash.message = "Run " + name + " has been deleted from the system."
157
158                redirect( controller: "assay", action: "show", id: params.assayId )
159        }
160
161        def deleteRun = {
162                Run run = getRun( params.id );
163
164                if( !run ) {
165                        redirect(controller: 'run', action: 'index')
166                        return
167                }
168
169                // Don't remove runs for which data exists
170                if( run.assaySamples*.sequenceData.flatten().size() ) {
171                        flash.message = "Run could not be deleted because samples with data are associated with it.";
172                        redirect(controller: 'run', action: 'index')
173                }
174
175                // Check whether the user has sufficient privileges to remove the run from all assays
176                def hasPrivileges = true;
177                run.assays.each {
178                        if( !it.study.canWrite( session.user ) )
179                                hasPrivileges = false
180                }
181               
182                if( !hasPrivileges ) {
183                        flash.message = "Run could not be deleted because you don't have sufficient privileges to remove the run from all assays.";
184                        redirect(controller: 'run', action: 'index')
185                }
186               
187                // Remove all associations
188                def a = [] + run.assays
189                a.each {
190                        run.removeFromAssays( it );
191                }
192
193                def name = run.name
194                run.delete();
195                flash.message = "Run " + name + " has been deleted from the system."
196
197                redirect(controller: 'run', action: 'index')
198        }
199
200        /**************************************************************************
201         *
202         * Methods for handling data about the samples in this run
203         *
204         *************************************************************************/
205
206        /**
207         * Downloads an excel sheet with data about the assay samples, to enter data in excel
208         */
209        def downloadTagsExcel = {
210                Run run = getRun( params.id );
211
212                if( !run ) {
213                        redirect(controller: 'run')
214                        return
215                }
216
217                // Make it only possible to update samples writable by the user
218                def assaySamples = run.assaySamples.findAll { it.assay.study.canWrite( session.user ) }
219
220                def filename = "Run " + run.name + "_tags.xls"
221                def wb = sampleExcelService.downloadSampleExcel( assaySamples, false );
222
223                // Make file downloadable
224                log.trace( "Creation for downloading the file " + filename )
225                sampleExcelService.excelService.downloadFile( wb, filename, response )
226        }
227       
228        /**
229        * Downloads an example excel sheet to describe the format of a file-matching sheet. This
230        * file is used when uploading sequence files.
231        */
232   def downloadMatchExcel = {
233           Run run = getRun( params.id );
234
235           if( !run ) {
236                   redirect(controller: 'run')
237                   return
238           }
239
240           // Make it only possible to update samples writable by the user
241           def assaySamples = run.assaySamples.findAll { it.assay.study.canWrite( session.user ) }
242
243           def filename = "Run " + run.name + "_filenames.xls"
244           def wb = sampleExcelService.downloadMatchExcel( assaySamples );
245
246           // Make file downloadable
247           log.trace( "Creation for downloading the file " + filename )
248           sampleExcelService.excelService.downloadFile( wb, filename, response )
249   }
250
251        /**
252         * Parses an uploaded excel file and shows a form to match columns
253         */
254        def parseTagExcel = {
255                Run run = getRun( params.id );
256
257                if( !run ) {
258                        redirect(controller: 'study')
259                        return
260                }
261
262                def filename = params.filename
263
264                // Security check to prevent accessing files in other directories
265                if( !filename || filename.contains( '..' ) ) {
266                        response.status = 500;
267                        render "Invalid filename given";
268                        return;
269                }
270
271                // Check for existence and readability
272                File file = new File( fileService.getUploadDir(), filename)
273
274                if( !file.exists() || !file.canRead() ) {
275                        response.status = 404;
276                        render "The uploaded file doesn't exist or doesn't work as expected.";
277                        return;
278                }
279
280                // Save the filename in session for later use
281                session.filename = filename;
282                def excelData;
283                try {
284                        excelData = sampleExcelService.parseTagsExcel( file, false );
285                } catch( Throwable e ) { // Catch a throwable here instead of an exception, since the apache poi stuff gives an Error on failure
286                        // Couldn't create a workbook from this file.
287                        response.status = 400 // Bad request
288                        render "Uploaded file is not a valid excel file: " + e.getMessage()
289                        return
290                }
291                session.possibleFields = excelData.possibleFields
292
293                [run: run, headers: excelData.headers, exampleData: excelData.exampleData, filename: filename, possibleFields: [ "Don't import" ] + excelData.possibleFields, bestMatches: excelData.bestMatches]
294        }
295
296        /**
297         * Updates the assay samples based on the given excel file and the column matches
298         */
299        def updateTagsByExcel = {
300                Run run = getRun( params.id );
301
302                if( !run ) {
303                        // Now delete the file, since we don't need it anymore
304                        _deleteUploadedFileFromSession()
305
306                        redirect(controller: 'study')
307                        return
308                }
309
310                if( !session.filename ) {
311                        // Now delete the file, since we don't need it anymore
312                        _deleteUploadedFileFromSession()
313
314                        flash.error = "No excel file found because session timed out. Please try again."
315                        redirect( action: 'show', id: params.id)
316                        return
317                }
318
319                // Determine the match-columns
320                def matchColumns = params[ 'matches'];
321
322                // Now loop through the excel sheet and update all samples with the specified data
323                File file = new File( fileService.getUploadDir(), session.filename );
324
325                if( !file.exists() || !file.canRead() ) {
326                        flash.error = "Excel file has been removed since previous step. Please try again."
327                        redirect( action: 'show', id: params.id)
328                        return
329                }
330
331                // Make it only possible to update samples writable by the user
332                def assaySamples = run.assaySamples.findAll { it.assay.study.canWrite( session.user ) }
333               
334                def excelData = sampleExcelService.updateTagsByExcel( matchColumns, session.possibleFields, file, assaySamples );
335
336                println excelData
337               
338                // Return a message to the user
339                if( !excelData.success ) {
340                        flash.error = excelData.message
341                } else if( excelData.numSuccesful == 0 ) {
342                        flash.error = "None of the " + excelData.failedRows.size() + " row(s) could be imported, because none of the sample names matched or no samples are writable. Have you provided the right excel file?"
343                } else {
344                        flash.message = excelData.numSuccesful + " samples have been updated. "
345
346                        if( excelData.failedRows.size() > 0 )
347                                flash.message += excelData.failedRows.size() + " row(s) could not be imported, because the sample names could not be found in the database or you don't have the proper permissions to change them."
348                }
349
350                // Now delete the file, since we don't need it anymore
351                _deleteUploadedFileFromSession()
352
353                redirect( action: 'show', id: params.id )
354        }
355
356
357        /**
358         * Update the properties of the assay samples manually
359         */
360        def updateTagsManually = {
361                Run run = getRun( params.id );
362
363                if( !run ) {
364                        redirect(controller: 'study')
365                        return
366                }
367
368                // Loop through all assay samples and set data
369                def sampleParams = params.assaySample;
370
371                if( sampleParams ) {
372                        run.assaySamples.findAll { it.assay.study.canWrite( session.user ) }.each { assaySample ->
373                                def assaySampleParams = sampleParams.get( assaySample.id as String );
374                                if( assaySampleParams ) {
375                                        sampleExcelService.variableFields.each { k, v ->
376                                                assaySample[ k ] = assaySampleParams[ k ];
377                                        }
378                                        assaySample.save()
379                                }
380                        }
381                }
382
383                flash.message = "Data about samples is saved."
384                redirect( action: 'show', id: params.id )
385        }
386
387        /**************************************************************************
388         *
389         * Methods for handling data about assays for this run
390         *
391         *************************************************************************/
392
393        /**
394         * Adds existing samples to this run
395         */
396        def addSamples = {
397                Run run = getRun( params.id );
398
399                if( !run ) {
400                        redirect(controller: 'run', action: 'index')
401                        return
402                }
403
404                // Add checked runs to this assay
405                def assaySamples = params.assaySamples
406                if( assaySamples instanceof String ) {
407                        assaySamples = [ assaySamples ]
408                }
409
410                def numAdded = 0;
411                assaySamples.each { assaySampleId ->
412                        try {
413                                def assaySample = AssaySample.findById( assaySampleId as Long )
414                                if( assaySample.assay.study.canWrite( session.user ) ) {
415                                        if( run.assaySamples == null || !run.assaySamples.contains( assaySample ) ) {
416                                                run.addToAssaySamples( assaySample );
417                                                numAdded++;
418                                        }
419                                }
420                        } catch( Exception e ) {}
421                }
422
423                flash.message = numAdded + " samples are added to this run."
424                redirect( action: 'show', id: params.id)
425        }
426
427        /**
428         * Removes sample from this run
429         */
430        def removeSample = {
431                Run run = getRun( params.id );
432
433                if( !run ) {
434                        redirect(controller: 'study')
435                        return
436                }
437
438                if( !params.assaySampleId ) {
439                        flash.error = "No sample id given"
440                        redirect(action: 'show', id: params.id)
441                        return
442                }
443
444                def assaySample
445
446                try {
447                        assaySample = AssaySample.findById( params.assaySampleId as Long )
448                } catch( Exception e ) {
449                        log.error e
450                        flash.error = "Incorrect assaysample id given: " + params.assaySampleId
451                        redirect(action: 'show', id: params.id)
452                        return
453                }
454               
455                if( !assaySample.assay.study.canWrite( session.user ) ) {
456                        flash.error = "You don't have sufficient privileges to remove the specified sample from this run."
457                        redirect(action: 'show', id: params.id)
458                        return
459                }
460               
461                if( run.assaySamples.contains( assaySample ) ) {
462                        run.removeFromAssaySamples( assaySample );
463                        flash.message = "The sample has been removed from this run."
464                } else {
465                        flash.message = "The given sample was not associated with this run."
466                }
467
468                redirect( action: 'show', id: params.id)
469        }
470
471
472        /**
473         * Adds existing assays to this run
474         */
475        def addAssays = {
476                Run run = getRun( params.id );
477
478                if( !run ) {
479                        redirect(controller: 'study')
480                        return
481                }
482
483                // Add checked runs to this assay
484                def assays = params.assays
485                if( assays instanceof String ) {
486                        assays = [ assays ]
487                }
488
489                def numAdded = 0;
490                assays.each { assay_id ->
491                        try {
492                                def assay = Assay.findById( assay_id as Long )
493                                if( assay.study.canWrite( session.user ) ) {
494                                        if( run.assays == null || !run.assays.contains( assay ) ) {
495                                                run.addToAssays( assay );
496                                                numAdded++;
497                                        }
498                                }
499                        } catch( Exception e ) {}
500                }
501
502                flash.message = numAdded + " assays are added to this run."
503                redirect( action: 'show', id: params.id)
504        }
505
506        /**
507         * Removes assay for this run
508         */
509        def removeAssay = {
510                Run run = getRun( params.id );
511
512                if( !run ) {
513                        redirect(controller: 'run', action: 'index')
514                        return
515                }
516
517                if( !params.assay_id ) {
518                        flash.message = "No assay id given"
519                        redirect(action: 'show', id: params.id)
520                        return
521                }
522
523                def assay
524
525                try {
526                        assay = Assay.findById( params.assay_id as Long )
527                } catch( Exception e ) {
528                        throw e
529                        flash.message = "Incorrect assay id given: "
530                        redirect(action: 'show', id: params.id)
531                        return
532                }
533
534                if( !assay.study.canWrite( session.user ) ) {
535                        flash.error = "You don't have sufficient privileges to remove the specified assay from this run."
536                        redirect(action: 'show', id: params.id)
537                        return
538                }
539               
540                if( run.assays.contains( assay ) ) {
541                        run.removeFromAssays( assay );
542                        flash.message = "The assay has been removed from this run."
543                } else {
544                        flash.message = "The given assay was not associated with this run."
545                }
546
547                redirect( action: 'show', id: params.id)
548        }
549       
550        /**
551         * Deletes all sequences for a given run
552         */
553        def deleteSequenceData = {
554                Run run = getRun( params.id );
555               
556                if( !run ) {
557                        redirect(controller: 'run', action: 'index')
558                        return
559                }
560               
561                def assaySamples = run.assaySamples?.toList();
562                if( !assaySamples ) {
563                        flash.message = "No samples found for given run";
564                        redirect( controller: 'run', action: 'show', id: run.id );
565                        return
566                }
567               
568                def numFiles = fastaService.deleteSequenceData( assaySamples );
569               
570                flash.message = "All " + numFiles + " files have been removed from the run.";
571                redirect( controller: 'run', action: 'show', id: run.id );
572        }
573
574        /**
575         * Exports data about one or more runs in fasta format
576         */
577        def exportAsFasta = {
578                def assaySamples = getAssaySamples( params );
579
580                if( assaySamples == null )
581                        return;
582
583                def name
584
585                if( assaySamples.size() == 0 ) {
586                        flash.error = "No samples found for selected runs";
587                        redirect( action: "list" );
588                        return;
589                } else if( assaySamples*.run.unique().size() == 1 )
590                        name = "Run_" + assaySamples[0].run?.name?.replace( ' ', '_' );
591                else
592                        name = "runs";
593
594                // Export the sequences and quality scores
595                response.setHeader "Content-disposition", "attachment; filename=${name}.zip"
596                try {
597                        fastaService.export( assaySamples.unique(), response.getOutputStream() );
598                        response.outputStream.flush();
599                } catch( Exception e ) {
600                        log.error( "Exception occurred during export of sequences. Probably the user has cancelled the download." );
601                }
602        }
603       
604        /**
605         * Export metadata of selected samples in excel format
606         */
607        def exportMetaData = {
608                def assaySamples = getAssaySamples( params );
609                def name
610               
611                if( assaySamples == null )
612                        return;
613                       
614                if( assaySamples.size() == 0 ) {
615                        flash.error = "No samples found for selected runs";
616                        redirect( action: "list" );
617                        return;
618                } else if( assaySamples*.run.unique().size() == 1 ) {
619                        name = "Run_" + assaySamples[0].run?.name?.replace( ' ', '_' );
620                } else {
621                        name = "runs";
622                }
623
624                // Export the metadata
625                try {
626                        // The export functionality needs a assaysSample-tag list, but it
627                        // should be empty when only exporting metadata
628                        def tags = [];
629                        assaySamples.unique().each { assaySample ->
630                                tags << [assaySampleId: assaySample.id, sampleName: assaySample.sample.name, assayName: assaySample.assay.name, studyName: assaySample.assay.study.name, tag: "-"]
631                        }
632                        response.setHeader "Content-disposition", "attachment; filename=${name}.xls"
633                        if( !sampleExcelService.exportExcelSampleData( assaySamples.unique(), tags, response.getOutputStream() ) ) {
634                                flash.error = "An error occurred while fetching sample data. Maybe the session has timed out.";
635                                response.setHeader( "Content-disposition", "" );
636                                redirect( action: "index" );
637                        }
638                        response.outputStream.flush();
639                } catch( Exception e ) {
640                        log.error( "Exception occurred during export of sequences. Probably the user has cancelled the download." );
641                }
642        }
643       
644        def sequenceLengthHistogram = {
645                def id = params.long( 'id' );
646                def run = id ? Run.get( id ) : null
647               
648                if( !id || !run ) {
649                        flash.message = "No run selected";
650                        redirect( action: "index" );
651                        return;
652                }
653               
654                [ run: run, histogram: fastaService.sequenceLengthHistogram( run.assaySamples?.toList() ) ]
655        }
656       
657        protected List getAssaySamples( params ) {
658                def ids = params.list( 'ids' );
659               
660                ids = ids.findAll { it.isLong() }.collect { Long.parseLong( it ) }
661
662                if( !ids ) {
663                        def message = "No run ids given"
664                        flash.error = message
665                        redirect( action: "index" );
666                        return;
667                }
668
669                def assaySamples = [];
670
671                // Determine which assaySamples to export
672                ids.each { id ->
673                        def run = Run.get( id );
674                        if( run )
675                                assaySamples += run.assaySamples.findAll { it.assay.study.canRead( session.user ) }
676                }
677               
678                return assaySamples;
679        }
680
681        /**
682         * Deletes an uploaded file for which the filename is given in the session.
683         * @return
684         */
685        def _deleteUploadedFileFromSession() {
686                if( !session.filename )
687                        return
688
689                // Now delete the file, since we don't need it anymore
690                fileService.delete( session.filename  )
691                session.filename = ''
692        }
693
694        protected Run getRun(def runId) {
695                // load study with id specified by param.id
696                def run
697                try {
698                        run = Run.get(runId as Long)
699                } catch( Exception e ) {
700                        flash.error = "Incorrect id given: " + runId
701                        return null
702                }
703
704                if (!run) {
705                        flash.error = "No run found with id: " + runId
706                        return null
707                }
708
709                return run
710        }
711
712        protected Assay getAssay(def assayId) {
713                // load study with id specified by param.id
714                def assay
715                try {
716                        assay = Assay.get(assayId as Long)
717                } catch( Exception e ) {
718                        flash.error = "Incorrect id given: " + assayId
719                        return null
720                }
721
722                if (!assay) {
723                        flash.error = "No assay found with id: " + assayId
724                        return null
725                }
726
727                if (!assay.study.canRead( session.user ) ) {
728                        flash.error = "You don't have the right authorizaton to access assay " + assay.name
729                        return null
730                }
731
732                return assay
733        }
734}
Note: See TracBrowser for help on using the repository browser.