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

Last change on this file since 433 was 433, checked in by duh, 9 years ago
  • fixed issue with the event view and controller logic where

1) instances of Event were stored together in the same group
2) template groups being added when the event did not validate

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