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

Last change on this file since 389 was 389, checked in by duh, 9 years ago
  • reverted changes of revisions 386, 387 and 388 as they completely broke the repository (you need to get cake now! ;)
  • committed development version of modified study capture wizard (works until events)
  • Property svn:keywords set to Date Author Rev
File size: 20.5 KB
Line 
1package dbnp.studycapturing
2
3import dbnp.data.*
4
5/**
6 * Wizard Controler
7 *
8 * The wizard controller handles the handeling of pages and data flow
9 * through the study capturing wizard.
10 *
11 * TODO: refactor the 'handle*' methods to work as subflows instead
12 *               of methods outside of the flow
13 *
14 * @author Jeroen Wesbeek
15 * @since 20100107
16 * @package studycapturing
17 *
18 * Revision information:
19 * $Rev: 389 $
20 * $Author: duh $
21 * $Date: 2010-04-28 14:28:39 +0000 (wo, 28 apr 2010) $
22 */
23class WizardController {
24        /**
25         * index method, redirect to the webflow
26         * @void
27         */
28        def index = {
29                /**
30                 * Do you believe it in your head?
31                 * I can go with the flow
32                 * Don't say it doesn't matter (with the flow) matter anymore
33                 * I can go with the flow (I can go)
34                 * Do you believe it in your head?
35                 */
36                redirect(action: 'pages')
37        }
38
39        /**
40         * WebFlow definition
41         * @see http://grails.org/WebFlow
42         * @void
43         */
44        def pagesFlow = {
45                // start the flow
46                onStart {
47                        // define flow variables
48                        flow.page = 0
49                        flow.pages = [
50                                //[title: 'Templates'],                 // templates
51                                [title: 'Start'],                               // load or create a study
52                                [title: 'Study'],                               // study
53                                [title: 'Subjects'],                    // subjects
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").to "study"
79                        on("modify").to "modify"
80                }
81
82                // load a study to modify
83                modify {
84                        render(view: "_modify")
85                        onRender {
86                                flow.page = 1
87                                flash.cancel = true
88                        }
89                        on("cancel") {
90                                flow.study = null
91                        }.to "start"
92                        on("next") {
93                                // TODO: loading a study is not yet implemented
94                                //       create a error stating this feature is
95                                //       not yet implemented
96                                flash.errors = [:]
97                                this.appendErrorMap(
98                                        ['study': 'Loading a study and modifying it has not yet been implemented. Please press \'cancel\' to go back to the initial page...'],
99                                        flash.errors
100                                )
101                        }.to "modify"
102                }
103
104                // render and handle the study page
105                // TODO: make sure both template as well as logic will
106                //       handle Study templates as well!!!
107                study {
108                        render(view: "_study")
109                        onRender {
110                                flow.page = 2
111                        }
112                        on("refresh") {
113                                flash.values = params
114
115                                // handle study data
116                                this.handleStudy(flow, flash, params)
117
118                                // remove errors as we don't want any warnings now
119                                flash.errors = [:]                             
120                        }.to "study"
121                        on("switchTemplate") {
122                                flash.values = params
123
124                                // handle study data
125                                this.handleStudy(flow, flash, params)
126
127                                // remove errors as we don't want any warnings now
128                                flash.errors = [:]
129                        }.to "study"
130                        on("previous") {
131                                flash.errors = [:]
132
133                                // handle the study
134                                this.handleStudy(flow, flash, params)
135
136                                // reset errors
137                                flash.errors = [:]
138
139                                success()
140                        }.to "start"
141                        on("next") {
142                                flash.errors = [:]
143
144                                if (this.handleStudy(flow, flash, params)) {
145                                        success()
146                                } else {
147                                        error()
148                                }
149                        }.to "subjects"
150                }
151
152                // render and handle subjects page
153                subjects {
154                        render(view: "_subjects")
155                        onRender {
156                                flow.page = 3
157
158                                if (!flow.subjects) {
159                                        flow.subjects = []
160                                        flow.subjectTemplates = [:]
161                                }
162                        }
163                        on("refresh") {
164                                flash.values = params
165                        }.to "subjects"
166                        on("add") {
167                                flash.values = params
168                                def speciesTerm = Term.findByName(params.species);
169                                def subjectTemplateName = params.get('template');
170                                def subjectTemplate = Template.findByName(subjectTemplateName);
171
172                                // add this subject template to the subject template array
173                                if (!flow.subjectTemplates[ subjectTemplateName ]) {
174                                        flow.subjectTemplates[ subjectTemplateName ] = [
175                                                name: subjectTemplateName,
176                                                template: subjectTemplate,
177                                                subjects: []
178                                        ]
179                                }
180
181                                // add x subjects of species y
182                                (params.addNumber as int).times {
183                                        def increment = flow.subjects.size()
184                                        def subject = new Subject(
185                                                name: 'Subject ' + (increment + 1),
186                                                species: speciesTerm,
187                                                template: subjectTemplate
188                                        )
189
190                                        // instantiate a new Subject
191                                        flow.subjects[ increment ] = subject
192
193                                        // and remember the subject id with the template
194                                        def subjectsSize = flow.subjectTemplates[ subjectTemplateName ]['subjects'].size()
195                                        flow.subjectTemplates[ subjectTemplateName ]['subjects'][ subjectsSize ] = increment
196                                }
197                        }.to "subjects"
198                        on("next") {
199                                flash.errors = [:]
200
201                                // check if we have at least one subject
202                                // and check form data
203                                if (flow.subjects.size() < 1) {
204                                        // append error map
205                                        this.appendErrorMap(['subjects': 'You need at least to create one subject for your study'], flash.errors)
206                                        error()
207                                } else if (!this.handleSubjects(flow, flash, params)) {
208                                        error()
209                                } else {
210                                        success()
211                                }
212                        }.to "events"
213                        on("delete") {
214                                flash.errors = [:]
215                                def delete = params.get('do') as int;
216
217                                // remove subject
218                                if (flow.subjects[ delete ] && flow.subjects[ delete ] instanceof Subject) {
219                                        flow.subjectTemplates.each() { templateName, templateData ->
220                                                templateData.subjects.remove(delete)
221                                        }
222
223                                        flow.subjects.remove( delete )
224                                }
225                        }.to "subjects"
226                        on("previous") {
227                                flash.errors = [:]
228
229                                // handle form data
230                                if (!this.handleSubjects(flow, flash, params)) {
231                                        error()
232                                } else {
233                                        success()
234                                }
235                        }.to "study"
236                }
237
238                // render events page
239                events {
240                        render(view: "_events")
241                        onRender {
242                                flow.page = 4
243
244                                if (!flow.event) flow.event = new Event()
245                                if (!flow.events) flow.events = []
246                                if (!flow.eventGroups) {
247                                        flow.eventGroups = []
248                                        flow.eventGroups[0] = new EventGroup(name: 'Group 1')   // 1 group by default
249                                }
250                        }
251                        on("switchTemplate") {
252                                flash.values = params
253
254                                // handle study data
255                                this.handleEvents(flow, flash, params)
256
257                                // remove errors as we don't want any warnings now
258                                flash.errors = [:]
259                        }.to "events"
260                        on("add") {
261                                /*
262                                def startTime   = (params.get('startTime')) ? params.startTime = new Date().parse("d/M/yyyy HH:mm", params.get('startTime').toString()) : null
263                                def endTime             = (params.get('endTime')) ? new Date().parse("d/M/yyyy HH:mm", params.get('endTime').toString()) : null
264                                def template    = params.get('template')
265
266                                // handle template
267                                if (template instanceof String && template.size() > 0) {
268                                        template = Template.findByName(template)
269                                } else if (!template instanceof Template) {
270                                        template = null
271                                }
272
273                                // handle data
274                                if (template && startTime && endTime) {
275                                        // add an event instance
276                                        def event = new Event(
277                                                template        : template,
278                                                startTime       : startTime,
279                                                endTime         : endTime
280                                        )
281
282                                        // validate event
283                                        if (event.validate()) {
284                                                // add event to event list
285                                                flow.events[ flow.events.size() ] = event
286                                                success()
287                                        } else {
288                                                // validation failed, feedback errors
289                                                flash.errors = [:]
290                                                flash.values = params
291                                                this.appendErrors(event, flash.errors)
292                                                error()
293                                        }
294                                } else {
295                                        // validation failed, feedback errors
296                                        flash.errors    = [:]
297                                        flash.values    = params
298
299                                        if (!template)  this.appendErrorMap(['template': 'You need to select an event template'], flash.errors)
300                                        if (!startTime) this.appendErrorMap(['startTime': 'You need to define the start time of your study event'], flash.errors)
301                                        if (!endTime)   this.appendErrorMap(['endTime': 'You need to define the end time of your study event'], flash.errors)
302                                        error()
303                                }
304                                */
305                                flash.values = params
306
307                                // handle study data
308                                this.handleEvents(flow, flash, params)
309
310                                // validate event object
311                                if (flow.event.validate()) {
312                                        //flow.events[ flow.events.size() ] = flow.event
313                                        success()
314                                } else {
315                                        flash.errors = [:]
316                                        this.appendErrors(flow.event, flash.errors)
317                                        error()
318                                }
319                        }.to "events"
320                        on("deleteEvent") {
321                                flash.values = params
322                                def delete = params.get('do') as int;
323
324                                // handle event groupings
325                                this.handleEventGrouping(flow, flash, params)
326
327                                // remove event
328                                if (flow.events[ delete ] && flow.events[ delete ] instanceof Event) {
329                                        flow.events.remove(delete)
330                                }
331                        }.to "events"
332                        on("addEventGroup") {
333                                flash.values = params
334                               
335                                // handle event groupings
336                                this.handleEventGrouping(flow, flash, params)
337
338                                def increment = flow.eventGroups.size()
339                                def groupName = "Group " + (increment + 1)
340
341                                // check if group name exists
342                                def nameExists = true
343                                def u = 0
344
345                                // make sure a unique name is generated
346                                while (nameExists) {
347                                        u++
348                                        def count = 0
349                                       
350                                        flow.eventGroups.each() {
351                                                if (it.name == groupName) {
352                                                        groupName = "Group " + (increment + 1) + "," + u
353                                                } else {
354                                                        count++
355                                                }
356                                        }
357
358                                        nameExists = !(count == flow.eventGroups.size())
359                                }
360
361                                flow.eventGroups[increment] = new EventGroup(name: groupName)
362                        }.to "events"
363                        on("deleteEventGroup") {
364                                flash.values = params
365                               
366                                def delete = params.get('do') as int;
367
368                                // handle event groupings
369                                this.handleEventGrouping(flow, flash, params)
370
371                                // remove the group with this specific id
372                                if (flow.eventGroups[delete] && flow.eventGroups[delete] instanceof EventGroup) {
373                                        // remove this eventGroup
374                                        flow.eventGroups.remove(delete)
375                                }
376                        }.to "events"
377                        on("previous") {
378                                // handle event groupings
379                                this.handleEventGrouping(flow, flash, params)
380                        }.to "subjects"
381                        on("next") {
382                                flash.values = params
383                                flash.errors = [:]
384
385                                // handle event groupings
386                                this.handleEventGrouping(flow, flash, params)
387
388                                // check if we have at least one subject
389                                // and check form data
390                                if (flow.events.size() < 1) {
391                                        // append error map
392                                        flash.values = params
393                                        this.appendErrorMap(['events': 'You need at least to create one event for your study'], flash.errors)
394                                        error()
395                                }
396                        }.to "confirm"
397                }
398
399                confirm {
400                        render(view: "_confirmation")
401                        onRender {
402                                flow.page = 5
403                        }
404                        on("toStudy").to "study"
405                        on("toSubjects").to "subjects"
406                        on("toEvents").to "events"
407                        on("previous").to "events"
408                        on("next").to "save"
409                }
410
411                // store all study data
412                save {
413                        action {
414                                println "saving..."
415                                flash.errors = [:]
416
417                                // start transaction
418                                def transaction = sessionFactory.getCurrentSession().beginTransaction()
419
420                                // persist data to the database
421                                try {
422                                        // save EventDescriptions
423                                        flow.eventDescriptions.each() {
424                                                if (!it.save(flush:true)) {
425                                                        this.appendErrors(it, flash.errors)
426                                                        throw new Exception('error saving eventDescription')
427                                                }
428                                                println "saved eventdescription "+it
429                                        }
430
431                                        // TODO: eventDescriptions that are not linked to an event are currently
432                                        //               stored but end up in a black hole. We should either decide to
433                                        //               NOT store these eventDescriptions, or add "hasmany eventDescriptions"
434                                        //               to Study domain class
435
436                                        // save events
437                                        flow.events.each() {
438                                                if (!it.save(flush:true)) {
439                                                        this.appendErrors(it, flash.errors)
440                                                        throw new Exception('error saving event')
441                                                }
442                                                println "saved event "+it
443
444                                                // add to study
445                                                if (it instanceof SamplingEvent) {
446                                                        flow.study.addToSamplingEvents(it)
447                                                } else {
448                                                        flow.study.addToEvents(it)
449                                                }
450                                        }
451
452                                        // save eventGroups
453                                        flow.eventGroups.each() {
454                                                if (!it.save(flush:true)) {
455                                                        this.appendErrors(it, flash.errors)
456                                                        throw new Exception('error saving eventGroup')
457                                                }
458                                                println "saved eventGroup "+it
459
460                                                // add to study
461                                                flow.study.addToEventGroups(it)
462                                        }
463                                       
464                                        // save subjects
465                                        flow.subjects.each() {
466                                                if (!it.save(flush:true)) {
467                                                        this.appendErrors(it, flash.errors)
468                                                        throw new Exception('error saving subject')
469                                                }
470                                                println "saved subject "+it
471
472                                                // add this subject to the study
473                                                flow.study.addToSubjects(it)
474                                        }
475
476                                        // save study
477                                        if (!flow.study.save(flush:true)) {
478                                                this.appendErrors(flow.study, flash.errors)
479                                                throw new Exception('error saving study')
480                                        }
481                                        println "saved study "+flow.study+" (id: "+flow.study.id+")"
482
483                                        // commit transaction
484                                        println "commit"
485                                        transaction.commit()
486                                        success()
487                                } catch (Exception e) {
488                                        // rollback
489                                        this.appendErrorMap(['exception': e.toString() + ', see log for stacktrace' ], flash.errors)
490
491                                        // stacktrace in flash scope
492                                        flash.debug = e.getStackTrace()
493
494                                        println "rollback"
495                                        transaction.rollback()
496                                        error()
497                                }
498                        }
499                        on("error").to "error"
500                        on(Exception).to "error"
501                        on("success").to "done"
502                }
503
504                // error storing data
505                error {
506                        render(view: "_error")
507                        onRender {
508                                flow.page = 6
509                        }
510                        on("next").to "save"
511                        on("previous").to "events"
512                }
513
514                // render page three
515                done {
516                        render(view: "_done")
517                        onRender {
518                                flow.page = 7
519                        }
520                        on("previous") {
521                                // TODO
522                        }.to "confirm"
523                }
524        }
525
526
527        /**
528         * re-usable code for handling event form data in a web flow
529         * @param Map LocalAttributeMap (the flow scope)
530         * @param Map localAttributeMap (the flash scope)
531         * @param Map GrailsParameterMap (the flow parameters = form data)
532         * @returns boolean
533         */
534        def handleEvents(flow, flash, params) {
535                // got an event in the flash scope?
536                if (!flow.event) flow.event = new Event()
537
538                // if a template is selected, get template instance
539                def template = params.remove('template')
540                if (template instanceof String && template.size() > 0) {
541                        params.template = Template.findByName(template)
542                } else if (template instanceof Template) {
543                        params.template = template
544                } else {
545                        params.template = null
546                }
547
548                // set template
549                if (params.template) flow.event.template = params.template
550println flow.event.template
551println params
552               
553                // update event instance with parameters
554                params.each() { key, value ->
555                        try {
556                                flow.event.setFieldValue(key, value)
557                                println "has "+key
558                        } catch (Exception e) {
559                                println "does NOT have "+key
560                        }
561                        /*
562                        if (flow.event.hasProperty(key)) {
563println "has property " +  key
564                                flow.event.setProperty(key, value);
565                        }
566                        */
567                }
568        }
569
570        /**
571         * re-usable code for handling study form data in a web flow
572         * @param Map LocalAttributeMap (the flow scope)
573         * @param Map localAttributeMap (the flash scope)
574         * @param Map GrailsParameterMap (the flow parameters = form data)
575         * @returns boolean
576         */
577        def handleStudy(flow, flash, params) {
578                // create study instance if we have none
579                if (!flow.study) flow.study = new Study();
580
581                // create date instance from date string?
582                // @see WizardTagLibrary::dateElement{...}
583                if (params.get('startDate')) {
584                        params.startDate = new Date().parse("d/M/yyyy", params.get('startDate').toString())
585                } else {
586                        params.remove('startDate')
587                }
588
589                // if a template is selected, get template instance
590                def template = params.remove('template')
591                if (template instanceof String && template.size() > 0) {
592                        params.template = Template.findByName(template)
593                } else if (template instanceof Template) {
594                        params.template = template
595                }
596
597                // update study instance with parameters
598                params.each() { key, value ->
599                        if (flow.study.hasProperty(key)) {
600                                flow.study.setProperty(key, value);
601                        }
602                }
603
604                // walk through template fields
605                if (params.template) {
606                        params.template.fields.each() { field ->
607                                flow.study.setFieldValue(field.name, params.get(field.escapedName()))
608                        }
609                }
610
611                // validate study
612                if (flow.study.validate()) {
613                        return true
614                } else {
615                        // validation failed, feedback errors
616                        flash.errors = [:]
617                        this.appendErrors(flow.study, flash.errors)
618                        return false
619                }
620        }
621
622        /**
623         * re-usable code for handling eventDescription form data in a web flow
624         * @param Map LocalAttributeMap (the flow scope)
625         * @param Map localAttributeMap (the flash scope)
626         * @param Map GrailsParameterMap (the flow parameters = form data)
627         * @returns boolean
628         */
629        def handleEventDescriptions(flow, flash, params) {
630                def names = [:]
631                def errors = false
632                def id = 0
633
634                flow.eventDescriptions.each() {
635                        it.name = params.get('eventDescription_' + id + '_name')
636                        it.description = params.get('eventDescription_' + id + '_description')
637                        it.protocol = Protocol.findByName(params.get('eventDescription_' + id + '_protocol'))
638                        //it.classification = Term.findByName(params.get('eventDescription_' + id + '_classification'))
639                        it.isSamplingEvent = (params.containsKey('eventDescription_' + id + '_isSamplingEvent'))
640
641                        // validate eventDescription
642                        if (!it.validate()) {
643                                errors = true
644                                this.appendErrors(it, flash.errors, 'eventDescription_' + id + '_')
645                        }
646
647                        id++
648                }
649
650                return !errors
651        }
652
653        /**
654         * re-usable code for handling event grouping in a web flow
655         * @param Map LocalAttributeMap (the flow scope)
656         * @param Map localAttributeMap (the flash scope)
657         * @param Map GrailsParameterMap (the flow parameters = form data)
658         * @returns boolean
659         */
660        def handleEventGrouping(flow, flash, params) {
661                // walk through eventGroups
662                def g = 0
663                flow.eventGroups.each() {
664                        def e = 0
665                        def eventGroup = it
666
667                        // reset events
668                        eventGroup.events = new HashSet()
669
670                        // walk through events
671                        flow.events.each() {
672                                if (params.get('event_' + e + '_group_' + g) == 'on') {
673                                        eventGroup.addToEvents(it)
674                                }
675                                e++
676                        }
677                        g++
678                }
679        }
680
681        /**
682         * re-usable code for handling subject form data in a web flow
683         * @param Map LocalAttributeMap (the flow scope)
684         * @param Map localAttributeMap (the flash scope)
685         * @param Map GrailsParameterMap (the flow parameters = form data)
686         * @returns boolean
687         */
688        def handleSubjects(flow, flash, params) {
689                def names = [:];
690                def errors = false;
691                def id = 0;
692
693                // iterate through subject templates
694                flow.subjectTemplates.each() {
695                        def subjectTemplate = it.getValue().template
696                        def templateFields      = subjectTemplate.fields
697
698                        // iterate through subjects
699                        it.getValue().subjects.each() { subjectId ->
700                                flow.subjects[ subjectId ].name = params.get('subject_' + subjectId + '_name')
701                                flow.subjects[ subjectId ].species = Term.findByName(params.get('subject_' + subjectId + '_species'))
702
703                                // remember name and check for duplicates
704                                if (!names[ flow.subjects[ subjectId ].name ]) {
705                                        names[ flow.subjects[ subjectId ].name ] = [count: 1, first: 'subject_' + subjectId + '_name', firstId: subjectId]
706                                } else {
707                                        // duplicate name found, set error flag
708                                        names[ flow.subjects[ subjectId ].name ]['count']++
709
710                                        // second occurence?
711                                        if (names[ flow.subjects[ subjectId ].name ]['count'] == 2) {
712                                                // yeah, also mention the first
713                                                // occurrence in the error message
714                                                this.appendErrorMap(name: 'The subject name needs to be unique!', flash.errors, 'subject_' + names[ flow.subjects[ subjectId ].name ]['firstId'] + '_')
715                                        }
716
717                                        // add to error map
718                                        this.appendErrorMap([name: 'The subject name needs to be unique!'], flash.errors, 'subject_' + subjectId + '_')
719                                        errors = true
720                                }
721
722                                // iterate through template fields
723                                templateFields.each() { subjectField ->
724                                        flow.subjects[ subjectId ].setFieldValue(
725                                                subjectField.name,
726                                                params.get( 'subject_' + subjectId + '_' + subjectField.escapedName() )
727                                        )
728                                }
729
730                                // validate subject
731                                if (!flow.subjects[ subjectId ].validate()) {
732                                        errors = true
733                                        this.appendErrors(flow.subjects[ subjectId ], flash.errors, 'subject_' + subjectId + '_')
734                                }
735                        }
736                }
737
738                return !errors
739        }
740
741        /**
742         * return the object from a map of objects by searching for a name
743         * @param String name
744         * @param Map map of objects
745         * @return Object
746         */
747        def getObjectByName(name, map) {
748                def result = null
749                map.each() {
750                        if (it.name == name) {
751                                result = it
752                        }
753                }
754
755                return result
756        }
757
758        /**
759         * transform domain class validation errors into a human readable
760         * linked hash map
761         * @param object validated domain class
762         * @returns object  linkedHashMap
763         */
764        def getHumanReadableErrors(object) {
765                def errors = [:]
766                object.errors.getAllErrors().each() {
767                        errors[it.getArguments()[0]] = it.getDefaultMessage()
768                }
769
770                return errors
771        }
772
773        /**
774         * append errors of a particular object to a map
775         * @param object
776         * @param map linkedHashMap
777         * @void
778         */
779        def appendErrors(object, map) {
780                this.appendErrorMap(this.getHumanReadableErrors(object), map)
781        }
782
783        def appendErrors(object, map, prepend) {
784                this.appendErrorMap(this.getHumanReadableErrors(object), map, prepend)
785        }
786
787        /**
788         * append errors of one map to another map
789         * @param map linkedHashMap
790         * @param map linkedHashMap
791         * @void
792         */
793        def appendErrorMap(map, mapToExtend) {
794                map.each() {key, value ->
795                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
796                }
797        }
798
799        def appendErrorMap(map, mapToExtend, prepend) {
800                map.each() {key, value ->
801                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
802                }
803        }
804}
Note: See TracBrowser for help on using the repository browser.