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

Revision 1838, 46.3 KB (checked in by s.h.sikkema@…, 3 years ago)

First version of study inferring in simple wizard (does not support editing yet and assumes species == Homo sapiens)

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