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

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