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

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