source: trunk/grails-app/services/dbnp/studycapturing/SimpleService.groovy @ 1605

Last change on this file since 1605 was 1605, checked in by s.h.sikkema@…, 12 years ago

commit for Robert: see if webflow works when all logic is moved to SimpleService?

File size: 31.5 KB
Line 
1/**
2 * SimpleService Service
3 *
4 * Description of my service
5 *
6 * @author  your email (+name?)
7 * @since       2010mmdd
8 * @package     ???
9 *
10 * Revision information:
11 * $Rev: 1430 $
12 * $Author: work@osx.eu $
13 * $Date: 2011-01-21 21:05:36 +0100 (Fri, 21 Jan 2011) $
14 */
15package dbnp.studycapturing
16
17import org.dbnp.gdt.Template
18import org.apache.poi.ss.usermodel.DataFormatter
19import dbnp.importer.ImportRecord
20import dbnp.importer.MappingColumn
21import dbnp.importer.ImportCell
22import dbnp.authentication.SecUser
23
24class SimpleService {
25
26    static transactional = true
27    def authenticationService
28    def importerService
29    def fileService
30
31    def validationTagLib = new ValidationTagLib()
32
33
34        /**
35         * Retrieves the required study from the database or return an empty Study object if
36         * no id is given
37         *
38         * @param params        Request parameters with params.id being the ID of the study to be retrieved
39         * @return                      A study from the database or an empty study if no id was given
40         */
41        protected Study getStudyFromRequest( def params ) {
42                int id = params.int( "id" );
43
44                if( !id ) {
45                        return new Study( title: "New study", owner: authenticationService.getLoggedInUser() );
46                }
47
48                Study s = Study.get( id );
49
50                if( !s ) {
51                        flash.error = "No study found with given id";
52                        return null;
53                }
54                if( !s.canWrite( authenticationService.getLoggedInUser() ) ) {
55                        flash.error = "No authorization to edit this study."
56                        return null;
57                }
58
59                return s
60        }
61
62        /**
63         * Handles study input
64         * @param study         Study to update
65         * @param params        Request parameter map
66         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error
67         */
68        def handleStudy( study, params ) {
69                // did the study template change?
70                if (params.get('template') && study.template?.name != params.get('template')) {
71                        // set the template
72                        study.template = Template.findByName(params.remove('template'))
73                }
74
75                // does the study have a template set?
76                if (study.template && study.template instanceof Template) {
77                        // yes, iterate through template fields
78                        study.giveFields().each() {
79                                // and set their values
80                                study.setFieldValue(it.name, params.get(it.escapedName()))
81                        }
82                }
83
84                // handle public checkbox
85                if (params.get("publicstudy")) {
86                        study.publicstudy = params.get("publicstudy")
87                }
88
89                // handle publications
90                handleStudyPublications(study, params)
91
92                // handle contacts
93                handleStudyContacts(study, params)
94
95                // handle users (readers, writers)
96                handleStudyUsers(study, params, 'readers')
97                handleStudyUsers(study, params, 'writers')
98
99                return true
100        }
101
102   /**
103    * re-usable code for handling publications form data
104        * @param study  Study object to update
105        * @param params GrailsParameterMap (the flow parameters = form data)
106        * @returns boolean
107        */
108   def handleStudyPublications(Study study,  params) {
109           if (study.publications) study.publications = []
110
111           // Check the ids of the pubblications that should be attached
112           // to this study. If they are already attached, keep 'm. If
113           // studies are attached that are not in the selected (i.e. the
114           // user deleted them), remove them
115           def publicationIDs = params.get('publication_ids')
116           if (publicationIDs) {
117                   // Find the individual IDs and make integers
118                   publicationIDs = publicationIDs.split(',').collect { Integer.parseInt(it, 10) }
119
120                   // First remove the publication that are not present in the array
121                   if( study.publications )
122                           study.publications.removeAll { publication -> !publicationIDs.find { id -> id == publication.id } }
123
124                   // Add those publications not yet present in the database
125                   publicationIDs.each { id ->
126                           if (!study.publications.find { publication -> id == publication.id }) {
127                                   def publication = Publication.get(id)
128                                   if (publication) {
129                                           study.addToPublications(publication)
130                                   } else {
131                                           log.info('.publication with ID ' + id + ' not found in database.')
132                                   }
133                           }
134                   }
135
136           } else {
137                   log.info('.no publications selected.')
138                   if( study.publications )
139                           study.publications.clear()
140           }
141   }
142
143
144    /**
145     * re-usable code for handling contacts form data
146     * @param study     Study object to update
147     * @param Map GrailsParameterMap (the flow parameters = form data)
148     * @return boolean
149     */
150    def handleStudyContacts(Study study, params) {
151        if (!study.persons) study.persons = []
152
153        // Check the ids of the contacts that should be attached
154        // to this study. If they are already attached, keep 'm. If
155        // studies are attached that are not in the selected (i.e. the
156        // user deleted them), remove them
157
158        // Contacts are saved as [person_id]-[role_id]
159        def contactIDs = params.get('contacts_ids')
160        if (contactIDs) {
161            // Find the individual IDs and make integers
162            contactIDs = contactIDs.split(',').collect {
163                def parts = it.split('-')
164                return [person: Integer.parseInt(parts[0]), role: Integer.parseInt(parts[1])]
165            }
166
167            // First remove the contacts that are not present in the array
168            if( study.persons ) {
169                study.persons.removeAll {
170                    studyperson -> !contactIDs.find { ids -> (ids.person == studyperson.person.id) && (ids.role == studyperson.role.id) }
171                }
172            }
173
174            // Add those contacts not yet present in the database
175            contactIDs.each { ids ->
176                if (!study.persons.find { studyperson -> (ids.person == studyperson.person.id) && (ids.role == studyperson.role.id) }) {
177                    def person = Person.get(ids.person)
178                    def role = PersonRole.get(ids.role)
179                    if (person && role) {
180                        // Find a studyperson object with these parameters
181                        def studyPerson = StudyPerson.findAll().find { studyperson -> studyperson.person.id == person.id && studyperson.role.id == role.id }
182
183                        // If if does not yet exist, save the example
184                        if (!studyPerson) {
185                            studyPerson = new StudyPerson(
186                                    person: person,
187                                    role: role
188                                    )
189                            studyPerson.save(flush: true)
190                        }
191
192                        study.addToPersons(studyPerson)
193                    } else {
194                        log.info('.person ' + ids.person + ' or Role ' + ids.role + ' not found in database.')
195                    }
196                }
197            }
198        } else {
199            log.info('.no persons selected.')
200            if( study.persons )
201                study.persons.clear()
202        }
203    }
204
205    /**
206     * re-usable code for handling contacts form data
207     * @param study     Study object to update
208     * @param Map GrailsParameterMap (the flow parameters = form data)
209     * @param String    'readers' or 'writers'
210     * @return boolean
211     */
212    def handleStudyUsers(Study study, params, type) {
213        def users = []
214
215        if (type == "readers" && study.readers ) {
216            users += study.readers
217        } else if (type == "writers" && study.writers ) {
218            users += study.writers
219        }
220
221        // Check the ids of the contacts that should be attached
222        // to this study. If they are already attached, keep 'm. If
223        // studies are attached that are not in the selected (i.e. the
224        // user deleted them), remove them
225
226        // Users are saved as user_id
227        def userIDs = params.get(type + '_ids')
228
229        if (userIDs) {
230            // Find the individual IDs and make integers
231            userIDs = userIDs.split(',').collect { Long.valueOf(it, 10) }
232
233            // First remove the publication that are not present in the array
234            users.removeAll { user -> !userIDs.find { id -> id == user.id } }
235
236            // Add those publications not yet present in the database
237            userIDs.each { id ->
238                if (!users.find { user -> id == user.id }) {
239                    def user = SecUser.get(id)
240                    if (user) {
241                        users.add(user)
242                    } else {
243                        log.info('.user with ID ' + id + ' not found in database.')
244                    }
245                }
246            }
247
248        } else {
249            log.info('.no users selected.')
250            users.clear()
251        }
252
253        if (type == "readers") {
254            if (study.readers)
255                study.readers.clear()
256
257            users.each { study.addToReaders(it) }
258        } else if (type == "writers") {
259            if (study.writers)
260                study.writers.clear()
261
262            users.each { study.addToWriters(it) }
263
264        }
265    }
266
267
268
269        /**
270        * Handles the editing of existing samples
271        * @param study          Study to update
272        * @param params         Request parameter map
273        * @return                       True if everything went OK, false otherwise. An error message is put in flash.error
274        */
275   def handleExistingSamples( study, params, flow ) {
276           flash.validationErrors = [:];
277
278           def errors = false;
279
280           // iterate through objects; set field values and validate the object
281           def eventgroups = study.samples.parentEventGroup.findAll { it }
282           def events;
283           if( !eventgroups )
284                   events = []
285           else
286                   events = eventgroups.events?.getAt(0);
287
288           def objects = [
289                   'Sample': study.samples,
290                   'Subject': study.samples.parentSubject.findAll { it },
291                   'SamplingEvent': study.samples.parentEvent.findAll { it },
292                   'Event': events.flatten().findAll { it }
293           ];
294
295           objects.each {
296                   def type = it.key;
297                   def entities = it.value;
298
299                   entities.each { entity ->
300                           // iterate through entity fields
301                           entity.giveFields().each() { field ->
302                                   def value = params.get( type.toLowerCase() + '_' + entity.getIdentifier() + '_' + field.escapedName())
303
304                                   // set field value; name cannot be set to an empty value
305                                   if (field.name != 'name' || value) {
306                                           log.info "setting "+field.name+" to "+value
307                                           entity.setFieldValue(field.name, value)
308                                   }
309                           }
310
311                           // has the template changed?
312                           def templateName = params.get(type.toLowerCase() + '_' + entity.getIdentifier() + '_template')
313                           if (templateName && entity.template?.name != templateName) {
314                                   entity.template = Template.findByName(templateName)
315                           }
316
317                           // validate sample
318                           if (!entity.validate()) {
319                                   errors = true;
320                                   flash.validationErrors << getHumanReadableErrors( entity )
321                           }
322                   }
323           }
324
325           return !errors
326   }
327
328        /**
329         * Handles the upload of sample data
330         * @param study         Study to update
331         * @param params        Request parameter map
332         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error
333         */
334        def handleSamples( study, params, flow ) {
335                def filename = params.get( 'importfile' );
336
337                // Handle 'existing*' in front of the filename. This is put in front to make a distinction between
338                // an already uploaded file test.txt (maybe moved to some other directory) and a newly uploaded file test.txt
339                // still being in the temporary directory.
340                // This import step doesn't have to make that distinction, since all files remain in the temporary directory.
341                if( filename == 'existing*' )
342                        filename = '';
343                else if( filename[0..8] == 'existing*' )
344                        filename = filename[9..-1]
345
346                def sampleTemplateId  = params.long( 'sample_template_id' )
347                def subjectTemplateId  = params.long( 'subject_template_id' )
348                def eventTemplateId  = params.long( 'event_template_id' )
349                def samplingEventTemplateId  = params.long( 'samplingEvent_template_id' )
350
351                // These fields have been removed from the form, so will always contain
352                // their default value. The code however remains like this for future use.
353                int sheetIndex = (params.int( 'sheetindex' ) ?: 1 )
354                int dataMatrixStart = (params.int( 'datamatrix_start' ) ?: 2 )
355                int headerRow = (params.int( 'headerrow' ) ?: 1 )
356
357                // Save form data in session
358                flow.sampleForm = [
359                                        importFile: filename,
360                                        templateId: [
361                                                'Sample': sampleTemplateId,
362                                                'Subject': subjectTemplateId,
363                                                'Event': eventTemplateId,
364                                                'SampingEvent': samplingEventTemplateId
365                                        ],
366                                        template: [
367                                                'Sample': sampleTemplateId ? Template.get( sampleTemplateId ) : null,
368                                                'Subject': subjectTemplateId ? Template.get( subjectTemplateId ) : null,
369                                                'Event': eventTemplateId ? Template.get( eventTemplateId ) : null,
370                                                'SampingEvent': samplingEventTemplateId ? Template.get( samplingEventTemplateId ) : null
371                                        ],
372                                        sheetIndex: sheetIndex,
373                                        dataMatrixStart: dataMatrixStart,
374                                        headerRow: headerRow
375                                ];
376
377                // Check whether the template exists
378                if (!sampleTemplateId || !Template.get( sampleTemplateId ) ){
379                        log.error ".simple study wizard not all fields are filled in: " + sampleTemplateId
380                        flash.error = "No template was chosen. Please choose a template for the samples you provided."
381                        return false
382                }
383
384                def importedfile = fileService.get( filename )
385                def workbook
386                if (importedfile.exists()) {
387                        try {
388                                workbook = importerService.getWorkbook(new FileInputStream(importedfile))
389                        } catch (Exception e) {
390                                log.error ".simple study wizard could not load file: " + e
391                                flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
392                                return false
393                        }
394                } else {
395                        log.error ".simple study wizard no file given";
396                        flash.error = "No file was given. Please provide an excel file for entering samples.";
397                        return false;
398                }
399
400                if( !workbook ) {
401                        log.error ".simple study wizard could not load file into a workbook"
402                        flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
403                        return false
404                }
405
406                def selectedentities = []
407
408                if( !excelChecks( workbook, sheetIndex, headerRow, dataMatrixStart ) )
409                        return false;
410
411                // Get the header from the Excel file using the arguments given in the first step of the wizard
412                def importerHeader;
413                def importerDataMatrix;
414
415                try {
416                        importerHeader = importerService.getHeader(workbook,
417                                        sheetIndex - 1,                 // 0 == first sheet
418                                        headerRow,                              // 1 == first row :s
419                                        dataMatrixStart - 1,    // 0 == first row
420                                        Sample.class)
421
422                        importerDataMatrix = importerService.getDatamatrix(
423                                        workbook,
424                                        importerHeader,
425                                        sheetIndex - 1,                 // 0 == first sheet
426                                        dataMatrixStart - 1,    // 0 == first row
427                                        5)
428                } catch( Exception e ) {
429                        // An error occurred while reading the excel file.
430                        log.error ".simple study wizard error while reading the excel file";
431                        e.printStackTrace();
432
433                        // Show a message to the user
434                        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.";
435                        return false;
436                }
437
438                // Save read excel data into session
439                def dataMatrix = [];
440                def df = new DataFormatter();
441                importerDataMatrix.each {
442                        dataMatrix << it.collect{ it ? df.formatCellValue(it) : "" }
443                }
444
445                flow.excel = [
446                                        filename: filename,
447                                        sheetIndex: sheetIndex,
448                                        dataMatrixStart: dataMatrixStart,
449                                        headerRow: headerRow,
450                                        data: [
451                                                header: importerHeader,
452                                                dataMatrix: dataMatrix
453                                        ]
454                                ]
455
456                return true
457        }
458
459
460        /**
461         * Handles the matching of template fields with excel columns by the user
462         * @param study         Study to update
463         * @param params        Request parameter map
464         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error
465         *                                      The field session.simpleWizard.imported.numInvalidEntities reflects the number of
466         *                                      entities that have errors, and should be fixed before saving. The errors for those entities
467         *                                      are saved into session.simpleWizard.imported.errors
468         */
469        def handleColumns( study, params, flow ) {
470                // Find actual Template object from the chosen template name
471                def templates = [:];
472                flow.sampleForm.templateId.each {
473                        templates[ it.key ] = it.value ? Template.get( it.value ) : null;
474                }
475
476                def headers = flow.excel.data.header;
477
478                if( !params.matches ) {
479                        log.error( ".simple study wizard no column matches given" );
480                        flash.error = "No column matches given";
481                        return false;
482                }
483
484                // Retrieve the chosen matches from the request parameters and put them into
485                // the headers-structure, for later reference
486                params.matches.index.each { columnindex, value ->
487                        // Determine the entity and property by splitting it
488                        def parts = value.toString().tokenize( "||" );
489
490                        def property
491                        def entityName
492                        if( parts.size() > 1 ) {
493                                property = parts[ 1 ];
494                                entityName = "dbnp.studycapturing." + parts[ 0 ];
495                        } else if( parts.size() == 1 ) {
496                                property = parts[ 0 ];
497                                entityName = headers[columnindex.toInteger()].entityclass.getName();
498                        }
499
500                        // Create an actual class instance of the selected entity with the selected template
501                        // This should be inside the closure because in some cases in the advanced importer, the fields can have different target entities
502                        def entityClass = Class.forName( entityName, true, this.getClass().getClassLoader())
503                        def entityObj = entityClass.newInstance(template: templates[ entityName[entityName.lastIndexOf( '.' ) + 1..-1] ])
504
505                        headers[ columnindex.toInteger() ].entityclass = entityClass
506
507                        // Store the selected property for this column into the column map for the ImporterService
508                        headers[columnindex.toInteger()].property = property
509
510                        // Look up the template field type of the target TemplateField and store it also in the map
511                        headers[columnindex.toInteger()].templatefieldtype = entityObj.giveFieldType(property)
512
513                        // Is a "Don't import" property assigned to the column?
514                        headers[columnindex.toInteger()].dontimport = (property == "dontimport") ? true : false
515
516                        //if it's an identifier set the mapping column true or false
517                        entityClass.giveDomainFields().each {
518                                headers[columnindex.toInteger()].identifier = ( it.preferredIdentifier && (it.name == property) )
519                        }
520                }
521
522                // Import the workbook and store the table with entity records and store the failed cells
523                println "Importing samples for study " + study + " (" + study.id + ")";
524
525                def importedfile = fileService.get( flow.excel.filename )
526                def workbook
527                if (importedfile.exists()) {
528                        try {
529                                workbook = importerService.getWorkbook(new FileInputStream(importedfile))
530                        } catch (Exception e) {
531                                log.error ".simple study wizard could not load file: " + e
532                                flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
533                                return false
534                        }
535                } else {
536                        log.error ".simple study wizard no file given";
537                        flash.error = "No file was given. Please provide an excel file for entering samples.";
538                        return false;
539                }
540
541                if( !workbook ) {
542                        log.error ".simple study wizard could not load file into a workbook"
543                        flash.error = "The given file doesn't seem to be an excel file. Please provide an excel file for entering samples.";
544                        return false
545                }
546
547
548                def imported = importerService.importOrUpdateDataBySampleIdentifier(templates,
549                                workbook,
550                                flow.excel.sheetIndex - 1,
551                                flow.excel.dataMatrixStart - 1,
552                                flow.excel.data.header,
553                                flow.study,
554                                true                    // Also create entities for which no data is imported but where templates were chosen
555                );
556
557                def table = imported.table
558                def failedcells = imported.failedCells
559
560                flow.imported = [
561                        data: table,
562                        failedCells: failedcells
563                ];
564
565                // loop through all entities to validate them and add them to failedcells if an error occurs
566                def numInvalidEntities = 0;
567                def errors = [];
568
569                // Add all samples
570                table.each { record ->
571                        record.each { entity ->
572                                if( entity ) {
573                                        // Determine entity class and add a parent
574                                        def preferredIdentifier = importerService.givePreferredIdentifier( entity.class );
575                                        def equalClosure = { it.getFieldValue( preferredIdentifier.name ) == entity.getFieldValue( preferredIdentifier.name ) }
576
577                                        entity.parent = study
578
579                                        switch( entity.class ) {
580                                                case Sample:
581                                                        if( preferredIdentifier && !study.samples?.find( equalClosure ) ) {
582                                                                study.addToSamples( entity );
583                                                        }
584                                                        break;
585                                                case Subject:
586                                                        if( preferredIdentifier && !study.subjects?.find( equalClosure ) ) {
587                                                                study.addToSubjects( entity );
588                                                        }
589                                                        break;
590                                                case Event:
591                                                        if( preferredIdentifier && !study.events?.find( equalClosure ) ) {
592                                                                study.addToEvents( entity );
593                                                        }
594                                                        break;
595                                                case SamplingEvent:
596                                                        if( preferredIdentifier && !study.samplingEvents?.find( equalClosure ) ) {
597                                                                study.addToSamplingEvents( entity );
598                                                        }
599                                                        break;
600                                        }
601
602                                        if (!entity.validate()) {
603                                                numInvalidEntities++;
604
605                                                // Add this field to the list of failed cells, in order to give the user feedback
606                                                failedcells = addNonValidatingCells( failedcells, entity )
607
608                                                // Also create a full list of errors
609                                                errors += getHumanReadableErrors( entity );
610                                        }
611                                }
612                        }
613                }
614
615                flow.imported.numInvalidEntities = numInvalidEntities + failedcells?.size();
616                flow.imported.errors = errors;
617
618                return true
619        }
620
621
622
623        /**
624         * Handles the update of the edited fields by the user
625         * @param study         Study to update
626         * @param params                Request parameter map
627         * @return                      True if everything went OK, false otherwise. An error message is put in flash.error.
628         *                                      The field session.simpleWizard.imported.numInvalidEntities reflects the number of
629         *                                      entities that still have errors, and should be fixed before saving. The errors for those entities
630         *                                      are saved into session.simpleWizard.imported.errors
631         */
632        def handleMissingFields( study, params, flow ) {
633                def numInvalidEntities = 0;
634                def errors = [];
635
636                // Check which fields failed previously
637                def failedCells = flow.imported.failedCells
638
639                flow.imported.data.each { table ->
640                        table.each { entity ->
641                                def invalidFields = 0
642
643                                // Set the fields for this entity by retrieving values from the params
644                                entity.giveFields().each { field ->
645                                        def fieldName = importerService.getFieldNameInTableEditor( entity, field );
646
647                                        if( params[ fieldName ] == "#invalidterm" ) {
648                                                // If the value '#invalidterm' is chosen, the user hasn't fixed anything, so this field is still incorrect
649                                                invalidFields++;
650                                        } else {
651                                                if( field.type == org.dbnp.gdt.TemplateFieldType.ONTOLOGYTERM || field.type == org.dbnp.gdt.TemplateFieldType.STRINGLIST ) {
652                                                        // If this field is an ontologyterm field or a stringlist field, the value has changed, so remove the field from
653                                                        // the failedCells list
654                                                        importerService.removeFailedCell( failedCells, entity, field )
655                                                }
656
657                                                // Update the field, regardless of the type of field
658                                                entity.setFieldValue(field.name, params[ fieldName ] )
659                                        }
660                                }
661
662                                // Determine entity class and add a parent
663                                entity.parent = study;
664
665                                // Try to validate the entity now all fields have been set. If it fails, return an error
666                                if (!entity.validate() || invalidFields) {
667                                        numInvalidEntities++;
668
669                                        // Add this field to the list of failed cells, in order to give the user feedback
670                                        failedCells = addNonValidatingCells( failedCells, entity )
671
672                                        // Also create a full list of errors
673                                        errors += getHumanReadableErrors( entity );
674                                } else {
675                                        importerService.removeFailedCell( failedCells, entity )
676                                }
677                        } // end of record
678                } // end of table
679
680                flow.imported.numInvalidEntities = numInvalidEntities;
681                flow.imported.errors = errors;
682
683                return true
684        }
685
686    /**
687         * Adds all fields of this entity that have given an error when validating to the failedcells list
688         * @param failedcells   Current list of ImportRecords
689         * @param entity                Entity to check. The entity must have been validated before
690         * @return                              Updated list of ImportRecords
691         */
692        protected def addNonValidatingCells( failedcells, entity ) {
693                // Add this entity and the fields with an error to the failedCells list
694                ImportRecord failedRecord = new ImportRecord();
695
696                entity.getErrors().getFieldErrors().each { error ->
697                        String field = error.getField();
698
699                        def mc = importerService.findMappingColumn( session.simpleWizard.excel.data.header, field );
700                        def mcInstance = new MappingColumn( name: field, entityClass: Sample.class, index: -1, property: field.toLowerCase(), templateFieldType: entity.giveFieldType( field ) );
701
702                        // Create a clone of the mapping column
703                        if( mc ) {
704                                mcInstance.properties = mc.properties
705                        }
706
707                        failedRecord.addToImportcells( new ImportCell(mappingcolumn: mcInstance, value: error.getRejectedValue(), entityidentifier: importerService.getFieldNameInTableEditor( entity, field ) ) )
708                }
709                failedcells.add( failedRecord );
710
711                return failedcells
712        }
713
714        /**
715        * Handles assay input
716        * @param study          Study to update
717        * @param params         Request parameter map
718        * @return                       True if everything went OK, false otherwise. An error message is put in flash.error
719        */
720   def handleAssays( assay, params, flow ) {
721           // did the study template change?
722           if (params.get('template') && assay.template?.name != params.get('template')) {
723                   // set the template
724                   assay.template = Template.findByName(params.remove('template'))
725           }
726
727           // does the study have a template set?
728           if (assay.template && assay.template instanceof Template) {
729                   // yes, iterate through template fields
730                   assay.giveFields().each() {
731                           // and set their values
732                           assay.setFieldValue(it.name, params.get(it.escapedName()))
733                   }
734           }
735
736           // Save the assay in session
737           flow.assay = assay;
738
739           return true
740   }
741
742
743        /**
744         * Checks whether the given study is simple enough to be edited using this controller.
745         *
746         * The study is simple enough if the samples, subjects, events and samplingEvents can be
747         * edited as a flat table. That is:
748         *              - Every subject belongs to 0 or 1 eventgroup
749         *              - Every eventgroup belongs to 0 or 1 sample
750         *              - Every eventgroup has 0 or 1 subjects, 0 or 1 event and 0 or 1 samplingEvents
751         *              - If a sample belongs to an eventgroup:
752         *                      - If that eventgroup has a samplingEvent, that same samplingEvent must also be
753         *                              the sampling event that generated this sample
754         *                      - If that eventgroup has a subject, that same subject must also be the subject
755         *                              from whom the sample was taken
756         *
757         * @param study         Study to check
758         * @return                      True if the study can be edited by this controller, false otherwise
759         */
760        def checkStudySimplicity( study ) {
761                def simplicity = true;
762
763                if( !study )
764                        return false
765
766                if( study.eventGroups ) {
767                        study.eventGroups.each { eventGroup ->
768                                // Check for simplicity of eventgroups: only 0 or 1 subject, 0 or 1 event and 0 or 1 samplingEvent
769                                if( eventGroup.subjects?.size() > 1 || eventGroup.events?.size() > 1 || eventGroup.samplingEvents?.size() > 1 ) {
770                                        flash.message = "One or more eventgroups contain multiple subjects or events."
771                                        simplicity = false;
772                                }
773
774                                // Check whether this eventgroup only belongs to (max) 1 sample
775                                def numSamples = 0;
776                                study.samples.each { sample ->
777                                        // If no id is given for the eventGroup, it has been entered in this wizard, but
778                                        // not yet saved. In that case, it is always OK
779                                        if( eventGroup.id && sample.parentEventGroup?.id == eventGroup.id )
780                                                numSamples++;
781                                }
782
783                                if( numSamples > 1 ) {
784                                        flash.message = "One or more eventgroups belong to multiple samples."
785                                        simplicity = false;
786                                }
787                        }
788
789                        if( !simplicity ) return false;
790
791                        // Check whether subject only belong to zero or one event group
792                        if( study.subjects ) {
793                                study.subjects.each { subject ->
794                                        def numEventGroups = 0
795                                        study.eventGroups.each { eventGroup ->
796                                                // If no id is given for the subject, it has been entered in this wizard, but
797                                                // not yet saved. In that case, it is always OK
798                                                if( subject.id && eventGroup.subjects[0]?.id == subject.id )
799                                                        numEventGroups++
800                                        }
801
802                                        if( numEventGroups > 1 ) {
803                                                flash.message = "One or more subjects belong to multiple eventgroups."
804                                                simplicity = false;
805                                        }
806                                }
807                        }
808
809                        if( !simplicity ) return false;
810
811                        // Check whether the samples that belong to an eventgroup have the right parentObjects
812                        study.samples.each { sample ->
813                                if( sample.parentEventGroup ) {
814                                        // If no id is given for the subject, it has been entered in this wizard, but
815                                        // not yet saved. In that case, it is always OK
816                                        if( sample.parentSubject && sample.parentSubject.id) {
817                                                if( !sample.parentEventGroup.subjects || sample.parentEventGroup.subjects[0]?.id != sample.parentSubject.id ) {
818                                                        flash.message = "The structure of the eventgroups of one or more samples is too complex"
819                                                        simplicity = false;
820                                                }
821                                        }
822
823                                        // If no id is given for the sampling event, it has been entered in this wizard, but
824                                        // not yet saved. In that case, it is always OK
825                                        if( sample.parentEvent && sample.parentEvent.id) {
826                                                if( !sample.parentEventGroup.samplingEvents || sample.parentEventGroup.samplingEvents[0]?.id != sample.parentEvent.id ) {
827                                                        flash.message = "The structure of the eventgroups of one or more samples is too complex"
828                                                        simplicity = false;
829                                                }
830                                        }
831                                }
832                        }
833
834                        if( !simplicity ) return false;
835                }
836
837                return simplicity;
838        }
839
840        /**
841        * Checks an excel workbook whether the given sheetindex and rownumbers are correct
842        * @param workbook                       Excel workbook to read
843        * @param sheetIndex             1-based sheet index for the sheet to read (1=first sheet)
844        * @param headerRow                      1-based row number for the header row (1=first row)
845        * @param dataMatrixStart        1-based row number for the first data row (1=first row)
846        * @return                                       True if the sheet index and row numbers are correct.
847        */
848   protected boolean excelChecks( def workbook, int sheetIndex, int headerRow, int dataMatrixStart ) {
849           // Perform some basic checks on the excel file. These checks should be performed by the importerservice
850           // in a perfect scenario.
851           if( sheetIndex > workbook.getNumberOfSheets() ) {
852                   log.error ".simple study wizard Sheet index is too high: " + sheetIndex + " / " + workbook.getNumberOfSheets();
853                   flash.error = "Your excel sheet contains too few excel sheets. The provided excel sheet has only " + workbook.getNumberOfSheets() + " sheet(s).";
854                   return false
855           }
856
857           def sheet = workbook.getSheetAt(sheetIndex - 1);
858           def firstRowNum = sheet.getFirstRowNum();
859           def lastRowNum = sheet.getLastRowNum();
860           def numRows = lastRowNum - firstRowNum + 1;
861
862           if( headerRow > numRows  ) {
863                   log.error ".simple study wizard Header row number is incorrect: " + headerRow + " / " + numRows;
864                   flash.error = "Your excel sheet doesn't contain enough rows (" + numRows + "). Please provide an excel sheet with one header row and data below";
865                   return false
866           }
867
868           if( dataMatrixStart > numRows  ) {
869                   log.error ".simple study wizard Data row number is incorrect: " + dataMatrixStart + " / " + numRows;
870                   flash.error = "Your excel sheet doesn't contain enough rows (" + numRows + "). Please provide an excel sheet with one header row and data below";
871                   return false
872           }
873
874           return true;
875   }
876
877        /**
878         * Validates an object and puts human readable errors in validationErrors variable
879         * @param entity                Entity to validate
880         * @return                      True iff the entity validates, false otherwise
881         */
882        protected boolean validateObject( def entity ) {
883                if( !entity.validate() ) {
884                        flash.validationErrors = getHumanReadableErrors( entity )
885                        return false;
886                }
887                return true;
888        }
889
890        /**
891         * transform domain class validation errors into a human readable
892         * linked hash map
893         * @param object validated domain class
894         * @return object  linkedHashMap
895         */
896        def getHumanReadableErrors(object) {
897                def errors = [:]
898                object.errors.getAllErrors().each() { error ->
899                        // error.codes.each() { code -> println code }
900
901                        // generally speaking g.message(...) should work,
902                        // however it fails in some steps of the wizard
903                        // (add event, add assay, etc) so g is not always
904                        // availably. Using our own instance of the
905                        // validationTagLib instead so it is always
906                        // available to us
907                        errors[error.getArguments()[0]] = validationTagLib.message(error: error)
908                }
909
910                return errors
911        }
912}
Note: See TracBrowser for help on using the repository browser.