source: trunk/grails-app/controllers/dbnp/studycapturing/WizardController.groovy @ 332

Last change on this file since 332 was 332, checked in by duh, 10 years ago
  • introduced template fields validation
  • added i18n messaging for validation errors
  • added template field validation support to subject fields in wizard
  • TODO: an unsetTemplateField(...) method should be added to TemplateEntity? and used in the wizard. As setting an empty value (=empty string) should result in the particular value be unset in the TemplateEntity? instance... This issue results now in not being able to unset previously set template fields.
  • Property svn:keywords set to Author Rev Date
File size: 20.5 KB
Line 
1package dbnp.studycapturing
2
3import dbnp.data.*
4
5/**
6 * Wizard Controler
7 *
8 * The wizard controller handles the handeling of pages and data flow
9 * through the study capturing wizard.
10 *
11 * TODO: refactor the 'handle*' methods to work as subflows instead
12 *               of methods outside of the flow
13 *
14 * @author Jeroen Wesbeek
15 * @since 20100107
16 * @package studycapturing
17 *
18 * Revision information:
19 * $Rev: 332 $
20 * $Author: duh $
21 * $Date: 2010-04-08 16:02:16 +0000 (do, 08 apr 2010) $
22 */
23class WizardController {
24        /**
25         * index method, redirect to the webflow
26         * @void
27         */
28        def index = {
29                /**
30                 * Do you believe it in your head?
31                 * I can go with the flow
32                 * Don't say it doesn't matter (with the flow) matter anymore
33                 * I can go with the flow (I can go)
34                 * Do you believe it in your head?
35                 */
36                redirect(action: 'pages')
37        }
38
39        /**
40         * WebFlow definition
41         * @see http://grails.org/WebFlow
42         * @void
43         */
44        def pagesFlow = {
45                // start the flow
46                onStart {
47                        // define flow variables
48                        flow.page = 0
49                        flow.pages = [
50                                //[title: 'Templates'],                 // templates
51                                [title: 'Start'],                               // load or create a study
52                                [title: 'Study'],                               // study
53                                [title: 'Subjects'],                    // subjects
54                                [title: 'Event Descriptions'],  // event descriptions
55                                [title: 'Events'],                              // events and event grouping
56                                [title: 'Confirmation'],                // confirmation page
57                                [title: 'Done']                                 // finish page
58                        ]
59                }
60
61                // render the main wizard page which immediately
62                // triggers the 'next' action (hence, the main
63                // page dynamically renders the study template
64                // and makes the flow jump to the study logic)
65                mainPage {
66                        render(view: "/wizard/index")
67                        onRender {
68                                flow.page = 1
69                        }
70                        on("next").to "start"
71                }
72
73                // create or modify a study
74                start {
75                        render(view: "_start")
76                        onRender {
77                                flow.page = 1
78                        }
79                        on("next") {
80                                // NOTE: this action is called by an Ajax
81                                //       request rendered in the _start
82                                //       template. So for the end user the
83                                //       webflow actually starts in the
84                                //       study logic...
85                                //       This ajax call is required to make
86                                //       the ajax flow work correctly
87                        }.to "study"
88                }
89
90                // render and handle the study page
91                // TODO: make sure both template as well as logic will
92                //       handle Study templates as well!!!
93                study {
94                        render(view: "_study")
95                        onRender {
96                                flow.page = 2
97                        }
98                        on("switchTemplate") {
99                                // handle study data
100                                this.handleStudy(flow, flash, params)
101
102                                // remove errors as we don't want any warnings now
103                                flash.errors = [:]
104                        }.to "study"
105                        on("previous") {
106                                flash.errors = [:]
107
108                                if (this.handleStudy(flow, flash, params)) {
109                                        success()
110                                } else {
111                                        error()
112                                }
113                        }.to "start"
114                        on("next") {
115                                flash.errors = [:]
116
117                                if (this.handleStudy(flow, flash, params)) {
118                                        success()
119                                } else {
120                                        error()
121                                }
122                        }.to "subjects"
123                }
124
125                // render and handle subjects page
126                subjects {
127                        render(view: "_subjects")
128                        onRender {
129                                flow.page = 3
130
131                                if (!flow.subjects) {
132                                        flow.subjects = []
133                                        flow.subjectTemplates = [:]
134                                }
135                        }
136                        on("add") {
137                                // fetch species by name (as posted by the form)
138                                def speciesTerm = Term.findByName(params.addSpecies)
139                                def subjectTemplateName = params.get('template')
140                                def subjectTemplate     = Template.findByName(subjectTemplateName)
141
142                                // add this subject template to the subject template array
143                                if (!flow.subjectTemplates[ subjectTemplateName ]) {
144                                        flow.subjectTemplates[ subjectTemplateName ] = [
145                                                name: subjectTemplateName,
146                                                template: subjectTemplate,
147                                                subjects: []
148                                        ]
149                                }
150
151                                // add x subject of species y
152                                (params.addNumber as int).times {
153                                        def increment = flow.subjects.size()
154                                        def subject = new Subject(
155                                                name: 'Subject ' + (increment + 1),
156                                                species: speciesTerm,
157                                                template: subjectTemplate
158                                        )
159
160                                        // instantiate a new Subject
161                                        flow.subjects[ increment ] = subject
162
163                                        // and remember the subject id with the template
164                                        def subjectsSize = flow.subjectTemplates[ subjectTemplateName ]['subjects'].size()
165                                        flow.subjectTemplates[ subjectTemplateName ]['subjects'][ subjectsSize ] = increment
166                                }
167                        }.to "subjects"
168                        on("next") {
169                                flash.errors = [:]
170
171                                // check if we have at least one subject
172                                // and check form data
173                                if (flow.subjects.size() < 1) {
174                                        // append error map
175                                        this.appendErrorMap(['subjects': 'You need at least to create one subject for your study'], flash.errors)
176                                        error()
177                                } else if (!this.handleSubjects(flow, flash, params)) {
178                                        error()
179                                } else {
180                                        success()
181                                }
182                        }.to "eventDescriptions"
183                        on("delete") {
184                                flash.errors = [:]
185                                def delete = params.get('do') as int;
186
187                                // remove subject
188                                if (flow.subjects[ delete ] && flow.subjects[ delete ] instanceof Subject) {
189                                        flow.subjectTemplates.each() { templateName, templateData ->
190                                                templateData.subjects.remove(delete)
191                                        }
192
193                                        flow.subjects.remove( delete )
194                                }
195                        }.to "subjects"
196                        on("previous") {
197                                flash.errors = [:]
198
199                                // handle form data
200                                if (!this.handleSubjects(flow, flash, params)) {
201                                        error()
202                                } else {
203                                        success()
204                                }
205                        }.to "study"
206                }
207
208                // render page three
209                eventDescriptions {
210                        render(view: "_eventDescriptions")
211                        onRender {
212                                flow.page = 4
213
214                                if (!flow.eventDescriptions) {
215                                        flow.eventDescriptions = []
216                                }
217                        }
218                        on("add") {
219                                // fetch classification by name (as posted by the form)
220                                //params.classification = Term.findByName(params.classification)
221
222                                // fetch protocol by name (as posted by the form)
223                                params.protocol = Protocol.findByName(params.protocol)
224
225                                // transform checkbox form value to boolean
226                                params.isSamplingEvent = (params.containsKey('isSamplingEvent'))
227
228                                // instantiate EventDescription with parameters
229                                def eventDescription = new EventDescription(params)
230
231                                // validate
232                                if (eventDescription.validate()) {
233                                        def increment = flow.eventDescriptions.size()
234                                        flow.eventDescriptions[increment] = eventDescription
235                                        success()
236                                } else {
237                                        // validation failed, feedback errors
238                                        flash.errors = [:]
239                                        flash.values = params
240                                        this.appendErrors(eventDescription, flash.errors)
241                                        error()
242                                }
243                        }.to "eventDescriptions"
244                        on("delete") {
245                                def delete = params.get('do') as int;
246
247                                // handle form data
248                                if (!this.handleEventDescriptions(flow, flash, params)) {
249                                        flash.values = params
250                                        error()
251                                } else {
252                                        success()
253                                }
254
255                                // remove eventDescription
256                                if (flow.eventDescriptions[ delete ] && flow.eventDescriptions[ delete ] instanceof EventDescription) {
257                                        // remove all events based on this eventDescription
258                                        for ( i in flow.events.size()..0 ) {
259                                                if (flow.events[ i ] && flow.events[ i ].eventDescription == flow.eventDescriptions[ delete ]) {
260                                                        flow.events.remove(i)
261                                                }
262                                        }
263
264                                        flow.eventDescriptions.remove(delete)
265                                }
266                        }.to "eventDescriptions"
267                        on("previous") {
268                                flash.errors = [:]
269
270                                // handle form data
271                                if (!this.handleEventDescriptions(flow, flash, params)) {
272                                        flash.values = params
273                                        error()
274                                } else {
275                                        success()
276                                }
277                        }.to "subjects"
278                        on("next") {
279                                flash.errors = [:]
280
281                                // check if we have at least one subject
282                                // and check form data
283                                if (flow.eventDescriptions.size() < 1) {
284                                        // append error map
285                                        flash.values = params
286                                        this.appendErrorMap(['eventDescriptions': 'You need at least to create one eventDescription for your study'], flash.errors)
287                                        error()
288                                } else if (!this.handleEventDescriptions(flow, flash, params)) {
289                                        flash.values = params
290                                        error()
291                                } else {
292                                        success()
293                                }
294                        }.to "events"
295                }
296
297                // render events page
298                events {
299                        render(view: "_events")
300                        onRender {
301                                flow.page = 5
302
303                                if (!flow.events) {
304                                        flow.events = []
305                                }
306
307                                if (!flow.eventGroups) {
308                                        flow.eventGroups = []
309                                        flow.eventGroups[0] = new EventGroup(name: 'Group 1')   // 1 group by default
310                                }
311                        }
312                        on("add") {
313                                // create date instances from date string?
314                                // @see WizardTagLibrary::timeElement{...}
315                                if (params.get('startTime')) {
316                                        params.startTime = new Date().parse("d/M/yyyy HH:mm", params.get('startTime').toString())
317                                }
318                                if (params.get('endTime')) {
319                                        params.get('endTime').toString()
320                                        params.endTime = new Date().parse("d/M/yyyy HH:mm", params.get('endTime').toString())
321                                }
322
323                                // get eventDescription instance by name
324                                params.eventDescription = this.getObjectByName(params.get('eventDescription'), flow.eventDescriptions)
325
326                                // instantiate Event with parameters
327                                def event = (params.eventDescription.isSamplingEvent) ? new SamplingEvent(params) : new Event(params)
328
329                                // handle event groupings
330                                this.handleEventGrouping(flow, flash, params)
331
332                                // validate event
333                                if (event.validate()) {
334                                        def increment = flow.events.size()
335                                        flow.events[increment] = event
336                                        success()
337                                } else {
338                                        // validation failed, feedback errors
339                                        flash.errors = [:]
340                                        flash.values = params
341                                        this.appendErrors(event, flash.errors)
342
343                                        flash.startTime = params.startTime
344                                        flash.endTime = params.endTime
345                                        flash.eventDescription = params.eventDescription
346
347                                        error()
348                                }
349                        }.to "events"
350                        on("deleteEvent") {
351                                flash.values = params
352                                def delete = params.get('do') as int;
353
354                                // handle event groupings
355                                this.handleEventGrouping(flow, flash, params)
356
357                                // remove event
358                                if (flow.events[ delete ] && flow.events[ delete ] instanceof Event) {
359                                        flow.events.remove(delete)
360                                }
361                        }.to "events"
362                        on("addEventGroup") {
363                                flash.values = params
364                               
365                                // handle event groupings
366                                this.handleEventGrouping(flow, flash, params)
367
368                                def increment = flow.eventGroups.size()
369                                def groupName = "Group " + (increment + 1)
370
371                                // check if group name exists
372                                def nameExists = true
373                                def u = 0
374
375                                // make sure a unique name is generated
376                                while (nameExists) {
377                                        u++
378                                        def count = 0
379                                       
380                                        flow.eventGroups.each() {
381                                                if (it.name == groupName) {
382                                                        groupName = "Group " + (increment + 1) + "," + u
383                                                } else {
384                                                        count++
385                                                }
386                                        }
387
388                                        nameExists = !(count == flow.eventGroups.size())
389                                }
390
391                                flow.eventGroups[increment] = new EventGroup(name: groupName)
392                        }.to "events"
393                        on("deleteEventGroup") {
394                                flash.values = params
395                               
396                                def delete = params.get('do') as int;
397
398                                // handle event groupings
399                                this.handleEventGrouping(flow, flash, params)
400
401                                // remove the group with this specific id
402                                if (flow.eventGroups[delete] && flow.eventGroups[delete] instanceof EventGroup) {
403                                        // remove this eventGroup
404                                        flow.eventGroups.remove(delete)
405                                }
406                        }.to "events"
407                        on("previous") {
408                                // handle event groupings
409                                this.handleEventGrouping(flow, flash, params)
410                        }.to "eventDescriptions"
411                        on("next") {
412                                flash.values = params
413                                flash.errors = [:]
414
415                                // handle event groupings
416                                this.handleEventGrouping(flow, flash, params)
417
418                                // check if we have at least one subject
419                                // and check form data
420                                if (flow.events.size() < 1) {
421                                        // append error map
422                                        flash.values = params
423                                        this.appendErrorMap(['events': 'You need at least to create one event for your study'], flash.errors)
424                                        error()
425                                }
426                        }.to "confirm"
427                }
428
429                confirm {
430                        render(view: "_confirmation")
431                        onRender {
432                                flow.page = 6
433                        }
434                        on("toStudy").to "study"
435                        on("toSubjects").to "subjects"
436                        on("toEvents").to "events"
437                        on("previous").to "events"
438                        on("next").to "save"
439                }
440
441                // store all study data
442                save {
443                        action {
444                                println "saving..."
445                                flash.errors = [:]
446
447                                // start transaction
448                                def transaction = sessionFactory.getCurrentSession().beginTransaction()
449
450                                // persist data to the database
451                                try {
452                                        // save EventDescriptions
453                                        flow.eventDescriptions.each() {
454                                                if (!it.save(flush:true)) {
455                                                        this.appendErrors(it, flash.errors)
456                                                        throw new Exception('error saving eventDescription')
457                                                }
458                                                println "saved eventdescription "+it
459                                        }
460
461                                        // TODO: eventDescriptions that are not linked to an event are currently
462                                        //               stored but end up in a black hole. We should either decide to
463                                        //               NOT store these eventDescriptions, or add "hasmany eventDescriptions"
464                                        //               to Study domain class
465
466                                        // save events
467                                        flow.events.each() {
468                                                if (!it.save(flush:true)) {
469                                                        this.appendErrors(it, flash.errors)
470                                                        throw new Exception('error saving event')
471                                                }
472                                                println "saved event "+it
473
474                                                // add to study
475                                                if (it instanceof SamplingEvent) {
476                                                        flow.study.addToSamplingEvents(it)
477                                                } else {
478                                                        flow.study.addToEvents(it)
479                                                }
480                                        }
481
482                                        // save eventGroups
483                                        flow.eventGroups.each() {
484                                                if (!it.save(flush:true)) {
485                                                        this.appendErrors(it, flash.errors)
486                                                        throw new Exception('error saving eventGroup')
487                                                }
488                                                println "saved eventGroup "+it
489
490                                                // add to study
491                                                flow.study.addToEventGroups(it)
492                                        }
493                                       
494                                        // save subjects
495                                        flow.subjects.each() {
496                                                if (!it.save(flush:true)) {
497                                                        this.appendErrors(it, flash.errors)
498                                                        throw new Exception('error saving subject')
499                                                }
500                                                println "saved subject "+it
501
502                                                // add this subject to the study
503                                                flow.study.addToSubjects(it)
504                                        }
505
506                                        // save study
507                                        if (!flow.study.save(flush:true)) {
508                                                this.appendErrors(flow.study, flash.errors)
509                                                throw new Exception('error saving study')
510                                        }
511                                        println "saved study "+flow.study+" (id: "+flow.study.id+")"
512
513                                        // commit transaction
514                                        println "commit"
515                                        transaction.commit()
516                                        success()
517                                } catch (Exception e) {
518                                        // rollback
519                                        this.appendErrorMap(['exception': e.toString() + ', see log for stacktrace' ], flash.errors)
520
521                                        // stacktrace in flash scope
522                                        flash.debug = e.getStackTrace()
523
524                                        println "rollback"
525                                        transaction.rollback()
526                                        error()
527                                }
528                        }
529                        on("error").to "error"
530                        on(Exception).to "error"
531                        on("success").to "done"
532                }
533
534                // error storing data
535                error {
536                        render(view: "_error")
537                        onRender {
538                                flow.page = 6
539                        }
540                        on("next").to "save"
541                        on("previous").to "events"
542                }
543
544                // render page three
545                done {
546                        render(view: "_done")
547                        onRender {
548                                flow.page = 7
549                        }
550                        on("previous") {
551                                // TODO
552                        }.to "confirm"
553                }
554        }
555
556        /**
557         * re-usable code for handling study form data in a web flow
558         * @param Map LocalAttributeMap (the flow scope)
559         * @param Map localAttributeMap (the flash scope)
560         * @param Map GrailsParameterMap (the flow parameters = form data)
561         * @returns boolean
562         */
563        def handleStudy(flow, flash, params) {
564                // create study instance if we have none
565                if (!flow.study) flow.study = new Study();
566
567                // create date instance from date string?
568                // @see WizardTagLibrary::dateElement{...}
569                if (params.get('startDate')) {
570                        params.startDate = new Date().parse("d/M/yyyy", params.get('startDate').toString())
571                } else {
572                        params.remove('startDate')
573                }
574
575                // if a template is selected, get template instance
576                def template = params.remove('template')
577                if (template instanceof String && template.size() > 0) {
578                        params.template = Template.findByName(template)
579                } else if (template instanceof Template) {
580                        params.template = template
581                }
582
583                // update study instance with parameters
584                params.each() { key, value ->
585                        if (flow.study.hasProperty(key)) {
586                                flow.study.setProperty(key, value);
587                        }
588                }
589
590                // walk through template fields
591                if (params.template) {
592                        params.template.fields.each() { field ->
593                                def value = params.get(field.escapedName())
594
595                                if (value) {
596                                        flow.study.setFieldValue(field.name, value)
597                                }
598                        }
599                }
600
601                // validate study
602                if (flow.study.validate()) {
603                        return true
604                } else {
605                        // validation failed, feedback errors
606                        flash.errors = [:]
607                        this.appendErrors(flow.study, flash.errors)
608                        return false
609                }
610        }
611
612        /**
613         * re-usable code for handling eventDescription form data in a web flow
614         * @param Map LocalAttributeMap (the flow scope)
615         * @param Map localAttributeMap (the flash scope)
616         * @param Map GrailsParameterMap (the flow parameters = form data)
617         * @returns boolean
618         */
619        def handleEventDescriptions(flow, flash, params) {
620                def names = [:]
621                def errors = false
622                def id = 0
623
624                flow.eventDescriptions.each() {
625                        it.name = params.get('eventDescription_' + id + '_name')
626                        it.description = params.get('eventDescription_' + id + '_description')
627                        it.protocol = Protocol.findByName(params.get('eventDescription_' + id + '_protocol'))
628                        //it.classification = Term.findByName(params.get('eventDescription_' + id + '_classification'))
629                        it.isSamplingEvent = (params.containsKey('eventDescription_' + id + '_isSamplingEvent'))
630
631                        // validate eventDescription
632                        if (!it.validate()) {
633                                errors = true
634                                this.appendErrors(it, flash.errors, 'eventDescription_' + id + '_')
635                        }
636
637                        id++
638                }
639
640                return !errors
641        }
642
643        /**
644         * re-usable code for handling event grouping in a web flow
645         * @param Map LocalAttributeMap (the flow scope)
646         * @param Map localAttributeMap (the flash scope)
647         * @param Map GrailsParameterMap (the flow parameters = form data)
648         * @returns boolean
649         */
650        def handleEventGrouping(flow, flash, params) {
651                // walk through eventGroups
652                def g = 0
653                flow.eventGroups.each() {
654                        def e = 0
655                        def eventGroup = it
656
657                        // reset events
658                        eventGroup.events = new HashSet()
659
660                        // walk through events
661                        flow.events.each() {
662                                if (params.get('event_' + e + '_group_' + g) == 'on') {
663                                        eventGroup.addToEvents(it)
664                                }
665                                e++
666                        }
667                        g++
668                }
669        }
670
671        /**
672         * re-usable code for handling subject form data in a web flow
673         * @param Map LocalAttributeMap (the flow scope)
674         * @param Map localAttributeMap (the flash scope)
675         * @param Map GrailsParameterMap (the flow parameters = form data)
676         * @returns boolean
677         */
678        def handleSubjects(flow, flash, params) {
679                def names = [:];
680                def errors = false;
681                def id = 0;
682
683                // iterate through subject templates
684                flow.subjectTemplates.each() {
685                        def subjectTemplate = it.getValue().template
686                        def templateFields      = subjectTemplate.fields
687
688                        // iterate through subjects
689                        it.getValue().subjects.each() { subjectId ->
690                                flow.subjects[ subjectId ].name = params.get('subject_' + subjectId + '_name')
691                                flow.subjects[ subjectId ].species = Term.findByName(params.get('subject_' + subjectId + '_species'))
692
693                                // remember name and check for duplicates
694                                if (!names[ flow.subjects[ subjectId ].name ]) {
695                                        names[ flow.subjects[ subjectId ].name ] = [count: 1, first: 'subject_' + subjectId + '_name', firstId: subjectId]
696                                } else {
697                                        // duplicate name found, set error flag
698                                        names[ flow.subjects[ subjectId ].name ]['count']++
699
700                                        // second occurence?
701                                        if (names[ flow.subjects[ subjectId ].name ]['count'] == 2) {
702                                                // yeah, also mention the first
703                                                // occurrence in the error message
704                                                this.appendErrorMap(name: 'The subject name needs to be unique!', flash.errors, 'subject_' + names[ flow.subjects[ subjectId ].name ]['firstId'] + '_')
705                                        }
706
707                                        // add to error map
708                                        this.appendErrorMap([name: 'The subject name needs to be unique!'], flash.errors, 'subject_' + subjectId + '_')
709                                        errors = true
710                                }
711
712                                // iterate through template fields
713                                templateFields.each() { subjectField ->
714                                        def value = params.get('subject_' + subjectId + '_' + subjectField.escapedName())
715
716                                        if (value) {
717                                                flow.subjects[ subjectId ].setFieldValue(subjectField.name, value)
718                                        }
719                                }
720
721                                // validate subject
722                                if (!flow.subjects[ subjectId ].validate()) {
723                                        errors = true
724                                        this.appendErrors(flow.subjects[ subjectId ], flash.errors, 'subject_' + subjectId + '_')
725                                }
726                        }
727                }
728
729                return !errors
730        }
731
732        /**
733         * return the object from a map of objects by searching for a name
734         * @param String name
735         * @param Map map of objects
736         * @return Object
737         */
738        def getObjectByName(name, map) {
739                def result = null
740                map.each() {
741                        if (it.name == name) {
742                                result = it
743                        }
744                }
745
746                return result
747        }
748
749        /**
750         * transform domain class validation errors into a human readable
751         * linked hash map
752         * @param object validated domain class
753         * @returns object  linkedHashMap
754         */
755        def getHumanReadableErrors(object) {
756                def errors = [:]
757                object.errors.getAllErrors().each() {
758                        errors[it.getArguments()[0]] = it.getDefaultMessage()
759                }
760
761                return errors
762        }
763
764        /**
765         * append errors of a particular object to a map
766         * @param object
767         * @param map linkedHashMap
768         * @void
769         */
770        def appendErrors(object, map) {
771                this.appendErrorMap(this.getHumanReadableErrors(object), map)
772        }
773
774        def appendErrors(object, map, prepend) {
775                this.appendErrorMap(this.getHumanReadableErrors(object), map, prepend)
776        }
777
778        /**
779         * append errors of one map to another map
780         * @param map linkedHashMap
781         * @param map linkedHashMap
782         * @void
783         */
784        def appendErrorMap(map, mapToExtend) {
785                map.each() {key, value ->
786                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
787                }
788        }
789
790        def appendErrorMap(map, mapToExtend, prepend) {
791                map.each() {key, value ->
792                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
793                }
794        }
795}
Note: See TracBrowser for help on using the repository browser.