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

Revision 275, 19.6 KB (checked in by duh, 4 years ago)

- initial version of wizard storing data; known issue: while everything is saved properly, linked elements (subjects, events, etc) are not properly stored. I think it might be caused by the transaction in which everything is being done...
- added date/year select to datepickers
- added delete option to subjects view and controller logic

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