root/trunk/grails-app/controllers/dbnp/studycapturing/WizardController.groovy @ 281

Revision 281, 20.0 KB (checked in by duh, 4 years ago)

- added initial version of visitor feedback, including template, javascript, controller and more famfamfam icons
- extended a comment in the wizard controller to clarify the process

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