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

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

Improved simple import wizard to be able to import events

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