root/trunk/grails-app/controllers/dbnp/studycapturing/SimpleWizardController.groovy @ 1608

Revision 1608, 34.1 KB (checked in by robert@…, 3 years ago)

Fixed bug with hibernate and transactional services in the simple wizard

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