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

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

Improved simple wizard so that is can edit studies

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