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

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

Bugfix with uploaded files with less than 9 characters

  • Property svn:keywords set to Rev Author Date
File size: 41.6 KB
Line 
1/**
2 * SimpleWizardController Controler
3 *
4 * Description of my controller
5 *
6 * @author  your email (+name?)
7 * @since       2010mmdd
8 * @package     ???
9 *
10 * Revision information:
11 * $Rev: 1907 $
12 * $Author: robert@isdat.nl $
13 * $Date: 2011-06-01 14:03:09 +0000 (wo, 01 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                                // 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
614                try {           
615                        importerHeader = importerService.getHeader(workbook,
616                                        sheetIndex - 1,                 // 0 == first sheet
617                                        headerRow,                              // 1 == first row :s
618                                        dataMatrixStart - 1,    // 0 == first row
619                                        Sample.class)
620               
621                        importerDataMatrix = importerService.getDatamatrix(
622                                        workbook,
623                                        importerHeader,
624                                        sheetIndex - 1,                 // 0 == first sheet
625                                        dataMatrixStart - 1,    // 0 == first row
626                                        5)
627                } catch( Exception e ) {
628                        // An error occurred while reading the excel file.
629                        log.error ".simple study wizard error while reading the excel file";
630                        e.printStackTrace();
631
632                        // Show a message to the user
633                        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.";
634                        return false;
635                }
636
637                // Match excel columns with template fields
638                def fieldNames = [];
639                flow.sampleForm.template.each { template ->
640                        if( template.value ) {
641                                def fields = template.value.entity.giveDomainFields() + template.value.getFields();
642                                fields.each { field ->
643                                        if( !field.entity )
644                                                field.entity = template.value.entity
645                                               
646                                        fieldNames << field
647                                }
648                        }
649                }
650                importerHeader.each { mc ->
651                        def bestfit = importerService.mostSimilar( mc.name, fieldNames, 0.8);
652                        if( bestfit ) {
653                                // Remove this fit from the list
654                                fieldNames.remove( bestfit );
655                               
656                                mc.entityclass = bestfit.entity
657                                mc.property = bestfit.name
658                        }
659                }
660               
661                // Save read excel data into session
662                def dataMatrix = [];
663                def df = new DataFormatter();
664                importerDataMatrix.each {
665                        dataMatrix << it.collect{ it ? df.formatCellValue(it) : "" }
666                }
667               
668                flow.excel = [
669                                        filename: filename,
670                                        sheetIndex: sheetIndex,
671                                        dataMatrixStart: dataMatrixStart,
672                                        headerRow: headerRow,
673                                        data: [
674                                                header: importerHeader,
675                                                dataMatrix: dataMatrix
676                                        ]
677                                ]
678
679                return true
680        }
681       
682        /**
683         * Copies data from the submitted sample form to the flow
684         * @param study
685         * @param params
686         * @param flow
687         * @return
688         */
689        protected def handleSampleForm( study, params, flow ) {
690                def sampleTemplateId  = params.long( 'sample_template_id' )
691                def subjectTemplateId  = params.long( 'subject_template_id' )
692                def eventTemplateId  = params.long( 'event_template_id' )
693                def samplingEventTemplateId  = params.long( 'samplingEvent_template_id' )
694
695                // Save form data in session
696                if( !flow.sampleForm )
697                        flow.sampleForm = [:]
698                       
699                flow.sampleForm.templateId = [
700                                                'Subject': subjectTemplateId,
701                                                'Event': eventTemplateId,
702                                                'SamplingEvent': samplingEventTemplateId,
703                                                'Sample': sampleTemplateId
704                ];
705                flow.sampleForm.template = [
706                                                'Subject': subjectTemplateId ? Template.get( subjectTemplateId ) : null,
707                                                'Event': eventTemplateId ? Template.get( eventTemplateId ) : null,
708                                                'SamplingEvent': samplingEventTemplateId ? Template.get( samplingEventTemplateId ) : null,
709                                                'Sample': sampleTemplateId ? Template.get( sampleTemplateId ) : null
710                ];
711        }
712       
713        /**
714         * Handles the matching of template fields with excel columns by the user
715         * @param study         Study to update
716         * @param params        Request parameter map
717         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error
718         *                                      The field session.simpleWizard.imported.numInvalidEntities reflects the number of
719         *                                      entities that have errors, and should be fixed before saving. The errors for those entities
720         *                                      are saved into session.simpleWizard.imported.errors
721         */
722        def handleColumns( study, params, flow ) {
723                // Find actual Template object from the chosen template name
724                def templates = [:];
725                flow.sampleForm.templateId.each {
726                        templates[ it.key ] = it.value ? Template.get( it.value ) : null;
727                }
728               
729                def headers = flow.excel.data.header;
730
731                if( !params.matches ) {
732                        log.error( ".simple study wizard no column matches given" );
733                        flash.error = "No column matches given";
734                        return false;
735                }
736
737                // Retrieve the chosen matches from the request parameters and put them into
738                // the headers-structure, for later reference
739                params.matches.index.each { columnindex, value ->
740                        // Determine the entity and property by splitting it
741                        def parts = value.toString().tokenize( "||" );
742                       
743                        def property
744                        def entityName
745                        if( parts.size() > 1 ) {
746                                property = parts[ 1 ];
747                                entityName = "dbnp.studycapturing." + parts[ 0 ];
748                        } else if( parts.size() == 1 ) {
749                                property = parts[ 0 ];
750                                entityName = headers[columnindex.toInteger()].entityclass.getName();
751                        }
752                       
753                        // Create an actual class instance of the selected entity with the selected template
754                        // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities
755                        def entityClass = Class.forName( entityName, true, this.getClass().getClassLoader())
756                        def entityObj = entityClass.newInstance(template: templates[ entityName[entityName.lastIndexOf( '.' ) + 1..-1] ])
757
758                        headers[ columnindex.toInteger() ].entityclass = entityClass
759                       
760                        // Store the selected property for this column into the column map for the ImporterService
761                        headers[columnindex.toInteger()].property = property
762
763                        // Look up the template field type of the target TemplateField and store it also in the map
764                        headers[columnindex.toInteger()].templatefieldtype = entityObj.giveFieldType(property)
765
766                        // Is a "Don't import" property assigned to the column?
767                        headers[columnindex.toInteger()].dontimport = (property == "dontimport") ? true : false
768
769                        //if it's an identifier set the mapping column true or false
770                        entityClass.giveDomainFields().each {
771                                headers[columnindex.toInteger()].identifier = ( it.preferredIdentifier && (it.name == property) )
772                        }
773                }
774
775                // Import the workbook and store the table with entity records and store the failed cells
776                //println "Importing samples for study " + study + " (" + study.id + ")";
777               
778                def importedfile = fileService.get( flow.excel.filename )
779                def workbook
780                if (importedfile.exists()) {
781                        try {
782                                workbook = importerService.getWorkbook(new FileInputStream(importedfile))
783                        } catch (Exception e) {
784                                log.error ".simple study wizard could not load file: " + e
785                                flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
786                                return false
787                        }
788                } else {
789                        log.error ".simple study wizard no file given";
790                        flash.error = "No file was given. Please provide an excel file for entering samples.";
791                        return false;
792                }
793
794                if( !workbook ) {
795                        log.error ".simple study wizard could not load file into a workbook"
796                        flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
797                        return false
798                }
799                       
800                def imported = importerService.importOrUpdateDataBySampleIdentifier(templates,
801                                workbook,
802                                flow.excel.sheetIndex - 1,
803                                flow.excel.dataMatrixStart - 1,
804                                flow.excel.data.header,
805                                flow.study,
806                                true                    // Also create entities for which no data is imported but where templates were chosen
807                );
808
809                def table = imported.table
810                def failedcells = imported.failedCells
811
812                flow.imported = [
813                        data: table,
814                        failedCells: failedcells
815                ];
816
817        // loop through all entities to validate them and add them to failedcells if an error occurs
818        def numInvalidEntities = 0;
819        def errors = [];
820
821        // Add all samples
822        table.each { record ->
823            record.each { entity ->
824                if( entity ) {
825                    // Determine entity class and add a parent. Add the entity to the study
826                    def preferredIdentifier = importerService.givePreferredIdentifier( entity.class );
827                    def equalClosure = { it.getIdentifier() == entity.getIdentifier() }
828                    def entityName = entity.class.name[ entity.class.name.lastIndexOf( "." ) + 1 .. -1 ]
829
830                    entity.parent = study
831
832                    switch( entity.class ) {
833                        case Sample:
834                            if( !study.samples?.find( equalClosure ) ) {
835                                study.addToSamples( entity );
836                            }
837
838                            // If an eventgroup is created, add it to the study
839                            // The eventgroup must have a unique name, but the user shouldn't be bothered with it
840                            // Add 'group ' + samplename and it that is not unique, add a number to it
841                            if( entity.parentEventGroup ) {
842                                study.addToEventGroups( entity.parentEventGroup )
843
844                                entity.parentEventGroup.name = "Group " + entity.name
845                                while( !entity.parentEventGroup.validate() ) {
846                                    //entity.parentEventGroup.getErrors().each { println it }
847                                    entity.parentEventGroup.name += "" + Math.floor( Math.random() * 100 )
848                                }
849                            }
850
851                            break;
852                        case Subject:
853                            if( !study.samples?.find( equalClosure ) ) {
854
855                                if( preferredIdentifier ) {
856                                    // Subjects without a name should just be called 'subject'
857                                    if( !entity.getFieldValue( preferredIdentifier.name ) )
858                                        entity.setFieldValue( preferredIdentifier.name, "Subject" );
859
860                                    // Subjects should have unique names; if the user has entered the same name multiple times,
861                                    // the subject will be renamed
862                                    def baseName = entity.getFieldValue( preferredIdentifier.name )
863                                    def counter = 2;
864
865                                    while( study.subjects?.find { it.getFieldValue( preferredIdentifier.name ) == entity.getFieldValue( preferredIdentifier.name ) } ) {
866                                        entity.setFieldValue( preferredIdentifier.name, baseName + " (" + counter++ + ")" )
867                                    }
868                                }
869
870                                study.addToSubjects( entity );
871
872                            }
873
874                            break;
875                        case Event:
876                            if( !study.events?.find( equalClosure ) ) {
877                                study.addToEvents( entity );
878                            }
879                            break;
880                        case SamplingEvent:
881                            // Sampling events have a 'sampleTemplate' value, which should be filled by the
882                            // template that is chosen for samples.
883                            if( !entity.getFieldValue( 'sampleTemplate' ) ) {
884                                entity.setFieldValue( 'sampleTemplate', flow.sampleForm.template.Sample.name )
885                            }
886
887                            if( !study.samplingEvents?.find( equalClosure ) ) {
888                                study.addToSamplingEvents( entity );
889                            }
890                            break;
891                    }
892
893                    if (!entity.validate()) {
894                        numInvalidEntities++;
895
896                        // Add this field to the list of failed cells, in order to give the user feedback
897                        failedcells = addNonValidatingCells( failedcells, entity, flow )
898
899                        // Also create a full list of errors
900                        def currentErrors = getHumanReadableErrors( entity )
901                        if( currentErrors ) {
902                            currentErrors.each {
903                                errors += "(" + entityName + ") " + it.value;
904                            }
905                        }
906                    }
907                }
908            }
909        }
910
911                flow.imported.numInvalidEntities = numInvalidEntities + failedcells?.size();
912                flow.imported.errors = errors;
913
914                return true
915        }
916       
917        /**
918         * Handles the update of the edited fields by the user
919         * @param study         Study to update
920         * @param params                Request parameter map
921         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error.
922         *                                      The field session.simpleWizard.imported.numInvalidEntities reflects the number of
923         *                                      entities that still have errors, and should be fixed before saving. The errors for those entities
924         *                                      are saved into session.simpleWizard.imported.errors
925         */
926        def handleMissingFields( study, params, flow ) {
927                def numInvalidEntities = 0;
928                def errors = [];
929
930                // Check which fields failed previously
931                def failedCells = flow.imported.failedCells
932                def newFailedCells = [];
933
934                flow.imported.data.each { table ->
935                        table.each { entity ->
936                                def invalidFields = 0
937                                def failed = new ImportRecord();
938                                def entityName = entity.class.name[ entity.class.name.lastIndexOf( "." ) + 1 .. -1 ]
939                               
940
941                                // Set the fields for this entity by retrieving values from the params
942                                entity.giveFields().each { field ->
943                                        def fieldName = importerService.getFieldNameInTableEditor( entity, field );
944
945                                        if( params[ fieldName ] == "#invalidterm" ) {
946                                                // If the value '#invalidterm' is chosen, the user hasn't fixed anything, so this field is still incorrect
947                                                invalidFields++;
948                                               
949                                                // store the mapping column and value which failed
950                                                def identifier = entityName.toLowerCase() + "_" + entity.getIdentifier() + "_" + fieldName
951                                                def mcInstance = new MappingColumn()
952                                                failed.addToImportcells(new ImportCell(mappingcolumn: mcInstance, value: params[ fieldName ], entityidentifier: identifier))
953                                        } else {
954                                                if( field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM || field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST ) {
955                                                        // If this field is an ontologyterm field or a stringlist field, the value has changed, so remove the field from
956                                                        // the failedCells list
957                                                        importerService.removeFailedCell( failedCells, entity, field )
958                                                }
959
960                                                // Update the field, regardless of the type of field
961                                                entity.setFieldValue(field.name, params[ fieldName ] )
962                                        }
963                                }
964                               
965                                // Try to validate the entity now all fields have been set. If it fails, return an error
966                                if (!entity.validate() || invalidFields) {
967                                        numInvalidEntities++;
968
969                                        // Add this field to the list of failed cells, in order to give the user feedback
970                                        failedCells = addNonValidatingCellsToImportRecord( failed, entity, flow )
971
972                                        // Also create a full list of errors
973                                        def currentErrors = getHumanReadableErrors( entity )
974                                        if( currentErrors ) {
975                                                currentErrors.each {
976                                                        errors += "(" + entityName + ") " + it.value;
977                                                }
978                                        }
979                                       
980                                        newFailedCells << failed;
981                                } else {
982                                        importerService.removeFailedCell( failedCells, entity )
983                                }
984                        } // end of record
985                } // end of table
986
987                flow.imported.failedCells = newFailedCells
988                flow.imported.numInvalidEntities = numInvalidEntities;
989                flow.imported.errors = errors;
990
991                return numInvalidEntities == 0
992        }
993       
994        /**
995        * Handles assay input
996        * @param study          Study to update
997        * @param params         Request parameter map
998        * @return                       True if everything went OK, false otherwise. An error message is put in flash.error
999        */
1000   def handleAssays( assay, params, flow ) {
1001           // did the study template change?
1002           if (params.get('template') && assay.template?.name != params.get('template')) {
1003                   // set the template
1004                   assay.template = Template.findByName(params.remove('template'))
1005           }
1006
1007           // does the study have a template set?
1008           if (assay.template && assay.template instanceof Template) {
1009                   // yes, iterate through template fields
1010                   assay.giveFields().each() {
1011                           // and set their values
1012                           assay.setFieldValue(it.name, params.get(it.escapedName()))
1013                   }
1014           }
1015
1016           return true
1017   }
1018       
1019       
1020        /**
1021         * Checks whether the given study is simple enough to be edited using this controller.
1022         *
1023         * The study is simple enough if the samples, subjects, events and samplingEvents can be
1024         * edited as a flat table. That is:
1025         *              - Every subject belongs to 0 or 1 eventgroup
1026         *              - Every eventgroup belongs to 0 or 1 sample
1027         *              - Every eventgroup has 0 or 1 subjects, 0 or 1 event and 0 or 1 samplingEvents
1028         *              - If a sample belongs to an eventgroup:
1029         *                      - If that eventgroup has a samplingEvent, that same samplingEvent must also be
1030         *                              the sampling event that generated this sample
1031         *                      - If that eventgroup has a subject, that same subject must also be the subject
1032         *                              from whom the sample was taken
1033         *
1034         * @param study         Study to check
1035         * @return                      True if the study can be edited by this controller, false otherwise
1036         */
1037        def checkStudySimplicity( study ) {
1038                def simplicity = true;
1039
1040                if( !study )
1041                        return false
1042
1043                if( study.eventGroups ) {
1044                        study.eventGroups.each { eventGroup ->
1045                                // Check for simplicity of eventgroups: only 0 or 1 subject, 0 or 1 event and 0 or 1 samplingEvent
1046                                if( eventGroup.subjects?.size() > 1 || eventGroup.events?.size() > 1 || eventGroup.samplingEvents?.size() > 1 ) {
1047                                        flash.message = "One or more eventgroups contain multiple subjects or events."
1048                                        simplicity = false;
1049                                }
1050
1051                                // Check whether this eventgroup only belongs to (max) 1 sample
1052                                def numSamples = 0;
1053                                study.samples.each { sample ->
1054                                        // If no id is given for the eventGroup, it has been entered in this wizard, but
1055                                        // not yet saved. In that case, it is always OK
1056                                        if( eventGroup.id && sample.parentEventGroup?.id == eventGroup.id )
1057                                                numSamples++;
1058                                }
1059
1060                                if( numSamples > 1 ) {
1061                                        flash.message = "One or more eventgroups belong to multiple samples."
1062                                        simplicity = false;
1063                                }
1064                        }
1065
1066                        if( !simplicity ) return false;
1067
1068                        // Check whether subject only belong to zero or one event group
1069                        if( study.subjects ) {
1070                                study.subjects.each { subject ->
1071                                        def numEventGroups = 0
1072                                        study.eventGroups.each { eventGroup ->
1073                                                // If no id is given for the subject, it has been entered in this wizard, but
1074                                                // not yet saved. In that case, it is always OK
1075                                                if( subject.id && eventGroup.subjects && eventGroup.subjects.toList()[0]?.id == subject.id )
1076                                                        numEventGroups++
1077                                        }
1078
1079                                        if( numEventGroups > 1 ) {
1080                                                flash.message = "One or more subjects belong to multiple eventgroups."
1081                                                simplicity = false;
1082                                        }
1083                                }
1084                        }
1085
1086                        if( !simplicity ) return false;
1087
1088                        // Check whether the samples that belong to an eventgroup have the right parentObjects
1089                        study.samples.each { sample ->
1090                                if( sample.parentEventGroup ) {
1091                                        // If no id is given for the subject, it has been entered in this wizard, but
1092                                        // not yet saved. In that case, it is always OK
1093                                        if( sample.parentSubject && sample.parentSubject.id) {
1094                                                if( !sample.parentEventGroup.subjects || sample.parentEventGroup.subjects.toList()[0]?.id != sample.parentSubject.id ) {
1095                                                        flash.message = "The structure of the eventgroups of one or more samples is too complex"
1096                                                        simplicity = false;
1097                                                }
1098                                        }
1099
1100                                        // If no id is given for the sampling event, it has been entered in this wizard, but
1101                                        // not yet saved. In that case, it is always OK
1102                                        if( sample.parentEvent && sample.parentEvent.id) {
1103                                                if( !sample.parentEventGroup.samplingEvents || sample.parentEventGroup.samplingEvents.toList()[0]?.id != sample.parentEvent.id ) {
1104                                                        flash.message = "The structure of the eventgroups of one or more samples is too complex"
1105                                                        simplicity = false;
1106                                                }
1107                                        }
1108                                }
1109                        }
1110
1111                        if( !simplicity ) return false;
1112                }
1113
1114                return simplicity;
1115        }
1116
1117       
1118        /**
1119         * Adds all fields of this entity that have given an error when validating to the failedcells list
1120         * @param failedcells   Current list of ImportRecords
1121         * @param entity                Entity to check. The entity must have been validated before
1122         * @return                              Updated list of ImportRecords
1123         */
1124        protected def addNonValidatingCells( failedcells, entity, flow ) {
1125                // Add this entity and the fields with an error to the failedCells list
1126                ImportRecord failedRecord = addNonValidatingCellsToImportRecord( new ImportRecord(), entity, flow );
1127
1128                failedcells.add( failedRecord );
1129
1130                return failedcells
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 addNonValidatingCellsToImportRecord( failedRecord, entity, flow ) {
1140           entity.getErrors().getFieldErrors().each { error ->
1141                   String field = error.getField();
1142                   
1143                   def mc = importerService.findMappingColumn( flow.excel.data.header, field );
1144                   def mcInstance = new MappingColumn( name: field, entityClass: Sample.class, index: -1, property: field.toLowerCase(), templateFieldType: entity.giveFieldType( field ) );
1145
1146                   // Create a clone of the mapping column
1147                   if( mc ) {
1148                           mcInstance.properties = mc.properties
1149                   }
1150
1151                   failedRecord.addToImportcells( new ImportCell(mappingcolumn: mcInstance, value: error.getRejectedValue(), entityidentifier: importerService.getFieldNameInTableEditor( entity, field ) ) )
1152           }
1153           
1154           return failedRecord
1155   }
1156
1157       
1158        /**
1159        * Checks an excel workbook whether the given sheetindex and rownumbers are correct
1160        * @param workbook                       Excel workbook to read
1161        * @param sheetIndex             1-based sheet index for the sheet to read (1=first sheet)
1162        * @param headerRow                      1-based row number for the header row (1=first row)
1163        * @param dataMatrixStart        1-based row number for the first data row (1=first row)
1164        * @return                                       True if the sheet index and row numbers are correct.
1165        */
1166   protected boolean excelChecks( def workbook, int sheetIndex, int headerRow, int dataMatrixStart ) {
1167           // Perform some basic checks on the excel file. These checks should be performed by the importerservice
1168           // in a perfect scenario.
1169           if( sheetIndex > workbook.getNumberOfSheets() ) {
1170                   log.error ".simple study wizard Sheet index is too high: " + sheetIndex + " / " + workbook.getNumberOfSheets();
1171                   flash.error = "Your excel sheet contains too few excel sheets. The provided excel sheet has only " + workbook.getNumberOfSheets() + " sheet(s).";
1172                   return false
1173           }
1174
1175           def sheet = workbook.getSheetAt(sheetIndex - 1);
1176           def firstRowNum = sheet.getFirstRowNum();
1177           def lastRowNum = sheet.getLastRowNum();
1178           def numRows = lastRowNum - firstRowNum + 1;
1179
1180           if( headerRow > numRows  ) {
1181                   log.error ".simple study wizard Header row number is incorrect: " + headerRow + " / " + numRows;
1182                   flash.error = "Your excel sheet doesn't contain enough rows (" + numRows + "). Please provide an excel sheet with one header row and data below";
1183                   return false
1184           }
1185
1186           if( dataMatrixStart > numRows  ) {
1187                   log.error ".simple study wizard Data row number is incorrect: " + dataMatrixStart + " / " + numRows;
1188                   flash.error = "Your excel sheet doesn't contain enough rows (" + numRows + "). Please provide an excel sheet with one header row and data below";
1189                   return false
1190           }
1191
1192           return true;
1193   }
1194       
1195        /**
1196         * Validates an object and puts human readable errors in validationErrors variable
1197         * @param entity                Entity to validate
1198         * @return                      True iff the entity validates, false otherwise
1199         */
1200        protected boolean validateObject( def entity ) {
1201                if( !entity.validate() ) {
1202                        flash.validationErrors = getHumanReadableErrors( entity )
1203                        return false;
1204                }
1205                return true;
1206        }
1207
1208        /**
1209         * transform domain class validation errors into a human readable
1210         * linked hash map
1211         * @param object validated domain class
1212         * @return object  linkedHashMap
1213         */
1214        def getHumanReadableErrors(object) {
1215                def errors = [:]
1216                object.errors.getAllErrors().each() { error ->
1217                        // error.codes.each() { code -> println code }
1218
1219                        // generally speaking g.message(...) should work,
1220                        // however it fails in some steps of the wizard
1221                        // (add event, add assay, etc) so g is not always
1222                        // availably. Using our own instance of the
1223                        // validationTagLib instead so it is always
1224                        // available to us
1225                        errors[error.getArguments()[0]] = validationTagLib.message(error: error)
1226                }
1227
1228                return errors
1229        }
1230}
Note: See TracBrowser for help on using the repository browser.