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

Last change on this file since 296 was 296, checked in by duh, 9 years ago
  • refactored template field handeling in the wizard
  • added escapedName() method to TemplateField? which returns the field name in lowercase format, and all non-alphanumeric characters escaped to underscores (hence: jeroen_s_very_cool_field)
  • (re)moved some template 'note' boxes
  • Property svn:keywords set to Author Rev Date
File size: 20.5 KB
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: 296 $
22 * $Author: duh $
23 * $Date: 2010-03-22 12:44:00 +0000 (ma, 22 mrt 2010) $
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                                        // TODO: eventDescriptions that are not linked to an event are currently
464                                        //               stored but end up in a black hole. We should either decide to
465                                        //               NOT store these eventDescriptions, or add "hasmany eventDescriptions"
466                                        //               to Study domain class
467
468                                        // save events
469                                        flow.events.each() {
470                                                if (!it.save(flush:true)) {
471                                                        this.appendErrors(it, flash.errors)
472                                                        throw new Exception('error saving event')
473                                                }
474                                                println "saved event "+it
475
476                                                // add to study
477                                                if (it instanceof SamplingEvent) {
478                                                        flow.study.addToSamplingEvents(it)
479                                                } else {
480                                                        flow.study.addToEvents(it)
481                                                }
482                                        }
483
484                                        // save eventGroups
485                                        flow.eventGroups.each() {
486                                                if (!it.save(flush:true)) {
487                                                        this.appendErrors(it, flash.errors)
488                                                        throw new Exception('error saving eventGroup')
489                                                }
490                                                println "saved eventGroup "+it
491
492                                                // add to study
493                                                flow.study.addToEventGroups(it)
494                                        }
495                                       
496                                        // save subjects
497                                        flow.subjects.each() {
498                                                if (!it.save(flush:true)) {
499                                                        this.appendErrors(it, flash.errors)
500                                                        throw new Exception('error saving subject')
501                                                }
502                                                println "saved subject "+it
503
504                                                // add this subject to the study
505                                                flow.study.addToSubjects(it)
506                                        }
507
508                                        // save study
509                                        if (!flow.study.save(flush:true)) {
510                                                this.appendErrors(flow.study, flash.errors)
511                                                throw new Exception('error saving study')
512                                        }
513                                        println "saved study "+flow.study+" (id: "+flow.study.id+")"
514
515                                        // commit transaction
516                                        println "commit"
517                                        transaction.commit()
518                                        success()
519                                } catch (Exception e) {
520                                        // rollback
521                                        this.appendErrorMap(['exception': e.toString() + ', see log for stacktrace' ], flash.errors)
522
523                                        // stacktrace in flash scope
524                                        flash.debug = e.getStackTrace()
525
526                                        println "rollback"
527                                        transaction.rollback()
528                                        error()
529                                }
530                        }
531                        on("error").to "error"
532                        on(Exception).to "error"
533                        on("success").to "done"
534                }
535
536                // error storing data
537                error {
538                        render(view: "_error")
539                        onRender {
540                                flow.page = 6
541                        }
542                        on("next").to "save"
543                        on("previous").to "events"
544                }
545
546                // render page three
547                done {
548                        render(view: "_done")
549                        onRender {
550                                flow.page = 7
551                        }
552                        on("previous") {
553                                // TODO
554                        }.to "confirm"
555                }
556        }
557
558        /**
559         * re-usable code for handling study form data in a web flow
560         * @param Map LocalAttributeMap (the flow scope)
561         * @param Map localAttributeMap (the flash scope)
562         * @param Map GrailsParameterMap (the flow parameters = form data)
563         * @returns boolean
564         */
565        def handleStudy(flow, flash, params) {
566                // create study instance if we have none
567                if (!flow.study) flow.study = new Study();
568
569                // create date instance from date string?
570                // @see WizardTagLibrary::dateElement{...}
571                if (params.get('startDate')) {
572                        params.startDate = new Date().parse("d/M/yyyy", params.get('startDate').toString())
573                } else {
574                        params.remove('startDate')
575                }
576
577                // if a template is selected, get template instance
578                def template = params.remove('template')
579                if (template instanceof String && template.size() > 0) {
580                        params.template = Template.findByName(template)
581                } else if (template instanceof Template) {
582                        params.template = template
583                }
584
585                // update study instance with parameters
586                params.each() { key, value ->
587                        if (flow.study.hasProperty(key)) {
588                                flow.study.setProperty(key, value);
589                        }
590                }
591
592                // walk through template fields
593                params.template.fields.each() { field ->
594                        def value = params.get(field.escapedName())
595
596                        if (value) {
597                                flow.study.setFieldValue(field.name, value)
598                        }
599                }
600
601                // validate study
602                if (flow.study.validate()) {
603                        return true
604                } else {
605                        // validation failed, feedback errors
606                        flash.errors = [:]
607                        this.appendErrors(flow.study, flash.errors)
608                        return false
609                }
610        }
611
612        /**
613         * re-usable code for handling eventDescription form data in a web flow
614         * @param Map LocalAttributeMap (the flow scope)
615         * @param Map localAttributeMap (the flash scope)
616         * @param Map GrailsParameterMap (the flow parameters = form data)
617         * @returns boolean
618         */
619        def handleEventDescriptions(flow, flash, params) {
620                def names = [:]
621                def errors = false
622                def id = 0
623
624                flow.eventDescriptions.each() {
625                        it.name = params.get('eventDescription_' + id + '_name')
626                        it.description = params.get('eventDescription_' + id + '_description')
627                        it.protocol = Protocol.findByName(params.get('eventDescription_' + id + '_protocol'))
628                        //it.classification = Term.findByName(params.get('eventDescription_' + id + '_classification'))
629                        it.isSamplingEvent = (params.containsKey('eventDescription_' + id + '_isSamplingEvent'))
630
631                        // validate eventDescription
632                        if (!it.validate()) {
633                                errors = true
634                                this.appendErrors(it, flash.errors, 'eventDescription_' + id + '_')
635                        }
636
637                        id++
638                }
639
640                return !errors
641        }
642
643        /**
644         * re-usable code for handling event grouping in a web flow
645         * @param Map LocalAttributeMap (the flow scope)
646         * @param Map localAttributeMap (the flash scope)
647         * @param Map GrailsParameterMap (the flow parameters = form data)
648         * @returns boolean
649         */
650        def handleEventGrouping(flow, flash, params) {
651                // walk through eventGroups
652                def g = 0
653                flow.eventGroups.each() {
654                        def e = 0
655                        def eventGroup = it
656
657                        // reset events
658                        eventGroup.events = new HashSet()
659
660                        // walk through events
661                        flow.events.each() {
662                                if (params.get('event_' + e + '_group_' + g) == 'on') {
663                                        eventGroup.addToEvents(it)
664                                }
665                                e++
666                        }
667                        g++
668                }
669        }
670
671        /**
672         * re-usable code for handling subject form data in a web flow
673         * @param Map LocalAttributeMap (the flow scope)
674         * @param Map localAttributeMap (the flash scope)
675         * @param Map GrailsParameterMap (the flow parameters = form data)
676         * @returns boolean
677         */
678        def handleSubjects(flow, flash, params) {
679                def names = [:];
680                def errors = false;
681                def id = 0;
682
683                // iterate through subject templates
684                flow.subjectTemplates.each() {
685                        def subjectTemplate = it.getValue().template
686                        def templateFields      = subjectTemplate.fields
687
688                        // iterate through subjects
689                        it.getValue().subjects.each() { subjectId ->
690                                flow.subjects[ subjectId ].name = params.get('subject_' + subjectId + '_name')
691                                flow.subjects[ subjectId ].species = Term.findByName(params.get('subject_' + subjectId + '_species'))
692
693                                // remember name and check for duplicates
694                                if (!names[ flow.subjects[ subjectId ].name ]) {
695                                        names[ flow.subjects[ subjectId ].name ] = [count: 1, first: 'subject_' + subjectId + '_name', firstId: subjectId]
696                                } else {
697                                        // duplicate name found, set error flag
698                                        names[ flow.subjects[ subjectId ].name ]['count']++
699
700                                        // second occurence?
701                                        if (names[ flow.subjects[ subjectId ].name ]['count'] == 2) {
702                                                // yeah, also mention the first
703                                                // occurrence in the error message
704                                                this.appendErrorMap(name: 'The subject name needs to be unique!', flash.errors, 'subject_' + names[ flow.subjects[ subjectId ].name ]['firstId'] + '_')
705                                        }
706
707                                        // add to error map
708                                        this.appendErrorMap([name: 'The subject name needs to be unique!'], flash.errors, 'subject_' + subjectId + '_')
709                                        errors = true
710                                }
711
712                                // iterate through template fields
713                                templateFields.each() { subjectField ->
714                                        def value = params.get('subject_' + subjectId + '_' + subjectField.escapedName())
715
716                                        if (value) {
717                                                flow.subjects[ subjectId ].setFieldValue(subjectField.name, value)
718                                        }
719                                }
720
721                                // validate subject
722                                if (!flow.subjects[ subjectId ].validate()) {
723                                        errors = true
724                                        this.appendErrors(flow.subjects[ subjectId ], flash.errors)
725                                }
726                        }
727                }
728
729                return !errors
730        }
731
732        /**
733         * return the object from a map of objects by searching for a name
734         * @param String name
735         * @param Map map of objects
736         * @return Object
737         */
738        def getObjectByName(name, map) {
739                def result = null
740                map.each() {
741                        if (it.name == name) {
742                                result = it
743                        }
744                }
745
746                return result
747        }
748
749        /**
750         * transform domain class validation errors into a human readable
751         * linked hash map
752         * @param object validated domain class
753         * @returns object  linkedHashMap
754         */
755        def getHumanReadableErrors(object) {
756                def errors = [:]
757
758                object.errors.getAllErrors().each() {
759                        errors[it.getArguments()[0]] = it.getDefaultMessage()
760                }
761
762                return errors
763        }
764
765        /**
766         * append errors of a particular object to a map
767         * @param object
768         * @param map linkedHashMap
769         * @void
770         */
771        def appendErrors(object, map) {
772                this.appendErrorMap(this.getHumanReadableErrors(object), map)
773        }
774
775        def appendErrors(object, map, prepend) {
776                this.appendErrorMap(this.getHumanReadableErrors(object), map, prepend)
777        }
778
779        /**
780         * append errors of one map to another map
781         * @param map linkedHashMap
782         * @param map linkedHashMap
783         * @void
784         */
785        def appendErrorMap(map, mapToExtend) {
786                map.each() {key, value ->
787                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
788                }
789        }
790
791        def appendErrorMap(map, mapToExtend, prepend) {
792                map.each() {key, value ->
793                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
794                }
795        }
796}
Note: See TracBrowser for help on using the repository browser.