source: trunk/grails-app/controllers/dbnp/studycapturing/SimpleWizardController.groovy @ 1684

Last change on this file since 1684 was 1684, checked in by robert@…, 10 years ago

improvements in simple wizard

  • Property svn:keywords set to Rev Author Date
File size: 38.6 KB
Line 
1/**
2 * SimpleWizardController Controler
3 *
4 * Description of my controller
5 *
6 * @author  your email (+name?)
7 * @since       2010mmdd
8 * @package     ???
9 *
10 * Revision information:
11 * $Rev: 1684 $
12 * $Author: robert@isdat.nl $
13 * $Date: 2011-03-31 15:03:31 +0000 (do, 31 mrt 2011) $
14 */
15package dbnp.studycapturing
16
17import org.apache.poi.ss.usermodel.DataFormatter
18import org.dbnp.gdt.*
19import grails.plugins.springsecurity.Secured
20import dbnp.authentication.SecUser
21import dbnp.importer.ImportCell
22import dbnp.importer.ImportRecord
23import dbnp.importer.MappingColumn
24import org.hibernate.SessionFactory;
25
26@Secured(['IS_AUTHENTICATED_REMEMBERED'])
27class SimpleWizardController extends StudyWizardController {
28        def authenticationService
29        def fileService
30        def importerService
31        def gdtService = new GdtService()
32        def sessionFactory
33
34        /**
35         * index closure
36         */
37        def index = {
38                if( params.id )
39                        redirect( action: "simpleWizard", id: params.id );
40                else
41                        redirect( action: "simpleWizard" );
42        }
43
44        def simpleWizardFlow = {
45                entry {
46                        action{
47                                flow.study = getStudyFromRequest( params )
48                                if (!flow.study) retrievalError()
49                        }
50                        on("retrievalError").to "handleError"
51                        on("success").to "study"
52                }
53
54                study {
55                        on("next") {
56                                handleStudy( flow.study, params )
57                                if( !validateObject( flow.study ) )
58                                        error()
59                        }.to "decisionState"
60                        on("refresh") {  handleStudy( flow.study, params ); }.to "study"
61                        on( "save" ) {
62                                handleStudy( flow.study, params );
63                                if( !validateObject( flow.study ) )
64                                        return error()
65                               
66                                if( flow.study.save( flush: true ) ) {
67                                        flash.message = "Your study is succesfully saved.";
68                                } else {
69                                        flash.error = "An error occurred while saving your study: <br />"
70                                        flow.study.getErrors().each { flash.error += it.toString() + "<br />"}
71                                }
72                                success()               
73                        }.to "study"
74                }
75
76                decisionState {
77                        action {
78                                // Create data in the flow
79                                flow.templates = [
80                                                        'Subject': Template.findAllByEntity( Subject.class ),
81                                                        'Event': Template.findAllByEntity( Event.class ),
82                                                        'SamplingEvent': Template.findAllByEntity( SamplingEvent.class ),
83                                                        'Sample': Template.findAllByEntity( Sample.class )
84                                ];
85                       
86                                flow.encodedEntity = [
87                                                        'Subject': gdtService.encryptEntity( Subject.class.name ),
88                                                        'Event': gdtService.encryptEntity( Event.class.name ),
89                                                        'SamplingEvent': gdtService.encryptEntity( SamplingEvent.class.name ),
90                                                        'Sample': gdtService.encryptEntity( Sample.class.name )
91                                ]
92
93                                if (flow.study.samples)
94                                        checkStudySimplicity(flow.study) ? existingSamples() : complexStudy()
95                                else
96                                        samples()
97                        }
98                        on ("existingSamples").to "startExistingSamples"
99                        on ("complexStudy").to "complexStudy"
100                        on ("samples").to "samples"
101                }
102               
103                startExistingSamples {
104                        action {
105                                def records = importerService.getRecords( flow.study );
106                                flow.records = records
107                                flow.templateCombinations = records.templateCombination.unique()
108                                success();
109                        }
110                        on( "success" ).to "existingSamples"
111                }
112
113                existingSamples {
114                        on("next") {
115                                handleExistingSamples( flow.study, params, flow ) ? success() : error()
116                        }.to "startAssays"
117                        on( "save" ) {
118                                if( !handleExistingSamples( flow.study, params, flow ) )
119                                        return error()
120                               
121                                if( flow.study.save( flush: true ) ) {
122                                        flash.message = "Your study is succesfully saved.";
123                                } else {
124                                        flash.error = "An error occurred while saving your study: <br />"
125                                        flow.study.getErrors().each { flash.error += it.toString() + "<br />"}
126                                }
127                                success()
128                        }.to "existingSamples"
129
130                        on("previous").to "study"
131                        on("update") {
132                                handleExistingSamples( flow.study, params, flow ) ? success() : error()
133                        }.to "samples"
134
135                        on("skip").to "startAssays"
136                }
137
138                complexStudy {
139                        on("save").to "save"
140                        on("previous").to "study"
141                }
142
143                samples {
144                        on("next") {
145                                if( !handleSamples( flow.study, params, flow ) )
146                                        return error();
147                               
148                                // Add domain fields for all entities
149                                flow.domainFields = [:]
150                               
151                                flow.templates.each { 
152                                        if( it.value ) {
153                                                flow.domainFields[ it.key ] = it.value[0].entity.giveDomainFields();
154                                        }
155                                }
156                               
157                                println flow.sampleForm.template
158                        }.to "columns"
159                        on("refresh") {
160                                def filename = params.get( 'importfile' );
161               
162                                handleSampleForm( flow.study, params, flow )
163
164                                // Handle 'existing*' in front of the filename. This is put in front to make a distinction between
165                                // an already uploaded file test.txt (maybe moved to some other directory) and a newly uploaded file test.txt
166                                // still being in the temporary directory.
167                                // This import step doesn't have to make that distinction, since all files remain in the temporary directory.
168                                if( filename == 'existing*' )
169                                        filename = '';
170                                else if( filename[0..8] == 'existing*' )
171                                        filename = filename[9..-1]
172                               
173                                flow.sampleForm.importFile = filename
174                                       
175                                // Refresh the templates, since the template editor has been opened. To make this happen
176                                // the hibernate session has to be cleared first
177                                sessionFactory.getCurrentSession().clear();
178                               
179                                flow.templates = [
180                                                'Subject': Template.findAllByEntity( Subject.class ),
181                                                'Event': Template.findAllByEntity( Event.class ),
182                                                'SamplingEvent': Template.findAllByEntity( SamplingEvent.class ),
183                                                'Sample': Template.findAllByEntity( Sample.class )
184                                ];
185                       
186                        }.to "samples"
187                        on("previous").to "returnFromSamples"
188                        on("study").to "study"
189                        on("skip").to "startAssays"
190                }
191
192                returnFromSamples {
193                        action {
194                                flow.study.samples ? existingSamples() : study();
195                        }
196                        on( "existingSamples" ).to "startExistingSamples"
197                        on( "study" ).to "study"
198                }
199               
200                columns {
201                        on( "next" ) {
202                                flow.editImportedData = params.get( 'editAfterwards' ) ? true : false;
203                                handleColumns( flow.study, params, flow ) ? success() : error()
204                        }.to "checkImportedEntities"
205                        on( "previous" ).to "samples" 
206                }
207               
208                checkImportedEntities {
209                        action {
210                                // Only continue to the next page if the information entered is correct
211                                if( flow.editImportedData || flow.imported.numInvalidEntities > 0 ) {
212                                        missingFields();
213                                } else {
214                                        // The import of the excel file has finished. Now delete the excelfile
215                                        if( flow.excel.filename )
216                                                fileService.delete( flow.excel.filename );
217       
218                                        flow.sampleForm = null
219       
220                                        assays();
221                                }
222                        }
223                        on( "missingFields" ).to "missingFields"
224                        on( "assays" ).to "startAssays" 
225                }
226               
227                missingFields {
228                        on( "refresh" ) {
229                                handleMissingFields( flow.study, params, flow );
230                                success();
231                        }.to "missingFields"
232                        on( "next" ) {
233                                if( !handleMissingFields( flow.study, params, flow ) ) {
234                                        return error();
235                                }
236                               
237                                // The import of the excel file has finished. Now delete the excelfile
238                                if( flow.excel.filename )
239                                        fileService.delete( flow.excel.filename );
240
241                                flow.sampleForm = null
242                               
243                                success();
244                        }.to "startAssays"
245                        on( "previous" ) {
246                                // The user goes back to the previous page, so the already imported entities
247                                // (of which some gave an error) should be removed again.
248                                // Add all samples
249                                flow.imported.data.each { record ->
250                                        record.each { entity ->
251                                                if( entity ) {
252                                                        switch( entity.class ) {
253                                                                case Sample:    flow.study.removeFromSamples( entity ); break;
254                                                                case Subject:   flow.study.removeFromSubjects( entity ); break;
255                                                                case Event:             flow.study.removeFromEvents( entity ); break;
256                                                                case SamplingEvent:     flow.study.removeFromSamplingEvents( entity ); break;
257                                                        }
258                                                }
259                                        }
260                                }
261                               
262                                success();
263                        }.to "columns"
264                }
265               
266                startAssays {
267                        action {
268                                if( !flow.assay ) {
269                                        if( flow.study.assays ) {
270                                                flow.assay = flow.study.assays[ 0 ]
271                                                println "Existing assay: " + flow.assay
272                                        } else {
273                                                flow.assay = new Assay( parent: flow.study );
274                                        }
275                                }
276                                success();
277                        }
278                        on( "success" ).to "assays"
279                }
280               
281                assays {
282                        on( "next" ) { 
283                                handleAssays( flow.assay, params, flow );
284                                if( flow.assay.template && !validateObject( flow.assay ) )
285                                        error();
286                                       
287                         }.to "overview"
288                        on( "skip" ) {
289                                // In case the user has created an assay before he clicked 'skip', it should only be kept if it
290                                // existed before this step
291                                if( flow.assay != null && !flow.assay.id ) {
292                                        flow.remove( "assay" )
293                                }
294
295                         }.to "overview"
296                        on( "previous" ).to "returnFromAssays"
297                        on("refresh") { 
298                                // Refresh the templates, since the template editor has been opened. To make this happen
299                                // the hibernate session has to be cleared first
300                                //sessionFactory.getCurrentSession().clear();
301                               
302                                handleAssays( flow.assay, params, flow ); 
303                               
304                                flow.assay?.template?.refresh()
305                                success() 
306                        }.to "assays"
307                }
308
309                returnFromAssays {
310                        action {
311                                flow.study.samples ? existingSamples() : samples();
312                        }
313                        on( "existingSamples" ).to "existingSamples"
314                        on( "samples" ).to "samples"
315                }
316               
317                overview { 
318                        on( "save" ).to "saveStudy" 
319                        on( "previous" ).to "startAssays"
320                }
321               
322                saveStudy {
323                        action {
324                                if( flow.assay && flow.assay.template && !flow.study.assays?.contains( flow.assay ) ) {
325                                        flow.study.addToAssays( flow.assay );
326                                }
327                               
328                                if( flow.study.save( flush: true ) ) {
329                                        // Make sure all samples are attached to all assays
330                                        flow.study.assays.each { assay ->
331                                                def l = []+ assay.samples;
332                                                l.each { sample ->
333                                                        if( sample )
334                                                                assay.removeFromSamples( sample );
335                                                }
336                                                assay.samples?.clear();
337               
338                                                flow.study.samples.each { sample ->
339                                                        assay.addToSamples( sample )
340                                                }
341                                        }
342                       
343                                        flash.message = "Your study is succesfully saved.";
344                                       
345                                        finish();
346                                } else {
347                                        flash.error = "An error occurred while saving your study: <br />"
348                                        flow.study.getErrors().each { flash.error += it.toString() + "<br />"}
349                                       
350                                        // Remove the assay from the study again, since it is still available
351                                        // in the session
352                                        if( flow.assay ) {
353                                                flow.study.removeFromAssays( flow.assay );
354                                                flow.assay.parent = flow.study;
355                                        }
356                                       
357                                        overview();
358                                }
359                        }
360                        on( "finish" ).to "finish"
361                        on( "overview" ).to "overview"
362                }
363               
364                finish()
365               
366                handleError{
367                        redirect action: "errorPage"
368                }
369        }
370
371        /**
372         * Retrieves the required study from the database or return an empty Study object if
373         * no id is given
374         *
375         * @param params        Request parameters with params.id being the ID of the study to be retrieved
376         * @return                      A study from the database or an empty study if no id was given
377         */
378        protected Study getStudyFromRequest( def params ) {
379                int id = params.int( "id" );
380
381                if( !id ) {
382                        return new Study( title: "New study", owner: authenticationService.getLoggedInUser() );
383                }
384
385                Study s = Study.get( id );
386
387                if( !s ) {
388                        flash.error = "No study found with given id";
389                        return null;
390                }
391                if( !s.canWrite( authenticationService.getLoggedInUser() ) ) {
392                        flash.error = "No authorization to edit this study."
393                        return null;
394                }
395
396                return s
397        }
398
399        /**
400         * Handles study input
401         * @param study         Study to update
402         * @param params        Request parameter map
403         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error
404         */
405        def handleStudy( study, params ) {
406                // did the study template change?
407                if (params.get('template') && study.template?.name != params.get('template')) {
408                        // set the template
409                        study.template = Template.findByName(params.remove('template'))
410                }
411
412                // does the study have a template set?
413                if (study.template && study.template instanceof Template) {
414                        // yes, iterate through template fields
415                        study.giveFields().each() {
416                                // and set their values
417                                study.setFieldValue(it.name, params.get(it.escapedName()))
418                        }
419                }
420
421                // handle public checkbox
422                if (params.get("publicstudy")) {
423                        study.publicstudy = params.get("publicstudy")
424                }
425
426                // handle publications
427                handleStudyPublications(study, params)
428
429                // handle contacts
430                handleStudyContacts(study, params)
431
432                // handle users (readers, writers)
433                handleStudyUsers(study, params, 'readers')
434                handleStudyUsers(study, params, 'writers')
435
436                return true
437        }
438       
439        /**
440        * Handles the editing of existing samples
441        * @param study          Study to update
442        * @param params         Request parameter map
443        * @return                       True if everything went OK, false otherwise. An error message is put in flash.error
444        */
445   def handleExistingSamples( study, params, flow ) {
446           flash.validationErrors = [];
447
448           def errors = false;
449           
450           // iterate through objects; set field values and validate the object
451           def eventgroups = study.samples.parentEventGroup.findAll { it }
452           def events;
453           if( !eventgroups )
454                   events = []
455           else
456                   events = eventgroups.events?.getAt(0);
457           
458           def objects = [
459                   'Subject': study.samples.parentSubject.findAll { it },
460                   'SamplingEvent': study.samples.parentEvent.findAll { it },
461                   'Event': events.flatten().findAll { it },
462                   'Sample': study.samples
463           ];
464           objects.each {
465                   def type = it.key;
466                   def entities = it.value;
467                   
468                   entities.each { entity ->
469                           // iterate through entity fields
470                           entity.giveFields().each() { field ->
471                                   def value = params.get( type.toLowerCase() + '_' + entity.getIdentifier() + '_' + field.escapedName())
472
473                                   // set field value; name cannot be set to an empty value
474                                   if (field.name != 'name' || value) {
475                                           log.info "setting "+field.name+" to "+value
476                                           entity.setFieldValue(field.name, value)
477                                   }
478                           }
479                           
480                           // has the template changed?
481                           def templateName = params.get(type.toLowerCase() + '_' + entity.getIdentifier() + '_template')
482                           if (templateName && entity.template?.name != templateName) {
483                                   entity.template = Template.findByName(templateName)
484                           }
485   
486                           // validate sample
487                           if (!entity.validate()) {
488                                   errors = true;
489                                   
490                                   def entityName = entity.class.name[ entity.class.name.lastIndexOf( "." ) + 1 .. -1 ]
491                                   getHumanReadableErrors( entity ).each {
492                                                flash.validationErrors << [ key: it.key, value: "(" + entityName + ") " + it.value ];
493                                   }
494                           }
495                   }
496           }
497
498           return !errors
499   }
500
501        /**
502         * Handles the upload of sample data
503         * @param study         Study to update
504         * @param params        Request parameter map
505         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error
506         */
507        def handleSamples( study, params, flow ) {
508                def filename = params.get( 'importfile' );
509
510                // Handle 'existing*' in front of the filename. This is put in front to make a distinction between
511                // an already uploaded file test.txt (maybe moved to some other directory) and a newly uploaded file test.txt
512                // still being in the temporary directory.
513                // This import step doesn't have to make that distinction, since all files remain in the temporary directory.
514                if( filename == 'existing*' )
515                        filename = '';
516                else if( filename[0..8] == 'existing*' )
517                        filename = filename[9..-1]
518
519                handleSampleForm( study, params, flow );
520
521                // Check whether the template exists
522                if (!flow.sampleForm.template.Sample ){
523                        log.error ".simple study wizard not all fields are filled in (sample template) "
524                        flash.error = "No template was chosen. Please choose a template for the samples you provided."
525                        return false
526                }
527               
528                // These fields have been removed from the form, so will always contain
529                // their default value. The code however remains like this for future use.
530                int sheetIndex = (params.int( 'sheetindex' ) ?: 1 )
531                int dataMatrixStart = (params.int( 'datamatrix_start' ) ?: 2 )
532                int headerRow = (params.int( 'headerrow' ) ?: 1 )
533               
534                flow.sampleForm.sheetIndex = sheetIndex;
535                flow.sampleForm.dataMatrixStart = dataMatrixStart
536                flow.sampleForm.headerRow = headerRow
537                flow.sampleForm.importFile = filename
538               
539                def importedfile = fileService.get( filename )
540                def workbook
541                if (importedfile.exists()) {
542                        try {
543                                workbook = importerService.getWorkbook(new FileInputStream(importedfile))
544                        } catch (Exception e) {
545                                log.error ".simple study wizard could not load file: " + e
546                                flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
547                                return false
548                        }
549                } else {
550                        log.error ".simple study wizard no file given";
551                        flash.error = "No file was given. Please provide an excel file for entering samples.";
552                        return false;
553                }
554
555                if( !workbook ) {
556                        log.error ".simple study wizard could not load file into a workbook"
557                        flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
558                        return false
559                }
560
561                def selectedentities = []
562
563                if( !excelChecks( workbook, sheetIndex, headerRow, dataMatrixStart ) )
564                        return false;
565
566                // Get the header from the Excel file using the arguments given in the first step of the wizard
567                def importerHeader;
568                def importerDataMatrix;
569
570                try {           
571                        importerHeader = importerService.getHeader(workbook,
572                                        sheetIndex - 1,                 // 0 == first sheet
573                                        headerRow,                              // 1 == first row :s
574                                        dataMatrixStart - 1,    // 0 == first row
575                                        Sample.class)
576               
577                        importerDataMatrix = importerService.getDatamatrix(
578                                        workbook,
579                                        importerHeader,
580                                        sheetIndex - 1,                 // 0 == first sheet
581                                        dataMatrixStart - 1,    // 0 == first row
582                                        5)
583                } catch( Exception e ) {
584                        // An error occurred while reading the excel file.
585                        log.error ".simple study wizard error while reading the excel file";
586                        e.printStackTrace();
587
588                        // Show a message to the user
589                        flash.error = "An error occurred while reading the excel file. Have you provided the right sheet number and row numbers. Contact your system administrator if this problem persists.";
590                        return false;
591                }
592
593                // Match excel columns with template fields
594                def fieldNames = [];
595                flow.sampleForm.template.each { template ->
596                        if( template.value ) {
597                                def fields = template.value.entity.giveDomainFields() + template.value.getFields();
598                                fields.each { field ->
599                                        if( !field.entity )
600                                                field.entity = template.value.entity
601                                               
602                                        fieldNames << field
603                                }
604                        }
605                }
606                importerHeader.each { mc ->
607                        def bestfit = importerService.mostSimilar( mc.name, fieldNames, 0.8);
608                        if( bestfit ) {
609                                // Remove this fit from the list
610                                fieldNames.remove( bestfit );
611                               
612                                mc.entityclass = bestfit.entity
613                                mc.property = bestfit.name
614                        }
615                }
616               
617                // Save read excel data into session
618                def dataMatrix = [];
619                def df = new DataFormatter();
620                importerDataMatrix.each {
621                        dataMatrix << it.collect{ it ? df.formatCellValue(it) : "" }
622                }
623               
624                flow.excel = [
625                                        filename: filename,
626                                        sheetIndex: sheetIndex,
627                                        dataMatrixStart: dataMatrixStart,
628                                        headerRow: headerRow,
629                                        data: [
630                                                header: importerHeader,
631                                                dataMatrix: dataMatrix
632                                        ]
633                                ]
634
635                return true
636        }
637       
638        /**
639         * Copies data from the submitted sample form to the flow
640         * @param study
641         * @param params
642         * @param flow
643         * @return
644         */
645        protected def handleSampleForm( study, params, flow ) {
646                def sampleTemplateId  = params.long( 'sample_template_id' )
647                def subjectTemplateId  = params.long( 'subject_template_id' )
648                def eventTemplateId  = params.long( 'event_template_id' )
649                def samplingEventTemplateId  = params.long( 'samplingEvent_template_id' )
650
651                // Save form data in session
652                if( !flow.sampleForm )
653                        flow.sampleForm = [:]
654                       
655                flow.sampleForm.templateId = [
656                                                'Subject': subjectTemplateId,
657                                                'Event': eventTemplateId,
658                                                'SamplingEvent': samplingEventTemplateId,
659                                                'Sample': sampleTemplateId
660                ];
661                flow.sampleForm.template = [
662                                                'Subject': subjectTemplateId ? Template.get( subjectTemplateId ) : null,
663                                                'Event': eventTemplateId ? Template.get( eventTemplateId ) : null,
664                                                'SamplingEvent': samplingEventTemplateId ? Template.get( samplingEventTemplateId ) : null,
665                                                'Sample': sampleTemplateId ? Template.get( sampleTemplateId ) : null
666                ];
667        }
668       
669        /**
670         * Handles the matching of template fields with excel columns by the user
671         * @param study         Study to update
672         * @param params        Request parameter map
673         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error
674         *                                      The field session.simpleWizard.imported.numInvalidEntities reflects the number of
675         *                                      entities that have errors, and should be fixed before saving. The errors for those entities
676         *                                      are saved into session.simpleWizard.imported.errors
677         */
678        def handleColumns( study, params, flow ) {
679                // Find actual Template object from the chosen template name
680                def templates = [:];
681                flow.sampleForm.templateId.each {
682                        templates[ it.key ] = it.value ? Template.get( it.value ) : null;
683                }
684               
685                def headers = flow.excel.data.header;
686
687                if( !params.matches ) {
688                        log.error( ".simple study wizard no column matches given" );
689                        flash.error = "No column matches given";
690                        return false;
691                }
692
693                // Retrieve the chosen matches from the request parameters and put them into
694                // the headers-structure, for later reference
695                params.matches.index.each { columnindex, value ->
696                        // Determine the entity and property by splitting it
697                        def parts = value.toString().tokenize( "||" );
698                       
699                        def property
700                        def entityName
701                        if( parts.size() > 1 ) {
702                                property = parts[ 1 ];
703                                entityName = "dbnp.studycapturing." + parts[ 0 ];
704                        } else if( parts.size() == 1 ) {
705                                property = parts[ 0 ];
706                                entityName = headers[columnindex.toInteger()].entityclass.getName();
707                        }
708                       
709                        // Create an actual class instance of the selected entity with the selected template
710                        // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities
711                        def entityClass = Class.forName( entityName, true, this.getClass().getClassLoader())
712                        def entityObj = entityClass.newInstance(template: templates[ entityName[entityName.lastIndexOf( '.' ) + 1..-1] ])
713
714                        headers[ columnindex.toInteger() ].entityclass = entityClass
715                       
716                        // Store the selected property for this column into the column map for the ImporterService
717                        headers[columnindex.toInteger()].property = property
718
719                        // Look up the template field type of the target TemplateField and store it also in the map
720                        headers[columnindex.toInteger()].templatefieldtype = entityObj.giveFieldType(property)
721
722                        // Is a "Don't import" property assigned to the column?
723                        headers[columnindex.toInteger()].dontimport = (property == "dontimport") ? true : false
724
725                        //if it's an identifier set the mapping column true or false
726                        entityClass.giveDomainFields().each {
727                                headers[columnindex.toInteger()].identifier = ( it.preferredIdentifier && (it.name == property) )
728                        }
729                }
730
731                // Import the workbook and store the table with entity records and store the failed cells
732                println "Importing samples for study " + study + " (" + study.id + ")";
733               
734                def importedfile = fileService.get( flow.excel.filename )
735                def workbook
736                if (importedfile.exists()) {
737                        try {
738                                workbook = importerService.getWorkbook(new FileInputStream(importedfile))
739                        } catch (Exception e) {
740                                log.error ".simple study wizard could not load file: " + e
741                                flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
742                                return false
743                        }
744                } else {
745                        log.error ".simple study wizard no file given";
746                        flash.error = "No file was given. Please provide an excel file for entering samples.";
747                        return false;
748                }
749
750                if( !workbook ) {
751                        log.error ".simple study wizard could not load file into a workbook"
752                        flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
753                        return false
754                }
755                       
756                def imported = importerService.importOrUpdateDataBySampleIdentifier(templates,
757                                workbook,
758                                flow.excel.sheetIndex - 1,
759                                flow.excel.dataMatrixStart - 1,
760                                flow.excel.data.header,
761                                flow.study,
762                                true                    // Also create entities for which no data is imported but where templates were chosen
763                );
764
765                def table = imported.table
766                def failedcells = imported.failedCells
767
768                flow.imported = [
769                        data: table,
770                        failedCells: failedcells
771                ];
772       
773                // loop through all entities to validate them and add them to failedcells if an error occurs
774                def numInvalidEntities = 0;
775                def errors = [];
776
777                // Add all samples
778                table.each { record ->
779                        record.each { entity ->
780                                if( entity ) {
781                                        // Determine entity class and add a parent. Add the entity to the study
782                                        def preferredIdentifier = importerService.givePreferredIdentifier( entity.class );
783                                        def equalClosure = { it.getIdentifier() == entity.getIdentifier() }
784                                        def entityName = entity.class.name[ entity.class.name.lastIndexOf( "." ) + 1 .. -1 ]
785
786                                        entity.parent = study
787                                       
788                                        switch( entity.class ) {
789                                                case Sample:
790                                                        if( !study.samples?.find( equalClosure ) ) {
791                                                                study.addToSamples( entity );
792                                                        }
793                                                       
794                                                        // If an eventgroup is created, add it to the study
795                                                        // The eventgroup must have a unique name, but the user shouldn't be bothered with it
796                                                        // Add 'group ' + samplename and it that is not unique, add a number to it
797                                                        if( entity.parentEventGroup ) {
798                                                                study.addToEventGroups( entity.parentEventGroup )
799
800                                                                entity.parentEventGroup.name = "Group " + entity.name
801                                                                while( !entity.parentEventGroup.validate() ) {
802                                                                        entity.parentEventGroup.getErrors().each { println it }
803                                                                        entity.parentEventGroup.name += "" + Math.floor( Math.random() * 100 )
804                                                                }
805                                                        }
806                                                       
807                                                        break;
808                                                case Subject:
809                                                        if( !study.samples?.find( equalClosure ) ) {
810                                                               
811                                                                if( preferredIdentifier ) {
812                                                                        // Subjects without a name should just be called 'subject'
813                                                                        if( !entity.getFieldValue( preferredIdentifier.name ) )
814                                                                                entity.setFieldValue( preferredIdentifier.name, "Subject" );
815                                                               
816                                                                        // Subjects should have unique names; if the user has entered the same name multiple times,
817                                                                        // the subject will be renamed
818                                                                        def baseName = entity.getFieldValue( preferredIdentifier.name )
819                                                                        def counter = 2;
820                                                                       
821                                                                        while( study.subjects?.find { it.getFieldValue( preferredIdentifier.name ) == entity.getFieldValue( preferredIdentifier.name ) } ) {
822                                                                                entity.setFieldValue( preferredIdentifier.name, baseName + " (" + counter++ + ")" )
823                                                                        }
824                                                                }
825                                                               
826                                                                study.addToSubjects( entity );
827                                                       
828                                                        }
829                                                       
830                                                        break;
831                                                case Event:
832                                                        if( !study.events?.find( equalClosure ) ) {
833                                                                study.addToEvents( entity );
834                                                        }
835                                                        break;
836                                                case SamplingEvent:
837                                                        // Sampling events have a 'sampleTemplate' value, which should be filled by the
838                                                        // template that is chosen for samples.
839                                                        if( !entity.getFieldValue( 'sampleTemplate' ) ) {
840                                                                entity.setFieldValue( 'sampleTemplate', flow.sampleForm.template.Sample.name )
841                                                        } 
842                                               
843                                                        if( !study.samplingEvents?.find( equalClosure ) ) {
844                                                                study.addToSamplingEvents( entity );
845                                                        }
846                                                        break;
847                                        }
848                                       
849                                        if (!entity.validate()) {
850                                                numInvalidEntities++;
851                                               
852                                                // Add this field to the list of failed cells, in order to give the user feedback
853                                                failedcells = addNonValidatingCells( failedcells, entity, flow )
854       
855                                                // Also create a full list of errors
856                                                def currentErrors = getHumanReadableErrors( entity )
857                                                if( currentErrors ) {
858                                                        currentErrors.each {
859                                                                errors += "(" + entityName + ") " + it.value;
860                                                        }
861                                                }
862                                        }
863                                }
864                        }
865                }
866
867                flow.imported.numInvalidEntities = numInvalidEntities + failedcells?.size();
868                flow.imported.errors = errors;
869
870                return true
871        }
872       
873        /**
874         * Handles the update of the edited fields by the user
875         * @param study         Study to update
876         * @param params                Request parameter map
877         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error.
878         *                                      The field session.simpleWizard.imported.numInvalidEntities reflects the number of
879         *                                      entities that still have errors, and should be fixed before saving. The errors for those entities
880         *                                      are saved into session.simpleWizard.imported.errors
881         */
882        def handleMissingFields( study, params, flow ) {
883                def numInvalidEntities = 0;
884                def errors = [];
885
886                // Check which fields failed previously
887                def failedCells = flow.imported.failedCells
888                def newFailedCells = [];
889
890                flow.imported.data.each { table ->
891                        table.each { entity ->
892                                def invalidFields = 0
893                                def failed = new ImportRecord();
894                                def entityName = entity.class.name[ entity.class.name.lastIndexOf( "." ) + 1 .. -1 ]
895                               
896
897                                // Set the fields for this entity by retrieving values from the params
898                                entity.giveFields().each { field ->
899                                        def fieldName = importerService.getFieldNameInTableEditor( entity, field );
900
901                                        if( params[ fieldName ] == "#invalidterm" ) {
902                                                // If the value '#invalidterm' is chosen, the user hasn't fixed anything, so this field is still incorrect
903                                                invalidFields++;
904                                               
905                                                // store the mapping column and value which failed
906                                                def identifier = entityName.toLowerCase() + "_" + entity.getIdentifier() + "_" + fieldName
907                                                def mcInstance = new MappingColumn()
908                                                failed.addToImportcells(new ImportCell(mappingcolumn: mcInstance, value: params[ fieldName ], entityidentifier: identifier))
909                                        } else {
910                                                if( field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM || field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST ) {
911                                                        // If this field is an ontologyterm field or a stringlist field, the value has changed, so remove the field from
912                                                        // the failedCells list
913                                                        importerService.removeFailedCell( failedCells, entity, field )
914                                                }
915
916                                                // Update the field, regardless of the type of field
917                                                entity.setFieldValue(field.name, params[ fieldName ] )
918                                        }
919                                }
920                               
921                                // Try to validate the entity now all fields have been set. If it fails, return an error
922                                if (!entity.validate() || invalidFields) {
923                                        numInvalidEntities++;
924
925                                        // Add this field to the list of failed cells, in order to give the user feedback
926                                        failedCells = addNonValidatingCellsToImportRecord( failed, entity, flow )
927
928                                        // Also create a full list of errors
929                                        def currentErrors = getHumanReadableErrors( entity )
930                                        if( currentErrors ) {
931                                                currentErrors.each {
932                                                        errors += "(" + entityName + ") " + it.value;
933                                                }
934                                        }
935                                       
936                                        newFailedCells << failed;
937                                } else {
938                                        importerService.removeFailedCell( failedCells, entity )
939                                }
940                        } // end of record
941                } // end of table
942
943                flow.imported.failedCells = newFailedCells
944                flow.imported.numInvalidEntities = numInvalidEntities;
945                flow.imported.errors = errors;
946
947                return numInvalidEntities == 0
948        }
949       
950        /**
951        * Handles assay input
952        * @param study          Study to update
953        * @param params         Request parameter map
954        * @return                       True if everything went OK, false otherwise. An error message is put in flash.error
955        */
956   def handleAssays( assay, params, flow ) {
957           // did the study template change?
958           if (params.get('template') && assay.template?.name != params.get('template')) {
959                   // set the template
960                   assay.template = Template.findByName(params.remove('template'))
961           }
962
963           // does the study have a template set?
964           if (assay.template && assay.template instanceof Template) {
965                   // yes, iterate through template fields
966                   assay.giveFields().each() {
967                           // and set their values
968                           assay.setFieldValue(it.name, params.get(it.escapedName()))
969                   }
970           }
971
972           return true
973   }
974       
975       
976        /**
977         * Checks whether the given study is simple enough to be edited using this controller.
978         *
979         * The study is simple enough if the samples, subjects, events and samplingEvents can be
980         * edited as a flat table. That is:
981         *              - Every subject belongs to 0 or 1 eventgroup
982         *              - Every eventgroup belongs to 0 or 1 sample
983         *              - Every eventgroup has 0 or 1 subjects, 0 or 1 event and 0 or 1 samplingEvents
984         *              - If a sample belongs to an eventgroup:
985         *                      - If that eventgroup has a samplingEvent, that same samplingEvent must also be
986         *                              the sampling event that generated this sample
987         *                      - If that eventgroup has a subject, that same subject must also be the subject
988         *                              from whom the sample was taken
989         *
990         * @param study         Study to check
991         * @return                      True if the study can be edited by this controller, false otherwise
992         */
993        def checkStudySimplicity( study ) {
994                def simplicity = true;
995
996                if( !study )
997                        return false
998
999                if( study.eventGroups ) {
1000                        study.eventGroups.each { eventGroup ->
1001                                // Check for simplicity of eventgroups: only 0 or 1 subject, 0 or 1 event and 0 or 1 samplingEvent
1002                                if( eventGroup.subjects?.size() > 1 || eventGroup.events?.size() > 1 || eventGroup.samplingEvents?.size() > 1 ) {
1003                                        flash.message = "One or more eventgroups contain multiple subjects or events."
1004                                        simplicity = false;
1005                                }
1006
1007                                // Check whether this eventgroup only belongs to (max) 1 sample
1008                                def numSamples = 0;
1009                                study.samples.each { sample ->
1010                                        // If no id is given for the eventGroup, it has been entered in this wizard, but
1011                                        // not yet saved. In that case, it is always OK
1012                                        if( eventGroup.id && sample.parentEventGroup?.id == eventGroup.id )
1013                                                numSamples++;
1014                                }
1015
1016                                if( numSamples > 1 ) {
1017                                        flash.message = "One or more eventgroups belong to multiple samples."
1018                                        simplicity = false;
1019                                }
1020                        }
1021
1022                        if( !simplicity ) return false;
1023
1024                        // Check whether subject only belong to zero or one event group
1025                        if( study.subjects ) {
1026                                study.subjects.each { subject ->
1027                                        def numEventGroups = 0
1028                                        study.eventGroups.each { eventGroup ->
1029                                                // If no id is given for the subject, it has been entered in this wizard, but
1030                                                // not yet saved. In that case, it is always OK
1031                                                if( subject.id && eventGroup.subjects && eventGroup.subjects.toList()[0]?.id == subject.id )
1032                                                        numEventGroups++
1033                                        }
1034
1035                                        if( numEventGroups > 1 ) {
1036                                                flash.message = "One or more subjects belong to multiple eventgroups."
1037                                                simplicity = false;
1038                                        }
1039                                }
1040                        }
1041
1042                        if( !simplicity ) return false;
1043
1044                        // Check whether the samples that belong to an eventgroup have the right parentObjects
1045                        study.samples.each { sample ->
1046                                if( sample.parentEventGroup ) {
1047                                        // If no id is given for the subject, it has been entered in this wizard, but
1048                                        // not yet saved. In that case, it is always OK
1049                                        if( sample.parentSubject && sample.parentSubject.id) {
1050                                                if( !sample.parentEventGroup.subjects || sample.parentEventGroup.subjects.toList()[0]?.id != sample.parentSubject.id ) {
1051                                                        flash.message = "The structure of the eventgroups of one or more samples is too complex"
1052                                                        simplicity = false;
1053                                                }
1054                                        }
1055
1056                                        // If no id is given for the sampling event, it has been entered in this wizard, but
1057                                        // not yet saved. In that case, it is always OK
1058                                        if( sample.parentEvent && sample.parentEvent.id) {
1059                                                if( !sample.parentEventGroup.samplingEvents || sample.parentEventGroup.samplingEvents.toList()[0]?.id != sample.parentEvent.id ) {
1060                                                        flash.message = "The structure of the eventgroups of one or more samples is too complex"
1061                                                        simplicity = false;
1062                                                }
1063                                        }
1064                                }
1065                        }
1066
1067                        if( !simplicity ) return false;
1068                }
1069
1070                return simplicity;
1071        }
1072
1073       
1074        /**
1075         * Adds all fields of this entity that have given an error when validating to the failedcells list
1076         * @param failedcells   Current list of ImportRecords
1077         * @param entity                Entity to check. The entity must have been validated before
1078         * @return                              Updated list of ImportRecords
1079         */
1080        protected def addNonValidatingCells( failedcells, entity, flow ) {
1081                // Add this entity and the fields with an error to the failedCells list
1082                ImportRecord failedRecord = addNonValidatingCellsToImportRecord( new ImportRecord(), entity, flow );
1083
1084                failedcells.add( failedRecord );
1085
1086                return failedcells
1087        }
1088       
1089        /**
1090        * Adds all fields of this entity that have given an error when validating to the failedcells list
1091        * @param failedcells    Current list of ImportRecords
1092        * @param entity         Entity to check. The entity must have been validated before
1093        * @return                               Updated list of ImportRecords
1094        */
1095   protected def addNonValidatingCellsToImportRecord( failedRecord, entity, flow ) {
1096           entity.getErrors().getFieldErrors().each { error ->
1097                   String field = error.getField();
1098                   
1099                   def mc = importerService.findMappingColumn( flow.excel.data.header, field );
1100                   def mcInstance = new MappingColumn( name: field, entityClass: Sample.class, index: -1, property: field.toLowerCase(), templateFieldType: entity.giveFieldType( field ) );
1101
1102                   // Create a clone of the mapping column
1103                   if( mc ) {
1104                           mcInstance.properties = mc.properties
1105                   }
1106
1107                   failedRecord.addToImportcells( new ImportCell(mappingcolumn: mcInstance, value: error.getRejectedValue(), entityidentifier: importerService.getFieldNameInTableEditor( entity, field ) ) )
1108           }
1109           
1110           return failedRecord
1111   }
1112
1113       
1114        /**
1115        * Checks an excel workbook whether the given sheetindex and rownumbers are correct
1116        * @param workbook                       Excel workbook to read
1117        * @param sheetIndex             1-based sheet index for the sheet to read (1=first sheet)
1118        * @param headerRow                      1-based row number for the header row (1=first row)
1119        * @param dataMatrixStart        1-based row number for the first data row (1=first row)
1120        * @return                                       True if the sheet index and row numbers are correct.
1121        */
1122   protected boolean excelChecks( def workbook, int sheetIndex, int headerRow, int dataMatrixStart ) {
1123           // Perform some basic checks on the excel file. These checks should be performed by the importerservice
1124           // in a perfect scenario.
1125           if( sheetIndex > workbook.getNumberOfSheets() ) {
1126                   log.error ".simple study wizard Sheet index is too high: " + sheetIndex + " / " + workbook.getNumberOfSheets();
1127                   flash.error = "Your excel sheet contains too few excel sheets. The provided excel sheet has only " + workbook.getNumberOfSheets() + " sheet(s).";
1128                   return false
1129           }
1130
1131           def sheet = workbook.getSheetAt(sheetIndex - 1);
1132           def firstRowNum = sheet.getFirstRowNum();
1133           def lastRowNum = sheet.getLastRowNum();
1134           def numRows = lastRowNum - firstRowNum + 1;
1135
1136           if( headerRow > numRows  ) {
1137                   log.error ".simple study wizard Header row number is incorrect: " + headerRow + " / " + numRows;
1138                   flash.error = "Your excel sheet doesn't contain enough rows (" + numRows + "). Please provide an excel sheet with one header row and data below";
1139                   return false
1140           }
1141
1142           if( dataMatrixStart > numRows  ) {
1143                   log.error ".simple study wizard Data row number is incorrect: " + dataMatrixStart + " / " + numRows;
1144                   flash.error = "Your excel sheet doesn't contain enough rows (" + numRows + "). Please provide an excel sheet with one header row and data below";
1145                   return false
1146           }
1147
1148           return true;
1149   }
1150       
1151        /**
1152         * Validates an object and puts human readable errors in validationErrors variable
1153         * @param entity                Entity to validate
1154         * @return                      True iff the entity validates, false otherwise
1155         */
1156        protected boolean validateObject( def entity ) {
1157                if( !entity.validate() ) {
1158                        flash.validationErrors = getHumanReadableErrors( entity )
1159                        return false;
1160                }
1161                return true;
1162        }
1163
1164        /**
1165         * transform domain class validation errors into a human readable
1166         * linked hash map
1167         * @param object validated domain class
1168         * @return object  linkedHashMap
1169         */
1170        def getHumanReadableErrors(object) {
1171                def errors = [:]
1172                object.errors.getAllErrors().each() { error ->
1173                        // error.codes.each() { code -> println code }
1174
1175                        // generally speaking g.message(...) should work,
1176                        // however it fails in some steps of the wizard
1177                        // (add event, add assay, etc) so g is not always
1178                        // availably. Using our own instance of the
1179                        // validationTagLib instead so it is always
1180                        // available to us
1181                        errors[error.getArguments()[0]] = validationTagLib.message(error: error)
1182                }
1183
1184                return errors
1185        }
1186}
Note: See TracBrowser for help on using the repository browser.