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

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

Update simple wizard to import subject without a name correctly and import date values in the right way

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