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

Last change on this file since 502 was 502, checked in by roberth, 11 years ago

RELTIME fields in the wizard now show the parsed text in blue when leaving the input field.

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