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

Revision 1944, 42.2 KB (checked in by robert@…, 3 years ago)

- Added warning to simplewizard for importing a large number of samples

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