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

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