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

Last change on this file since 50 was 50, checked in by robert@…, 9 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: 14.2 KB
Line 
1package nl.tno.massSequencing
2
3import java.util.List;
4
5import org.codehaus.groovy.grails.commons.ConfigurationHolder
6
7class AssayController {
8        def synchronizationService
9        def gscfService
10        def fuzzySearchService
11
12        def fileService
13        def excelService
14        def sampleExcelService
15        def fastaService
16
17        def index = {
18                // Filter studies for the ones the user is allowed to see
19                def studies = Study.list();
20                [studies: studies.findAll { it.canRead( session.user ) },
21                        gscfAddUrl: gscfService.urlAddStudy() ]
22        }
23
24        def show = {
25                def assay = getAssay( params.id );
26                if( !assay )
27                        return
28
29                // Make sure the newest data is available
30                synchronizationService.sessionToken = session.sessionToken
31                synchronizationService.synchronizeAssay( assay );
32
33                if( !assay.study || !assay.study.canRead( session.user ) ) {
34                        flash.error = "You don't have sufficient privileges to access assay " + assay.name
35                        redirect( action: "index" );
36                        return;
37                }
38               
39                // Determine runs not used in this assay
40                def otherRuns = Run.list( sort: "name" ).findAll { !it.assays.contains( assay ) }
41
42                // Send the assay information to the view
43                [assay: assay, editable: assay.study.canWrite( session.user ), otherRuns: otherRuns]
44        }
45       
46        def sequenceLengthHistogram = {
47                def id = params.long( 'id' );
48                def assay = id ? Assay.get( id ) : null
49               
50                if( !id || !assay ) {
51                        flash.message = "No assay selected";
52                        redirect( action: "index" );
53                        return;
54                }
55               
56                [ assay: assay, histogram: fastaService.sequenceLengthHistogram( assay.assaySamples.toList() ) ]
57        }
58
59        def showByToken = {
60                // load study with token specified by param.id
61                def assay = Assay.findByAssayToken(params.id as String)
62
63                if (!assay) {
64                        // Initialize synchronizationService
65                        synchronizationService.sessionToken = session.sessionToken
66                        synchronizationService.user = session.user
67
68                        // If the assay is not found, synchronize studies and try again
69                        synchronizationService.synchronizeStudies();
70
71                        assay = Assay.findByAssayToken(params.id as String)
72
73                        if (!assay) {
74                                // If the assay is not found, synchronize all studies and try again, since we might be out of sync
75                                synchronizationService.eager = true
76                                synchronizationService.synchronizeStudies();
77
78                                assay = Assay.findByAssayToken(params.id as String)
79
80                                // If after synchronization still no assay is found, show an error
81                                if( !assay ) {
82                                        flash.message = "No assay found with token: $params.id"
83                                        redirect(controller: 'study')
84                                }
85                        }
86                }
87
88                if (!assay.study.canRead( session.user ) ) {
89                        flash.error = "You don't have the right authorizaton to access assay " + assay.name
90                        redirect(action: 'index')
91                        return null
92                }
93               
94                redirect( action: "show", id: assay.id );
95        }
96
97        /**************************************************************************
98         *
99         * Method for handling data about samples for this assay
100         *
101         *************************************************************************/
102
103        /**
104         * Downloads an excel sheet with data about the assay samples, to enter data in excel
105         */
106        def downloadTagsExcel = {
107                // load study with id specified by param.id
108                def assay = getAssay( params.id );
109                if( !assay )
110                        return
111
112                def filename = assay.study.name + "_" + assay.name + "_tags.xls"
113                def wb = sampleExcelService.downloadSampleExcel( assay.assaySamples );
114
115                // Make file downloadable
116                log.trace( "Creation for downloading the file " + filename )
117                sampleExcelService.excelService.downloadFile( wb, filename, response )
118        }
119
120       
121        /**
122        * Downloads an example excel sheet to describe the format of a file-matching sheet. This
123        * file is used when uploading sequence files.
124        */
125   def downloadMatchExcel = {
126           def assay = getAssay( params.id );
127
128           if( !assay ) {
129                   redirect(controller: 'assay')
130                   return
131           }
132
133           def filename = "Assay " + assay.name + "_filenames.xls"
134           def wb = sampleExcelService.downloadMatchExcel( assay.assaySamples );
135
136           // Make file downloadable
137           log.trace( "Creation for downloading the file " + filename )
138           sampleExcelService.excelService.downloadFile( wb, filename, response )
139   }
140
141       
142        /**
143         * Parses an uploaded excel file and shows a form to match columns
144         */
145        def parseTagExcel = {
146                def assay = getAssay( params.id, true );
147                if( !assay )
148                        return
149
150                def filename = params.filename
151
152                // Security check to prevent accessing files in other directories
153                if( !filename || filename.contains( '..' ) ) {
154                        response.status = 500;
155                        render "Invalid filename given";
156                        return;
157                }
158
159                // Check for existence and readability
160                File file = new File( fileService.getUploadDir(), filename)
161
162                if( !file.exists() || !file.canRead() ) {
163                        response.status = 404;
164                        render "The uploaded file doesn't exist or doesn't work as expected.";
165                        return;
166                }
167
168                // Save the filename in session for later use
169                session.filename = filename;
170                def excelData;
171                try {
172                        excelData = sampleExcelService.parseTagsExcel( file );
173                } catch( Throwable e ) { // Catch a throwable here instead of an exception, since the apache poi stuff gives an Error on failure
174                        // Couldn't create a workbook from this file.
175                        response.status = 400 // Bad request
176                        render "Uploaded file is not a valid excel file: " + e.getMessage()
177                        return
178                }
179                session.possibleFields = excelData.possibleFields
180
181                [assay: assay, headers: excelData.headers, exampleData: excelData.exampleData, filename: filename, possibleFields: [ "Don't import" ] + excelData.possibleFields, bestMatches: excelData.bestMatches]
182        }
183
184        /**
185         * Updates the assay samples based on the given excel file and the column matches
186         */
187        def updateTagsByExcel = {
188                def assay = getAssay( params.id, true );
189                if( !assay ) {
190                        // Now delete the file, since we don't need it anymore
191                        _deleteUploadedFileFromSession()
192                        return;
193                }
194
195                if( !session.filename ) {
196                        // Now delete the file, since we don't need it anymore
197                        _deleteUploadedFileFromSession()
198
199                        flash.error = "No excel file found because session timed out. Please try again."
200                        redirect( action: 'show', id: params.id)
201                        return
202                }
203
204                // Determine the match-columns
205                def matchColumns = params[ 'matches'];
206
207                // Now loop through the excel sheet and update all samples with the specified data
208                File file = new File( fileService.getUploadDir(), session.filename );
209
210                if( !file.exists() || !file.canRead() ) {
211                        flash.error = "Excel file has been removed since previous step. Please try again."
212                        redirect( action: 'show', id: params.id)
213                        return
214                }
215
216                def excelData = sampleExcelService.updateTagsByExcel( matchColumns, session.possibleFields, file, assay.assaySamples );
217
218                // Return a message to the user
219                if( !excelData.success ) {
220                        flash.error = excelData.message
221                } else if( excelData.numSuccesful == 0 ) {
222                        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?"
223                } else {
224                        flash.message = excelData.numSuccesful + " samples have been updated. "
225
226                        if( excelData.failedRows.size() > 0 )
227                                flash.message += excelData.failedRows.size() + " row(s) could not be imported, because the sample names could not be found in the database."
228                }
229               
230                // Now delete the file, since we don't need it anymore
231                _deleteUploadedFileFromSession()
232
233                redirect( action: 'show', id: params.id )
234        }
235
236        /**
237         * Update the properties of the assay samples manually
238         */
239        def updateTagsManually = {
240                def assay = getAssay( params.id, true );
241                if( !assay )
242                        return
243
244                // Loop through all assay samples and set data
245                def sampleParams = params.assaySample;
246
247                if( sampleParams ) {
248                        assay.assaySamples.each { assaySample ->
249                                def assaySampleParams = sampleParams.get( assaySample.id as String );
250
251                                if( assaySampleParams ) {
252                                        sampleExcelService.variableFields.each { k, v ->
253                                                assaySample[ k ] = assaySampleParams[ k ];
254                                        }
255                                        assaySample.save()
256                                       
257                                        try {
258                                                assaySample.run = Run.get( assaySampleParams.run as Long );
259                                        } catch( Exception e ) {}
260
261                                        assaySample.save()
262                                }
263                        }
264                }
265
266                flash.message = "Data about samples is saved."
267                redirect( action: 'show', id: params.id )
268        }
269
270        /**************************************************************************
271         *
272         * Methods for handling data about runs for this assay
273         *
274         *************************************************************************/
275
276        /**
277         * Adds existing runs to this assay
278         */
279        def addExistingRuns = {
280                def assay = getAssay( params.id, true );
281                if( !assay )
282                        return
283
284                // Add checked runs to this assay
285                def runs = params.runs
286                if( runs instanceof String ) {
287                        runs = [ runs ]
288                }
289
290                def numAdded = 0;
291                runs.each { run_id ->
292                        try {
293                                def run = Run.findById( run_id as Long )
294                                if( run.assays == null || !run.assays.contains( assay ) ) {
295                                        run.addToAssays( assay );
296                                        numAdded++;
297                                }
298                        } catch( Exception e ) {}
299                }
300
301                flash.message = numAdded + " runs are added to this assay."
302                redirect( action: 'show', id: params.id)
303        }
304
305        /**
306         * Removes a run from this assay
307         */
308        def removeRun = {
309                def assay = getAssay( params.id, true );
310                if( !assay )
311                        return
312
313                if( !params.run_id ) {
314                        flash.message = "No run id given"
315                        redirect(action: 'show', id: params.id)
316                        return
317                }
318
319                def run
320
321                try {
322                        run = Run.findById( params.run_id as Long )
323                } catch( Exception e ) {
324                        throw e
325                        flash.message = "Incorrect run id given: "
326                        redirect(action: 'show', id: params.id)
327                        return
328                }
329
330                if( assay.runs.contains( run ) ) {
331                        assay.removeFromRuns( run );
332                        flash.message = "The run has been removed from this assay."
333                } else {
334                        flash.message = "The given run was not associated with this assay."
335                }
336
337                redirect( action: 'show', id: params.id)
338        }
339
340        def errorPage = {
341                render "An error has occured. $flash.message"
342        }
343
344        /**
345         * Deletes an uploaded file for which the filename is given in the session.
346         * @return
347         */
348        def _deleteUploadedFileFromSession() {
349                if( !session.filename )
350                        return
351
352                // Now delete the file, since we don't need it anymore
353                fileService.delete( session.filename  )
354                session.filename = ''
355        }
356       
357       
358        /**
359         * Deletes all sequences for a given assay
360         * @param assayId       Id of the assay we are in
361         * @param ids           List of ids of the assaysamples to delete the sequences from
362         */
363        def deleteSequenceData = {
364                // Determine the assay we are in
365                Assay assay = getAssay( params.assayId );
366               
367                if( !assay ) {
368                        redirect(controller: 'assay', action: 'index')
369                        return
370                }
371
372                // Find the selected assaysamples
373                def ids = params.list( 'ids' );
374                ids = ids.findAll { it.isLong() }.collect { Long.parseLong( it ) }
375                def assaySamples = ids.collect { AssaySample.get( it ) }.findAll { it }
376
377                if( !assaySamples ) {
378                        flash.message = "No samples selected"
379                        redirect( controller: 'assay', action: 'show', id: assay.id );
380                        return;
381                }
382               
383                def numFiles = fastaService.deleteSequenceData( assaySamples );
384               
385                flash.message = numFiles + " files have been removed from the assay.";
386                redirect( controller: 'assay', action: 'show', id: assay.id );
387        }
388
389        /**
390         * Exports data about one or more assays in fasta format
391         */
392        def exportAsFasta = {
393                def assaySamples = getAssaySamples( params );
394                def name
395
396                if( assaySamples == null ) {
397                        return
398                } else if( assaySamples*.assay.unique().size() == 1 ) {
399                        name = "Assay_" + assaySamples[0].assay?.name?.replace( ' ', '_' );
400                } else {
401                        name = "assays";
402                }
403
404                // Export the sequences and quality scores
405                response.setHeader "Content-disposition", "attachment; filename=" + name.trim() + ".zip"
406                try {
407                        fastaService.export( assaySamples.unique(), response.getOutputStream() );
408                        response.outputStream.flush();
409                } catch( Exception e ) {
410                        log.error( "Exception occurred during export of sequences. Probably the user has cancelled the download." );
411                }
412        }
413
414        /**
415         * Export metadata of selected samples in excel format
416         */
417        def exportMetaData = {
418                def assaySamples = getAssaySamples( params );
419                def name
420
421                if( assaySamples == null ) {
422                        return
423                } else if( assaySamples*.assay.unique().size() == 1 ) {
424                        name = "Assay_" + assaySamples[0].assay?.name?.replace( ' ', '_' );
425                } else {
426                        name = "assays";
427                }
428
429                // Export the metadata
430                response.setHeader "Content-disposition", "attachment; filename=${name}.xls"
431                try {
432                        // The export functionality needs a assaySample-tag list, but it
433                        // should be empty when only exporting metadata
434                        def tags = [];
435                        assaySamples.unique().each { assaySample ->
436                                tags << [assaySampleId: assaySample.id, sampleName: assaySample.sample.name, assayName: assaySample.assay.name, studyName: assaySample.assay.study.name, tag: "-"]
437                        }
438                        sampleExcelService.exportExcelSampleData( assaySamples.unique(), tags, response.getOutputStream() );
439                        response.outputStream.flush();
440                } catch( Exception e ) {
441                        log.error( "Exception occurred during export of metadata. Probably the user has cancelled the download." );
442                }
443        }
444
445       
446        /**
447         * Retrieves an assay from the database, based on the assay ID given
448         * @param assayId               ID of the assay
449         * @param writeAccess   True if you require write access to this assay. The system will check for sufficient privileges
450         * @return
451         */
452        protected Assay getAssay(def assayId, boolean writeAccess = false ) {
453                // load study with id specified by param.id
454                def assay
455                try {
456                        assay = Assay.get(assayId as Long)
457                } catch( Exception e ) {
458                        flash.error = "Incorrect id given: " + assayId
459                        redirect(action: 'index')
460                        return null
461                }
462
463                if (!assay) {
464                        flash.error = "No assay found with id: " + assayId
465                        redirect(action: 'index')
466                        return null
467                }
468
469                if ( !assay.study.canRead( session.user ) || ( writeAccess && !assay.study.canWrite( session.user ) ) ) {
470                        flash.error = "You don't have the right authorizaton to access assay " + assay.name
471                        redirect(action: 'index')
472                        return null
473                }
474               
475                return assay
476        }
477
478       
479        protected List getAssaySamples( params ) {
480                def tokens = params.list( 'tokens' );
481                def ids = params.list( 'ids' );
482                def name;
483
484                ids = ids.findAll { it.isLong() }.collect { Long.parseLong( it ) }
485
486                if( !tokens && !ids ) {
487                        def message = "No assay tokens or ids given"
488                        flash.error = message
489                        redirect( action: "index" );
490                        return;
491                }
492
493                def assaySamples = [];
494
495                // Determine which assaySamples to export
496                def assay;
497                tokens.each { token ->
498                        assay = Assay.findByAssayToken( token );
499                        if( assay && assay.study.canRead( session.user ) )
500                                assaySamples += assay.assaySamples
501                }
502                ids.each { id ->
503                        assay = Assay.get( id );
504                        if( assay && assay.study.canRead( session.user ) )
505                                assaySamples += assay.assaySamples
506                }
507
508                return assaySamples;
509        }
510
511
512}
Note: See TracBrowser for help on using the repository browser.