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

Last change on this file since 50 was 50, checked in by robert@…, 8 years ago
  • Added links to remove samples from a run
  • Added synchronization of authorization for all studies, if a user logs in
  • Improved excel import to handle Error-values in a cell
File size: 20.6 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                        response.setContentType( "text/plain" );
268                        render "Invalid filename given";
269                        return;
270                }
271
272                // Check for existence and readability
273                File file = new File( fileService.getUploadDir(), filename)
274
275                if( !file.exists() || !file.canRead() ) {
276                        response.status = 404;
277                        response.setContentType( "text/plain" );
278                        render "The uploaded file doesn't exist or doesn't work as expected.";
279                        return;
280                }
281
282                // Save the filename in session for later use
283                session.filename = filename;
284                def excelData;
285                try {
286                        excelData = sampleExcelService.parseTagsExcel( file, false );
287                } catch( Throwable e ) { // Catch a throwable here instead of an exception, since the apache poi stuff gives an Error on failure
288                        e.printStackTrace()
289                        // Couldn't create a workbook from this file.
290                        response.status = 400 // Bad request
291                        response.setContentType( "text/plain" );
292                        render "Uploaded file is not a valid excel file: " + e.getMessage()
293                        return
294                }
295                session.possibleFields = excelData.possibleFields
296
297                [run: run, headers: excelData.headers, exampleData: excelData.exampleData, filename: filename, possibleFields: [ "Don't import" ] + excelData.possibleFields, bestMatches: excelData.bestMatches]
298        }
299
300        /**
301         * Updates the assay samples based on the given excel file and the column matches
302         */
303        def updateTagsByExcel = {
304                Run run = getRun( params.id );
305
306                if( !run ) {
307                        // Now delete the file, since we don't need it anymore
308                        _deleteUploadedFileFromSession()
309
310                        redirect(controller: 'study')
311                        return
312                }
313
314                if( !session.filename ) {
315                        // Now delete the file, since we don't need it anymore
316                        _deleteUploadedFileFromSession()
317
318                        flash.error = "No excel file found because session timed out. Please try again."
319                        redirect( action: 'show', id: params.id)
320                        return
321                }
322
323                // Determine the match-columns
324                def matchColumns = params[ 'matches'];
325
326                // Now loop through the excel sheet and update all samples with the specified data
327                File file = new File( fileService.getUploadDir(), session.filename );
328
329                if( !file.exists() || !file.canRead() ) {
330                        flash.error = "Excel file has been removed since previous step. Please try again."
331                        redirect( action: 'show', id: params.id)
332                        return
333                }
334
335                // Make it only possible to update samples writable by the user
336                def assaySamples = run.assaySamples.findAll { it.assay.study.canWrite( session.user ) }
337               
338                def excelData = sampleExcelService.updateTagsByExcel( matchColumns, session.possibleFields, file, assaySamples );
339
340                println excelData
341               
342                // Return a message to the user
343                if( !excelData.success ) {
344                        flash.error = excelData.message
345                } else if( excelData.numSuccesful == 0 ) {
346                        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?"
347                } else {
348                        flash.message = excelData.numSuccesful + " samples have been updated. "
349
350                        if( excelData.failedRows.size() > 0 )
351                                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."
352                }
353
354                // Now delete the file, since we don't need it anymore
355                _deleteUploadedFileFromSession()
356
357                redirect( action: 'show', id: params.id )
358        }
359
360
361        /**
362         * Update the properties of the assay samples manually
363         */
364        def updateTagsManually = {
365                Run run = getRun( params.id );
366
367                if( !run ) {
368                        redirect(controller: 'study')
369                        return
370                }
371
372                // Loop through all assay samples and set data
373                def sampleParams = params.assaySample;
374
375                if( sampleParams ) {
376                        run.assaySamples.findAll { it.assay.study.canWrite( session.user ) }.each { assaySample ->
377                                def assaySampleParams = sampleParams.get( assaySample.id as String );
378                                if( assaySampleParams ) {
379                                        sampleExcelService.variableFields.each { k, v ->
380                                                assaySample[ k ] = assaySampleParams[ k ];
381                                        }
382                                        assaySample.save()
383                                }
384                        }
385                }
386
387                flash.message = "Data about samples is saved."
388                redirect( action: 'show', id: params.id )
389        }
390
391        /**************************************************************************
392         *
393         * Methods for handling data about assays for this run
394         *
395         *************************************************************************/
396
397        /**
398         * Adds existing samples to this run
399         */
400        def addSamples = {
401                Run run = getRun( params.id );
402
403                if( !run ) {
404                        redirect(controller: 'run', action: 'index')
405                        return
406                }
407
408                // Add checked runs to this assay
409                def assaySamples = params.assaySamples
410                if( assaySamples instanceof String ) {
411                        assaySamples = [ assaySamples ]
412                }
413
414                def numAdded = 0;
415                assaySamples.each { assaySampleId ->
416                        try {
417                                def assaySample = AssaySample.findById( assaySampleId as Long )
418                                if( !assaySample.run && assaySample.assay.study.canWrite( session.user ) ) {
419                                        if( run.assaySamples == null || !run.assaySamples.contains( assaySample ) ) {
420                                                run.addToAssaySamples( assaySample );
421                                                numAdded++;
422                                        }
423                                }
424                        } catch( Exception e ) {}
425                }
426
427                flash.message = numAdded + " samples are added to this run."
428                redirect( action: 'show', id: params.id)
429        }
430
431        /**
432         * Removes sample from this run
433         */
434        def removeSample = {
435                Run run = getRun( params.id );
436
437                if( !run ) {
438                        redirect(controller: 'study')
439                        return
440                }
441
442                if( !params.assaySampleId ) {
443                        flash.error = "No sample id given"
444                        redirect(action: 'show', id: params.id)
445                        return
446                }
447
448                def assaySample
449
450                try {
451                        assaySample = AssaySample.findById( params.assaySampleId as Long )
452                } catch( Exception e ) {
453                        log.error e
454                        flash.error = "Incorrect assaysample id given: " + params.assaySampleId
455                        redirect(action: 'show', id: params.id)
456                        return
457                }
458               
459                if( !assaySample.assay.study.canWrite( session.user ) ) {
460                        flash.error = "You don't have sufficient privileges to remove the specified sample from this run."
461                        redirect(action: 'show', id: params.id)
462                        return
463                }
464               
465                if( run.assaySamples.contains( assaySample ) ) {
466                        run.removeFromAssaySamples( assaySample );
467                        flash.message = "The sample has been removed from this run."
468                } else {
469                        flash.message = "The given sample was not associated with this run."
470                }
471
472                redirect( action: 'show', id: params.id)
473        }
474       
475        /**
476         * Removes samples from this run
477         */
478        def removeSamples = {
479                // Determine the run we are in
480                Run run = getRun( params.runId );
481
482                if( !run ) {
483                        redirect(controller: 'run', action: 'list')
484                        return
485                }
486
487                // Find the selected assaysamples
488                def ids = params.list( 'ids' );
489                ids = ids.findAll { it.isLong() }.collect { Long.parseLong( it ) }
490                def assaySamples = ids.collect { AssaySample.get( it ) }.findAll { it }
491
492                if( !assaySamples ) {
493                        flash.message = "No samples selected for removal"
494                        redirect( action: 'show', id: run.id );
495                        return;
496                }
497
498                def numRemoved = 0;
499                assaySamples.each { assaySample ->             
500                        if( assaySample.assay.study.canWrite( session.user ) ) {
501                                if( assaySample.run ) {
502                                        assaySample.run.removeFromAssaySamples( assaySample );
503                                        numRemoved++;
504                                }
505                        }
506                }
507               
508                if( numRemoved > 0 ) 
509                        flash.message = numRemoved + " sample(s) have been removed from this run."
510                else
511                        flash.message = "No samples have been removed from this run, because you don't have the right privileges to do so."
512
513                redirect( action: 'show', id: run.id)
514        }
515
516
517        /**
518         * Adds existing assays to this run
519         */
520        def addAssays = {
521                Run run = getRun( params.id );
522
523                if( !run ) {
524                        redirect(controller: 'study')
525                        return
526                }
527
528                // Add checked runs to this assay
529                def assays = params.assays
530                if( assays instanceof String ) {
531                        assays = [ assays ]
532                }
533
534                def numAdded = 0;
535                assays.each { assay_id ->
536                        try {
537                                def assay = Assay.findById( assay_id as Long )
538                                if( assay.study.canWrite( session.user ) ) {
539                                        if( run.assays == null || !run.assays.contains( assay ) ) {
540                                                run.addToAssays( assay );
541                                                numAdded++;
542                                        }
543                                }
544                        } catch( Exception e ) {}
545                }
546
547                flash.message = numAdded + " assays are added to this run."
548                redirect( action: 'show', id: params.id)
549        }
550
551        /**
552         * Removes assay for this run
553         */
554        def removeAssay = {
555                Run run = getRun( params.id );
556
557                if( !run ) {
558                        redirect(controller: 'run', action: 'index')
559                        return
560                }
561
562                if( !params.assay_id ) {
563                        flash.message = "No assay id given"
564                        redirect(action: 'show', id: params.id)
565                        return
566                }
567
568                def assay
569
570                try {
571                        assay = Assay.findById( params.assay_id as Long )
572                } catch( Exception e ) {
573                        throw e
574                        flash.message = "Incorrect assay id given: "
575                        redirect(action: 'show', id: params.id)
576                        return
577                }
578
579                if( !assay.study.canWrite( session.user ) ) {
580                        flash.error = "You don't have sufficient privileges to remove the specified assay from this run."
581                        redirect(action: 'show', id: params.id)
582                        return
583                }
584               
585                if( run.assays.contains( assay ) ) {
586                        run.removeFromAssays( assay );
587                        flash.message = "The assay has been removed from this run."
588                } else {
589                        flash.message = "The given assay was not associated with this run."
590                }
591
592                redirect( action: 'show', id: params.id)
593        }
594       
595        /**
596         * Deletes all sequences for a given run
597         */
598        def deleteSequenceData = {
599                // Determine the run we are in
600                Run run = getRun( params.runId );
601
602                if( !run ) {
603                        redirect(controller: 'run', action: 'index')
604                        return
605                }
606
607                // Find the selected assaysamples
608                def ids = params.list( 'ids' );
609                ids = ids.findAll { it.isLong() }.collect { Long.parseLong( it ) }
610                def assaySamples = ids.collect { AssaySample.get( it ) }.findAll { it }
611
612                if( !assaySamples ) {
613                        flash.message = "No samples selected"
614                        redirect( action: 'show', id: run.id );
615                        return;
616                }
617               
618                def numFiles = fastaService.deleteSequenceData( assaySamples );
619               
620                flash.message = numFiles + " files have been removed from the run.";
621                redirect( controller: 'run', action: 'show', id: run.id );
622        }
623
624        /**
625         * Exports data about one or more runs in fasta format
626         */
627        def exportAsFasta = {
628                def assaySamples = getAssaySamples( params );
629
630                if( assaySamples == null )
631                        return;
632
633                def name
634
635                if( assaySamples.size() == 0 ) {
636                        flash.error = "No samples found for selected runs";
637                        redirect( action: "index" );
638                        return;
639                } else if( assaySamples*.run.unique().size() == 1 )
640                        name = "Run_" + assaySamples[0].run?.name?.replace( ' ', '_' );
641                else
642                        name = "runs";
643
644                // Export the sequences and quality scores
645                response.setHeader "Content-disposition", "attachment; filename=${name}.zip"
646                try {
647                        fastaService.export( assaySamples.unique(), response.getOutputStream() );
648                        response.outputStream.flush();
649                } catch( Exception e ) {
650                        log.error( "Exception occurred during export of sequences. Probably the user has cancelled the download." );
651                }
652        }
653       
654        /**
655         * Export metadata of selected samples in excel format
656         */
657        def exportMetaData = {
658                def assaySamples = getAssaySamples( params );
659                def name
660               
661                if( assaySamples == null )
662                        return;
663                       
664                if( assaySamples.size() == 0 ) {
665                        flash.error = "No samples found for selected runs";
666                        redirect( action: "index" );
667                        return;
668                } else if( assaySamples*.run.unique().size() == 1 ) {
669                        name = "Run_" + assaySamples[0].run?.name?.replace( ' ', '_' );
670                } else {
671                        name = "runs";
672                }
673
674                // Export the metadata
675                try {
676                        // The export functionality needs a assaysSample-tag list, but it
677                        // should be empty when only exporting metadata
678                        def tags = [];
679                        assaySamples.unique().each { assaySample ->
680                                tags << [assaySampleId: assaySample.id, sampleName: assaySample.sample.name, assayName: assaySample.assay.name, studyName: assaySample.assay.study.name, tag: "-"]
681                        }
682                        response.setHeader "Content-disposition", "attachment; filename=${name}.xls"
683                        if( !sampleExcelService.exportExcelSampleData( assaySamples.unique(), tags, response.getOutputStream() ) ) {
684                                flash.error = "An error occurred while fetching sample data. Maybe the session has timed out.";
685                                response.setHeader( "Content-disposition", "" );
686                                redirect( action: "index" );
687                        }
688                        response.outputStream.flush();
689                } catch( Exception e ) {
690                        log.error( "Exception occurred during export of sequences. Probably the user has cancelled the download." );
691                }
692        }
693       
694        def sequenceLengthHistogram = {
695                def id = params.long( 'id' );
696                def run = id ? Run.get( id ) : null
697               
698                if( !id || !run ) {
699                        flash.message = "No run selected";
700                        redirect( action: "index" );
701                        return;
702                }
703               
704                [ run: run, histogram: fastaService.sequenceLengthHistogram( run.assaySamples?.toList() ) ]
705        }
706       
707        protected List getAssaySamples( params ) {
708                def ids = params.list( 'ids' );
709               
710                ids = ids.findAll { it.isLong() }.collect { Long.parseLong( it ) }
711
712                if( !ids ) {
713                        def message = "No run ids given"
714                        flash.error = message
715                        redirect( action: "index" );
716                        return;
717                }
718
719                def assaySamples = [];
720
721                // Determine which assaySamples to export
722                ids.each { id ->
723                        def run = Run.get( id );
724                        if( run )
725                                assaySamples += run.assaySamples.findAll { it.assay.study.canRead( session.user ) }
726                }
727               
728                return assaySamples;
729        }
730
731        /**
732         * Deletes an uploaded file for which the filename is given in the session.
733         * @return
734         */
735        def _deleteUploadedFileFromSession() {
736                if( !session.filename )
737                        return
738
739                // Now delete the file, since we don't need it anymore
740                fileService.delete( session.filename  )
741                session.filename = ''
742        }
743
744        protected Run getRun(def runId) {
745                // load study with id specified by param.id
746                def run
747                try {
748                        run = Run.get(runId as Long)
749                } catch( Exception e ) {
750                        flash.error = "Incorrect id given: " + runId
751                        return null
752                }
753
754                if (!run) {
755                        flash.error = "No run found with id: " + runId
756                        return null
757                }
758
759                return run
760        }
761
762        protected Assay getAssay(def assayId) {
763                // load study with id specified by param.id
764                def assay
765                try {
766                        assay = Assay.get(assayId as Long)
767                } catch( Exception e ) {
768                        flash.error = "Incorrect id given: " + assayId
769                        return null
770                }
771
772                if (!assay) {
773                        flash.error = "No assay found with id: " + assayId
774                        return null
775                }
776
777                if (!assay.study.canRead( session.user ) ) {
778                        flash.error = "You don't have the right authorizaton to access assay " + assay.name
779                        return null
780                }
781
782                return assay
783        }
784}
Note: See TracBrowser for help on using the repository browser.