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

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