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

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

Added entity names above table editor in simple wizard and added template description to template select boxes (#378)

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