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

Last change on this file since 213 was 213, checked in by duh, 12 years ago
  • restructured wizard
  • added information boxes
  • improved error feedback (highlighted error fields)
  • added confirmation page
  • several smaller bugfixes and improvements
  • Property svn:keywords set to
    Date
    Author
    Rev
File size: 13.9 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: 213 $
19 * $Author: duh $
20 * $Date: 2010-02-25 15:18:22 +0000 (do, 25 feb 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: 'Study'],                               // study
51                                [title: 'Subjects'],                    // subjects
52                                [title: 'Event Descriptions'],  // event descriptions
53                                [title: 'Events'],                              // events
54                                [title: 'Confirmation'],        // confirmation page
55                                [title: 'Done']                                 // finish page
56                        ]
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 "templates"
70                }
71
72                // select the templates to use for this study
73                templates {
74                        render(view: "_templates")
75                        onRender {
76                                flow.page = 1
77                        }
78                        on("next") {
79                                // if we don't have a study, instantiate a study with dummy values
80                                if (!flow.study) {
81                                        flow.study = new Study(
82                                                title: "my study",
83                                                code: "",
84                                                ecCode: "",
85                                                researchQuestion: "",
86                                                description: "",
87                                                startDate: new Date()
88                                        )
89                                }
90
91                                // assign template to study
92                                flow.study.template = Template.findByName(params.get('template'));
93
94                                // validate study
95                                if (flow.study.validate()) {
96                                        success()
97                                } else {
98                                        // validation failed, feedback errors
99                                        flash.errors = new LinkedHashMap()
100                                        this.appendErrors(flow.study, flash.errors)
101                                        error()
102                                }
103                        }.to "study"
104                }
105
106                // render and handle the study page
107                study {
108                        render(view: "_study")
109                        onRender {
110                                flow.page = 2
111                        }
112                        on("previous") {
113                                flash.errors = new LinkedHashMap()
114
115                                if (this.handleStudy(flow, flash, params)) {
116                                        success()
117                                } else {
118                                        error()
119                                }
120                        }.to "templates"
121                        on("next") {
122                                flash.errors = new LinkedHashMap()
123
124                                if (this.handleStudy(flow, flash, params)) {
125                                        success()
126                                } else {
127                                        error()
128                                }
129                        }.to "subjects"
130                }
131
132                // render and handle subjects page
133                subjects {
134                        render(view: "_subjects")
135                        onRender {
136                                flow.page = 3
137
138                                if (!flow.subjects) {
139                                        flow.subjects = []
140                                }
141                        }
142                        on("add") {
143                                // fetch species by name (as posted by the form)
144                                def speciesTerm = Term.findByName(params.addSpecies)
145
146                                // add x subject of species y
147                                (params.addNumber as int).times {
148                                        def increment = flow.subjects.size()
149                                        flow.subjects[increment] = new Subject(
150                                                name: 'Subject ' + (increment + 1),
151                                                species: speciesTerm,
152                                                template: flow.study.template
153                                        )
154                                }
155                        }.to "subjects"
156                        on("next") {
157                                flash.errors = new LinkedHashMap()
158
159                                // check if we have at least one subject
160                                // and check form data
161                                if (flow.subjects.size() < 1) {
162                                        // append error map
163                                        this.appendErrorMap(['subjects': 'You need at least to create one subject for your study'], flash.errors)
164                                        error()
165                                } else if (!this.handleSubjects(flow, flash, params)) {
166                                        error()
167                                } else {
168                                        success()
169                                }
170                        }.to "eventDescriptions"
171                        on("previous") {
172                                flash.errors = new LinkedHashMap()
173
174                                // handle form data
175                                if (!this.handleSubjects(flow, flash, params)) {
176                                        error()
177                                } else {
178                                        success()
179                                }
180                        }.to "study"
181                }
182
183                // render page three
184                eventDescriptions {
185                        render(view: "_eventDescriptions")
186                        onRender {
187                                flow.page = 4
188
189                                if (!flow.eventDescriptions) {
190                                        flow.eventDescriptions = []
191                                }
192                        }
193                        on("add") {
194                                // fetch classification by name (as posted by the form)
195                                params.classification = Term.findByName(params.classification)
196
197                                // transform checkbox form value to boolean
198                                params.isSamplingEvent = (params.containsKey('isSamplingEvent'))
199
200                                // instantiate EventDescription with parameters
201                                def eventDescription = new EventDescription(params)
202
203                                // validate
204                                if (eventDescription.validate()) {
205                                        def increment = flow.eventDescriptions.size()
206                                        flow.eventDescriptions[increment] = eventDescription
207                                        success()
208                                } else {
209                                        // validation failed, feedback errors
210                                        flash.errors = new LinkedHashMap()
211                                        flash.values = params
212                                        this.appendErrors(eventDescription, flash.errors)
213                                        error()
214                                }
215                        }.to "eventDescriptions"
216                        on("previous") {
217                                flash.errors = new LinkedHashMap()
218
219                                // handle form data
220                                if (!this.handleEventDescriptions(flow, flash, params)) {
221                                        flash.values = params
222                                        error()
223                                } else {
224                                        success()
225                                }
226                        }.to "subjects"
227                        on("next") {
228                                flash.errors = new LinkedHashMap()
229
230                                // check if we have at least one subject
231                                // and check form data
232                                if (flow.eventDescriptions.size() < 1) {
233                                        // append error map
234                                        flash.values = params
235                                        this.appendErrorMap(['eventDescriptions': 'You need at least to create one eventDescription for your study'], flash.errors)
236                                        error()
237                                } else if (!this.handleEventDescriptions(flow, flash, params)) {
238                                        flash.values = params
239                                        error()
240                                } else {
241                                        success()
242                                }
243                        }.to "events"
244                }
245
246                // render events page
247                events {
248                        render(view: "_events")
249                        onRender {
250                                flow.page = 5
251
252                                if (!flow.events) {
253                                        flow.events = []
254                                }
255
256                                if (!flow.eventGroups) {
257                                        flow.eventGroups = []
258                                        flow.eventGroups[0] = new EventGroup(name: 'Group 1')   // 1 group by default
259                                }
260                        }
261                        on("add") {
262                                // create date instances from date string?
263                                // @see WizardTagLibrary::timeElement{...}
264                                if (params.get('startTime')) {
265                                        params.startTime = new Date().parse("d/M/yyyy HH:mm", params.get('startTime').toString())
266                                }
267                                if (params.get('endTime')) {
268                                        params.get('endTime').toString()
269                                        params.endTime = new Date().parse("d/M/yyyy HH:mm", params.get('endTime').toString())
270                                }
271
272                                // get eventDescription instance by name
273                                params.eventDescription = this.getObjectByName(params.get('eventDescription'),flow.eventDescriptions)
274
275                                // instantiate Event with parameters
276                                def event = new Event(params)
277
278                                // validate event
279                                if (event.validate()) {
280                                        def increment = flow.events.size()
281                                        flow.events[increment] = event
282                                        success()
283                                } else {
284                                        // validation failed, feedback errors
285                                        flash.errors = new LinkedHashMap()
286                                        flash.values = params
287                                        this.appendErrors(event, flash.errors)
288
289                                        flash.startTime                 = params.startTime
290                                        flash.endTime                   = params.endTime
291                                        flash.eventDescription  = params.eventDescription
292                                       
293                                        error()
294                                }
295                        }.to "events"
296                        on("addEventGroup") {
297                                def increment = flow.eventGroups.size()
298                                flow.eventGroups[ increment ] = new EventGroup(name: "Group "+(increment+1))
299                        }.to "events"
300                        on("previous") {
301                                // TODO
302                        }.to "eventDescriptions"
303                        on("next") {
304                                flash.errors = new LinkedHashMap()
305
306                                // check if we have at least one subject
307                                // and check form data
308                                if (flow.events.size() < 1) {
309                                        // append error map
310                                        flash.values = params
311                                        this.appendErrorMap(['events': 'You need at least to create one event for your study'], flash.errors)
312                                        error()
313                                }
314                        }.to "confirm"
315                }
316
317                confirm {
318                        render(view: "_confirmation")
319                        onRender {
320                                flow.page = 6
321                        }
322                        on("previous") {
323                                // do nothing
324                        }.to "events"
325                        on("next") {
326                                // store everything in the database!
327                                success()
328                        }.to "confirm"
329                }
330
331                // render page three
332                done {
333                        render(view: "_done")
334                        onRender {
335                                flow.page = 6
336                        }
337                        on("previous") {
338                                // TODO
339                        }.to "confirm"
340                }
341        }
342
343        /**
344         * re-usable code for handling study form data in a web flow
345         * @param Map LocalAttributeMap (the flow scope)
346         * @param Map localAttributeMap (the flash scope)
347         * @param Map GrailsParameterMap (the flow parameters = form data)
348         * @returns boolean
349         */
350        def handleStudy(flow, flash, params) {
351                // create study instance if we have none
352                if (!flow.study) flow.study = new Study();
353
354                // create date instance from date string?
355                // @see WizardTagLibrary::dateElement{...}
356                if (params.get('startDate')) {
357                        params.startDate = new Date().parse("d/M/yyyy", params.get('startDate').toString())
358                } else {
359                        params.remove('startDate')
360                }
361
362                // if a template is selected, get template instance
363                if (params.get('template')) {
364                        params.template = Template.findByName(params.get('template'))
365                }
366
367                // update study instance with parameters
368                params.each() {key, value ->
369                        if (flow.study.hasProperty(key)) {
370                                flow.study.setProperty(key, value);
371                        }
372                }
373
374                // validate study
375                if (flow.study.validate()) {
376                        return true
377                } else {
378                        // validation failed, feedback errors
379                        flash.errors = new LinkedHashMap()
380                        this.appendErrors(flow.study, flash.errors)
381                        return false
382                }
383        }
384
385        /**
386         * re-usable code for handling eventDescription form data in a web flow
387         * @param Map LocalAttributeMap (the flow scope)
388         * @param Map localAttributeMap (the flash scope)
389         * @param Map GrailsParameterMap (the flow parameters = form data)
390         * @returns boolean
391         */
392        def handleEventDescriptions(flow, flash, params) {
393                def names = new LinkedHashMap()
394                def errors = false
395                def id = 0
396
397                flow.eventDescriptions.each() {
398                        it.name                         = params.get('eventDescription_' + id + '_name')
399                        it.description          = params.get('eventDescription_' + id + '_description')
400                        it.classification       = Term.findByName(params.get('eventDescription_' + id + '_classification'))
401                        it.isSamplingEvent      = (params.containsKey('eventDescription_' + id + '_isSamplingEvent'))
402
403                        // validate eventDescription
404                        if (!it.validate()) {
405                                errors = true
406                                this.appendErrors(it, flash.errors, 'eventDescription_' + id + '_')
407                        }
408
409                        id++
410                }
411
412                return !(errors)
413        }
414
415        /**
416         * re-usable code for handling subject form data in a web flow
417         * @param Map LocalAttributeMap (the flow scope)
418         * @param Map localAttributeMap (the flash scope)
419         * @param Map GrailsParameterMap (the flow parameters = form data)
420         * @returns boolean
421         */
422        def handleSubjects(flow, flash, params) {
423                def names = new LinkedHashMap();
424                def errors = false;
425                def id = 0;
426
427                // iterate through subjects
428                flow.subjects.each() {
429                        // store subject properties
430                        def name = params.get('subject_' + id + '_name')
431                        it.name = params.get('subject_' + id + '_name')
432                        it.species = Term.findByName(params.get('subject_' + id + '_species'))
433
434                        // remember name and check for duplicates
435                        if (!names[it.name]) {
436                                names[it.name] = [count: 1, first: 'subject_' + id + '_name', firstId: id]
437                        } else {
438                                // duplicate name found, set error flag
439                                names[it.name]['count']++
440
441                                // second occurence?
442                                if (names[it.name]['count'] == 2) {
443                                        // yeah, also mention the first
444                                        // occurrence in the error message
445                                        this.appendErrorMap(name: 'The subject name needs to be unique!', flash.errors, 'subject_'+names[it.name]['firstId']+'_')
446                                }
447
448                                // add to error map
449                                this.appendErrorMap([name: 'The subject name needs to be unique!'], flash.errors, 'subject_'+id+'_')
450                                errors = true
451                        }
452
453                        // clear lists
454                        def stringList = new LinkedHashMap();
455                        def intList = new LinkedHashMap();
456                        def floatList = new LinkedHashMap();
457                        def termList = new LinkedHashMap();
458
459                        // get all template fields
460                        flow.study.template.subjectFields.each() {
461                                // valid type?
462                                if (!it.type) throw new NoSuchFieldException("Field name ${fieldName} not recognized")
463
464                                // get value
465                                def value = params.get('subject_' + id + '_' + it.name);
466                                if (value) {
467                                        // add to template parameters
468                                        switch (it.type) {
469                                                case 'STRINGLIST':
470                                                        stringList[it.name] = value
471                                                        break;
472                                                case 'INTEGER':
473                                                        intList[it.name] = value
474                                                        break;
475                                                case 'FLOAT':
476                                                        floatList[it.name] = value
477                                                        break;
478                                                default:
479                                                        // unsupported type?
480                                                        throw new NoSuchFieldException("Field type ${it.type} not recognized")
481                                                        break;
482                                        }
483                                }
484                        }
485
486                        // set field data
487                        it.templateStringFields = stringList
488                        it.templateIntegerFields = intList
489                        it.templateFloatFields = floatList
490                        it.templateTermFields = termList
491
492                        // validate subject
493                        if (!it.validate()) {
494                                errors = true
495                                this.appendErrors(it, flash.errors)
496                        }
497
498                        id++;
499                }
500
501                return !errors
502        }
503
504
505        /**
506         * return the object from a map of objects by searching for a name
507         * @param String        name
508         * @param Map           map of objects
509         * @return Object
510         */
511        def getObjectByName(name, map) {
512                def result = null
513                map.each() {
514                        if (it.name == name) {
515                                result = it
516                        }
517                }
518
519                return result
520        }
521
522        /**
523         * transform domain class validation errors into a human readable
524         * linked hash map
525         * @param object validated domain class
526         * @returns object  linkedHashMap
527         */
528        def getHumanReadableErrors(object) {
529                def errors = new LinkedHashMap()
530
531                object.errors.getAllErrors().each() {
532                        errors[it.getArguments()[0]] = it.getDefaultMessage()
533                }
534
535                return errors
536        }
537
538        /**
539         * append errors of a particular object to a map
540         * @param object
541         * @param map linkedHashMap
542         * @void
543         */
544        def appendErrors(object, map) {
545                this.appendErrorMap(this.getHumanReadableErrors(object), map)
546        }
547        def appendErrors(object, map, prepend) {
548                this.appendErrorMap(this.getHumanReadableErrors(object), map, prepend)
549        }
550
551        /**
552         * append errors of one map to another map
553         * @param map linkedHashMap
554         * @param map linkedHashMap
555         * @void
556         */
557        def appendErrorMap(map, mapToExtend) {
558                map.each() {key, value ->
559                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
560                }
561        }
562        def appendErrorMap(map, mapToExtend, prepend) {
563                map.each() {key, value ->
564                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
565                }
566        }
567}
Note: See TracBrowser for help on using the repository browser.