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

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