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

Last change on this file since 216 was 216, checked in by duh, 10 years ago
  • event grouping data is now handled properly
  • added image support to ajaxButton which now renders to an "input type='image'" instead of the default "input type='button'"
  • changed some in grouping to famfamfam icons
  • evenGroup deletions
  • eventGroup names are now uniquely generated
  • improved CSS
  • improved javascript
  • Property svn:keywords set to
    Date
    Author
    Rev
File size: 15.6 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: 216 $
19 * $Author: duh $
20 * $Date: 2010-02-26 14:23:03 +0000 (vr, 26 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                                // handle event groupings
279                                this.handleEventGrouping(flow, flash, params)
280
281                                // validate event
282                                if (event.validate()) {
283                                        def increment = flow.events.size()
284                                        flow.events[increment] = event
285                                        success()
286                                } else {
287                                        // validation failed, feedback errors
288                                        flash.errors = new LinkedHashMap()
289                                        flash.values = params
290                                        this.appendErrors(event, flash.errors)
291
292                                        flash.startTime = params.startTime
293                                        flash.endTime = params.endTime
294                                        flash.eventDescription = params.eventDescription
295
296                                        error()
297                                }
298                        }.to "events"
299                        on("addEventGroup") {
300                                def increment = flow.eventGroups.size()
301                                def groupName = "Group " + (increment + 1)
302
303                                // check if group name exists
304                                def nameExists = true
305                                def u = 0
306
307                                // make sure a unique name is generated
308                                while (nameExists) {
309                                        u++
310                                        def count = 0
311                                       
312                                        flow.eventGroups.each() {
313                                                if (it.name == groupName) {
314                                                        groupName = "Group " + (increment + 1) + "," + u
315                                                } else {
316                                                        count++
317                                                }
318                                        }
319
320                                        nameExists = !(count == flow.eventGroups.size())
321                                }
322
323                                flow.eventGroups[increment] = new EventGroup(name: groupName)
324                        }.to "events"
325                        on("deleteEventGroup") {
326                                def delete = params.get('do') as int;
327
328                                // handle event groupings
329                                this.handleEventGrouping(flow, flash, params)
330
331                                // remove the group with this specific id
332                                if (flow.eventGroups[delete] && flow.eventGroups[delete] instanceof EventGroup) {
333                                        // remove this eventGroup
334                                        flow.eventGroups.remove(delete)
335                                }
336                        }.to "events"
337                        on("previous") {
338                                // handle event groupings
339                                this.handleEventGrouping(flow, flash, params)
340                        }.to "eventDescriptions"
341                        on("next") {
342                                flash.errors = new LinkedHashMap()
343
344                                // handle event groupings
345                                this.handleEventGrouping(flow, flash, params)
346
347                                // check if we have at least one subject
348                                // and check form data
349                                if (flow.events.size() < 1) {
350                                        // append error map
351                                        flash.values = params
352                                        this.appendErrorMap(['events': 'You need at least to create one event for your study'], flash.errors)
353                                        error()
354                                }
355                        }.to "events"
356                }
357
358                confirm {
359                        render(view: "_confirmation")
360                        onRender {
361                                flow.page = 6
362                        }
363                        on("previous") {
364                                // do nothing
365                        }.to "events"
366                        on("next") {
367                                // store everything in the database!
368                                success()
369                        }.to "confirm"
370                }
371
372                // render page three
373                done {
374                        render(view: "_done")
375                        onRender {
376                                flow.page = 6
377                        }
378                        on("previous") {
379                                // TODO
380                        }.to "confirm"
381                }
382        }
383
384        /**
385         * re-usable code for handling event grouping in a web flow
386         * @param Map LocalAttributeMap (the flow scope)
387         * @param Map localAttributeMap (the flash scope)
388         * @param Map GrailsParameterMap (the flow parameters = form data)
389         * @returns boolean
390         */
391        def handleEventGrouping(flow, flash, params) {
392                // walk through eventGroups
393                def g = 0
394                flow.eventGroups.each() {
395                        def e = 0
396                        def eventGroup = it
397
398                        // reset events
399                        eventGroup.events = new HashSet()
400
401                        // walk through events
402                        flow.events.each() {
403                                if (params.get('event_' + e + '_group_' + g) == 'on') {
404                                        eventGroup.addToEvents(it)
405                                        //} else {
406                                        //      eventGroup.events.minus(it)
407                                }
408                                e++
409                        }
410                        g++
411                }
412        }
413
414        /**
415         * re-usable code for handling study form data in a web flow
416         * @param Map LocalAttributeMap (the flow scope)
417         * @param Map localAttributeMap (the flash scope)
418         * @param Map GrailsParameterMap (the flow parameters = form data)
419         * @returns boolean
420         */
421        def handleStudy(flow, flash, params) {
422                // create study instance if we have none
423                if (!flow.study) flow.study = new Study();
424
425                // create date instance from date string?
426                // @see WizardTagLibrary::dateElement{...}
427                if (params.get('startDate')) {
428                        params.startDate = new Date().parse("d/M/yyyy", params.get('startDate').toString())
429                } else {
430                        params.remove('startDate')
431                }
432
433                // if a template is selected, get template instance
434                if (params.get('template')) {
435                        params.template = Template.findByName(params.get('template'))
436                }
437
438                // update study instance with parameters
439                params.each() {key, value ->
440                        if (flow.study.hasProperty(key)) {
441                                flow.study.setProperty(key, value);
442                        }
443                }
444
445                // validate study
446                if (flow.study.validate()) {
447                        return true
448                } else {
449                        // validation failed, feedback errors
450                        flash.errors = new LinkedHashMap()
451                        this.appendErrors(flow.study, flash.errors)
452                        return false
453                }
454        }
455
456        /**
457         * re-usable code for handling eventDescription form data in a web flow
458         * @param Map LocalAttributeMap (the flow scope)
459         * @param Map localAttributeMap (the flash scope)
460         * @param Map GrailsParameterMap (the flow parameters = form data)
461         * @returns boolean
462         */
463        def handleEventDescriptions(flow, flash, params) {
464                def names = new LinkedHashMap()
465                def errors = false
466                def id = 0
467
468                flow.eventDescriptions.each() {
469                        it.name = params.get('eventDescription_' + id + '_name')
470                        it.description = params.get('eventDescription_' + id + '_description')
471                        it.classification = Term.findByName(params.get('eventDescription_' + id + '_classification'))
472                        it.isSamplingEvent = (params.containsKey('eventDescription_' + id + '_isSamplingEvent'))
473
474                        // validate eventDescription
475                        if (!it.validate()) {
476                                errors = true
477                                this.appendErrors(it, flash.errors, 'eventDescription_' + id + '_')
478                        }
479
480                        id++
481                }
482
483                return !(errors)
484        }
485
486        /**
487         * re-usable code for handling subject form data in a web flow
488         * @param Map LocalAttributeMap (the flow scope)
489         * @param Map localAttributeMap (the flash scope)
490         * @param Map GrailsParameterMap (the flow parameters = form data)
491         * @returns boolean
492         */
493        def handleSubjects(flow, flash, params) {
494                def names = new LinkedHashMap();
495                def errors = false;
496                def id = 0;
497
498                // iterate through subjects
499                flow.subjects.each() {
500                        // store subject properties
501                        def name = params.get('subject_' + id + '_name')
502                        it.name = params.get('subject_' + id + '_name')
503                        it.species = Term.findByName(params.get('subject_' + id + '_species'))
504
505                        // remember name and check for duplicates
506                        if (!names[it.name]) {
507                                names[it.name] = [count: 1, first: 'subject_' + id + '_name', firstId: id]
508                        } else {
509                                // duplicate name found, set error flag
510                                names[it.name]['count']++
511
512                                // second occurence?
513                                if (names[it.name]['count'] == 2) {
514                                        // yeah, also mention the first
515                                        // occurrence in the error message
516                                        this.appendErrorMap(name: 'The subject name needs to be unique!', flash.errors, 'subject_' + names[it.name]['firstId'] + '_')
517                                }
518
519                                // add to error map
520                                this.appendErrorMap([name: 'The subject name needs to be unique!'], flash.errors, 'subject_' + id + '_')
521                                errors = true
522                        }
523
524                        // clear lists
525                        def stringList = new LinkedHashMap();
526                        def intList = new LinkedHashMap();
527                        def floatList = new LinkedHashMap();
528                        def termList = new LinkedHashMap();
529
530                        // get all template fields
531                        flow.study.template.subjectFields.each() {
532                                // valid type?
533                                if (!it.type) throw new NoSuchFieldException("Field name ${fieldName} not recognized")
534
535                                // get value
536                                def value = params.get('subject_' + id + '_' + it.name);
537                                if (value) {
538                                        // add to template parameters
539                                        switch (it.type) {
540                                                case 'STRINGLIST':
541                                                        stringList[it.name] = value
542                                                        break;
543                                                case 'INTEGER':
544                                                        intList[it.name] = value
545                                                        break;
546                                                case 'FLOAT':
547                                                        floatList[it.name] = value
548                                                        break;
549                                                default:
550                                                        // unsupported type?
551                                                        throw new NoSuchFieldException("Field type ${it.type} not recognized")
552                                                        break;
553                                        }
554                                }
555                        }
556
557                        // set field data
558                        it.templateStringFields = stringList
559                        it.templateIntegerFields = intList
560                        it.templateFloatFields = floatList
561                        it.templateTermFields = termList
562
563                        // validate subject
564                        if (!it.validate()) {
565                                errors = true
566                                this.appendErrors(it, flash.errors)
567                        }
568
569                        id++;
570                }
571
572                return !errors
573        }
574
575        /**
576         * return the object from a map of objects by searching for a name
577         * @param String name
578         * @param Map map of objects
579         * @return Object
580         */
581        def getObjectByName(name, map) {
582                def result = null
583                map.each() {
584                        if (it.name == name) {
585                                result = it
586                        }
587                }
588
589                return result
590        }
591
592        /**
593         * transform domain class validation errors into a human readable
594         * linked hash map
595         * @param object validated domain class
596         * @returns object  linkedHashMap
597         */
598        def getHumanReadableErrors(object) {
599                def errors = new LinkedHashMap()
600
601                object.errors.getAllErrors().each() {
602                        errors[it.getArguments()[0]] = it.getDefaultMessage()
603                }
604
605                return errors
606        }
607
608        /**
609         * append errors of a particular object to a map
610         * @param object
611         * @param map linkedHashMap
612         * @void
613         */
614        def appendErrors(object, map) {
615                this.appendErrorMap(this.getHumanReadableErrors(object), map)
616        }
617
618        def appendErrors(object, map, prepend) {
619                this.appendErrorMap(this.getHumanReadableErrors(object), map, prepend)
620        }
621
622        /**
623         * append errors of one map to another map
624         * @param map linkedHashMap
625         * @param map linkedHashMap
626         * @void
627         */
628        def appendErrorMap(map, mapToExtend) {
629                map.each() {key, value ->
630                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
631                }
632        }
633
634        def appendErrorMap(map, mapToExtend, prepend) {
635                map.each() {key, value ->
636                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
637                }
638        }
639}
Note: See TracBrowser for help on using the repository browser.