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

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