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

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

Save buttons in simple wizard

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