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

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