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

Last change on this file since 260 was 260, checked in by duh, 10 years ago
  • upgraded jquery-ui to latest version
  • implemented ontology chooser using jquery-ui's autocomplete functionality. The NCBO ontology chooser was not properly implemented to be used with other functionality like the table editor, etcetera. Hence the rewritten version. Example usage: <input type="text" name="..." rel="ontology-all-name" />
  • ontology chooser dynamically inserts three hidden fields if they are not present (concept_id, ontology_id and full_id) using the naming convention of inputname-ontology_id etc
  • updated the jquery-ui style to the latest version
  • added 'known issues' boxes to wizard pages
  • updated CSS to format ontology chooser results
  • updated wizard javascript
  • some changes to the wizard controller
  • Property svn:keywords set to Author Rev Date
File size: 17.0 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: 260 $
19 * $Author: duh $
20 * $Date: 2010-03-12 15:12:53 +0000 (vr, 12 mrt 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: 'Start'],                               // load or create a study
51                                [title: 'Study'],                               // study
52                                [title: 'Subjects'],                    // subjects
53                                [title: 'Event Descriptions'],  // event descriptions
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") {
79
80                        }.to "study"
81                }
82
83                // render and handle the study page
84                // TODO: make sure both template as well as logic will
85                //       handle Study templates as well!!!
86                study {
87                        render(view: "_study")
88                        onRender {
89                                flow.page = 2
90                        }
91                        on("switchTemplate") {
92                                // handle study data
93                                this.handleStudy(flow, flash, params)
94
95                                // remove errors as we don't want any warnings now
96                                flash.errors = [:]
97                        }.to "study"
98                        on("previous") {
99                                flash.errors = [:]
100
101                                if (this.handleStudy(flow, flash, params)) {
102                                        success()
103                                } else {
104                                        error()
105                                }
106                        }.to "start"
107                        on("next") {
108                                flash.errors = [:]
109
110                                if (this.handleStudy(flow, flash, params)) {
111                                        success()
112                                } else {
113                                        error()
114                                }
115                        }.to "subjects"
116                }
117
118                // render and handle subjects page
119                subjects {
120                        render(view: "_subjects")
121                        onRender {
122                                flow.page = 3
123
124                                if (!flow.subjects) {
125                                        flow.subjects = []
126                                        flow.subjectTemplates = [:]
127                                }
128                        }
129                        on("add") {
130                                // fetch species by name (as posted by the form)
131                                def speciesTerm = Term.findByName(params.addSpecies)
132                                def subjectTemplateName = params.get('template')
133                                def subjectTemplate     = Template.findByName(subjectTemplateName)
134
135                                // add this subject template to the subject template array
136                                if (!flow.subjectTemplates[ subjectTemplateName ]) {
137                                        flow.subjectTemplates[ subjectTemplateName ] = [
138                                                name: subjectTemplateName,
139                                                template: subjectTemplate,
140                                                subjects: []
141                                        ]
142                                }
143
144                                // add x subject of species y
145                                (params.addNumber as int).times {
146                                        def increment = flow.subjects.size()
147                                        def subject = new Subject(
148                                                name: 'Subject ' + (increment + 1),
149                                                species: speciesTerm,
150                                                template: subjectTemplate
151                                        )
152
153                                        // instantiate a new Subject
154                                        flow.subjects[ increment ] = subject
155
156                                        // and remember the subject id with the template
157                                        def subjectsSize = flow.subjectTemplates[ subjectTemplateName ]['subjects'].size()
158                                        flow.subjectTemplates[ subjectTemplateName ]['subjects'][ subjectsSize ] = increment
159                                }
160                        }.to "subjects"
161                        on("next") {
162                                println params
163                                flash.errors = [:]
164
165                                // check if we have at least one subject
166                                // and check form data
167                                if (flow.subjects.size() < 1) {
168                                        // append error map
169                                        this.appendErrorMap(['subjects': 'You need at least to create one subject for your study'], flash.errors)
170                                        error()
171                                } else if (!this.handleSubjects(flow, flash, params)) {
172                                        error()
173                                } else {
174                                        success()
175                                }
176                        }.to "eventDescriptions"
177                        on("previous") {
178                                flash.errors = [:]
179
180                                // handle form data
181                                if (!this.handleSubjects(flow, flash, params)) {
182                                        error()
183                                } else {
184                                        success()
185                                }
186                        }.to "study"
187                }
188
189                // render page three
190                eventDescriptions {
191                        render(view: "_eventDescriptions")
192                        onRender {
193                                flow.page = 4
194
195                                if (!flow.eventDescriptions) {
196                                        flow.eventDescriptions = []
197                                }
198                        }
199                        on("add") {
200                                // fetch classification by name (as posted by the form)
201                                //params.classification = Term.findByName(params.classification)
202
203                                // fetch protocol by name (as posted by the form)
204                                params.protocol = Protocol.findByName(params.protocol)
205
206                                // transform checkbox form value to boolean
207                                params.isSamplingEvent = (params.containsKey('isSamplingEvent'))
208
209                                // instantiate EventDescription with parameters
210                                def eventDescription = new EventDescription(params)
211
212                                // validate
213                                if (eventDescription.validate()) {
214                                        def increment = flow.eventDescriptions.size()
215                                        flow.eventDescriptions[increment] = eventDescription
216                                        success()
217                                } else {
218                                        // validation failed, feedback errors
219                                        flash.errors = [:]
220                                        flash.values = params
221                                        this.appendErrors(eventDescription, flash.errors)
222                                        error()
223                                }
224                        }.to "eventDescriptions"
225                        on("delete") {
226                                def delete = params.get('do') as int;
227
228                                // handle form data
229                                if (!this.handleEventDescriptions(flow, flash, params)) {
230                                        flash.values = params
231                                        error()
232                                } else {
233                                        success()
234                                }
235
236                                // remove eventDescription
237                                if (flow.eventDescriptions[ delete ] && flow.eventDescriptions[ delete ] instanceof EventDescription) {
238                                        // remove all events based on this eventDescription
239                                        for ( i in flow.events.size()..0 ) {
240                                                if (flow.events[ i ] && flow.events[ i ].eventDescription == flow.eventDescriptions[ delete ]) {
241                                                        flow.events.remove(i)
242                                                }
243                                        }
244
245                                        flow.eventDescriptions.remove(delete)
246                                }
247                        }.to "eventDescriptions"
248                        on("previous") {
249                                flash.errors = [:]
250
251                                // handle form data
252                                if (!this.handleEventDescriptions(flow, flash, params)) {
253                                        flash.values = params
254                                        error()
255                                } else {
256                                        success()
257                                }
258                        }.to "subjects"
259                        on("next") {
260                                flash.errors = [:]
261
262                                // check if we have at least one subject
263                                // and check form data
264                                if (flow.eventDescriptions.size() < 1) {
265                                        // append error map
266                                        flash.values = params
267                                        this.appendErrorMap(['eventDescriptions': 'You need at least to create one eventDescription for your study'], flash.errors)
268                                        error()
269                                } else if (!this.handleEventDescriptions(flow, flash, params)) {
270                                        flash.values = params
271                                        error()
272                                } else {
273                                        success()
274                                }
275                        }.to "events"
276                }
277
278                // render events page
279                events {
280                        render(view: "_events")
281                        onRender {
282                                flow.page = 5
283
284                                if (!flow.events) {
285                                        flow.events = []
286                                }
287
288                                if (!flow.eventGroups) {
289                                        flow.eventGroups = []
290                                        flow.eventGroups[0] = new EventGroup(name: 'Group 1')   // 1 group by default
291                                }
292                        }
293                        on("add") {
294                                // create date instances from date string?
295                                // @see WizardTagLibrary::timeElement{...}
296                                if (params.get('startTime')) {
297                                        params.startTime = new Date().parse("d/M/yyyy HH:mm", params.get('startTime').toString())
298                                }
299                                if (params.get('endTime')) {
300                                        params.get('endTime').toString()
301                                        params.endTime = new Date().parse("d/M/yyyy HH:mm", params.get('endTime').toString())
302                                }
303
304                                // get eventDescription instance by name
305                                params.eventDescription = this.getObjectByName(params.get('eventDescription'), flow.eventDescriptions)
306
307                                // instantiate Event with parameters
308                                def event = new Event(params)
309
310                                // handle event groupings
311                                this.handleEventGrouping(flow, flash, params)
312
313                                // validate event
314                                if (event.validate()) {
315                                        def increment = flow.events.size()
316                                        flow.events[increment] = event
317                                        success()
318                                } else {
319                                        // validation failed, feedback errors
320                                        flash.errors = [:]
321                                        flash.values = params
322                                        this.appendErrors(event, flash.errors)
323
324                                        flash.startTime = params.startTime
325                                        flash.endTime = params.endTime
326                                        flash.eventDescription = params.eventDescription
327
328                                        error()
329                                }
330                        }.to "events"
331                        on("deleteEvent") {
332                                flash.values = params
333                                def delete = params.get('do') as int;
334
335                                // handle event groupings
336                                this.handleEventGrouping(flow, flash, params)
337
338                                // remove event
339                                if (flow.events[ delete ] && flow.events[ delete ] instanceof Event) {
340                                        flow.events.remove(delete)
341                                }
342                        }.to "events"
343                        on("addEventGroup") {
344                                flash.values = params
345                               
346                                // handle event groupings
347                                this.handleEventGrouping(flow, flash, params)
348
349                                def increment = flow.eventGroups.size()
350                                def groupName = "Group " + (increment + 1)
351
352                                // check if group name exists
353                                def nameExists = true
354                                def u = 0
355
356                                // make sure a unique name is generated
357                                while (nameExists) {
358                                        u++
359                                        def count = 0
360                                       
361                                        flow.eventGroups.each() {
362                                                if (it.name == groupName) {
363                                                        groupName = "Group " + (increment + 1) + "," + u
364                                                } else {
365                                                        count++
366                                                }
367                                        }
368
369                                        nameExists = !(count == flow.eventGroups.size())
370                                }
371
372                                flow.eventGroups[increment] = new EventGroup(name: groupName)
373                        }.to "events"
374                        on("deleteEventGroup") {
375                                flash.values = params
376                               
377                                def delete = params.get('do') as int;
378
379                                // handle event groupings
380                                this.handleEventGrouping(flow, flash, params)
381
382                                // remove the group with this specific id
383                                if (flow.eventGroups[delete] && flow.eventGroups[delete] instanceof EventGroup) {
384                                        // remove this eventGroup
385                                        flow.eventGroups.remove(delete)
386                                }
387                        }.to "events"
388                        on("previous") {
389                                // handle event groupings
390                                this.handleEventGrouping(flow, flash, params)
391                        }.to "eventDescriptions"
392                        on("next") {
393                                flash.values = params
394                               
395                                flash.errors = [:]
396
397                                // handle event groupings
398                                this.handleEventGrouping(flow, flash, params)
399
400                                // check if we have at least one subject
401                                // and check form data
402                                if (flow.events.size() < 1) {
403                                        // append error map
404                                        flash.values = params
405                                        this.appendErrorMap(['events': 'You need at least to create one event for your study'], flash.errors)
406                                        error()
407                                }
408                        }.to "confirm"
409                }
410
411                confirm {
412                        render(view: "_confirmation")
413                        onRender {
414                                flow.page = 6
415                        }
416                        on("toStudy").to "study"
417                        on("toSubjects").to "subjects"
418                        on("toEvents").to "events"
419                        on("previous") {
420                                // do nothing
421                        }.to "events"
422                        on("next") {
423                                // store everything in the database!
424                                success()
425                        }.to "confirm"
426                }
427
428                // render page three
429                done {
430                        render(view: "_done")
431                        onRender {
432                                flow.page = 6
433                        }
434                        on("previous") {
435                                // TODO
436                        }.to "confirm"
437                }
438        }
439
440        /**
441         * re-usable code for handling study form data in a web flow
442         * @param Map LocalAttributeMap (the flow scope)
443         * @param Map localAttributeMap (the flash scope)
444         * @param Map GrailsParameterMap (the flow parameters = form data)
445         * @returns boolean
446         */
447        def handleStudy(flow, flash, params) {
448                // create study instance if we have none
449                if (!flow.study) flow.study = new Study();
450
451                // create date instance from date string?
452                // @see WizardTagLibrary::dateElement{...}
453                if (params.get('startDate')) {
454                        params.startDate = new Date().parse("d/M/yyyy", params.get('startDate').toString())
455                } else {
456                        params.remove('startDate')
457                }
458
459                // if a template is selected, get template instance
460                def template = params.remove('template')
461                if (template instanceof String && template.size() > 0) {
462                        params.template = Template.findByName(template)
463                } else if (template instanceof Template) {
464                        params.template = template
465                }
466
467                // update study instance with parameters
468                params.each() {key, value ->
469                        if (flow.study.hasProperty(key)) {
470                                flow.study.setProperty(key, value);
471                        }
472                }
473
474                // validate study
475                if (flow.study.validate()) {
476                        return true
477                } else {
478                        // validation failed, feedback errors
479                        flash.errors = [:]
480                        this.appendErrors(flow.study, flash.errors)
481                        return false
482                }
483        }
484
485        /**
486         * re-usable code for handling eventDescription form data in a web flow
487         * @param Map LocalAttributeMap (the flow scope)
488         * @param Map localAttributeMap (the flash scope)
489         * @param Map GrailsParameterMap (the flow parameters = form data)
490         * @returns boolean
491         */
492        def handleEventDescriptions(flow, flash, params) {
493                def names = [:]
494                def errors = false
495                def id = 0
496
497                flow.eventDescriptions.each() {
498                        it.name = params.get('eventDescription_' + id + '_name')
499                        it.description = params.get('eventDescription_' + id + '_description')
500                        it.protocol = Protocol.findByName(params.get('eventDescription_' + id + '_protocol'))
501                        //it.classification = Term.findByName(params.get('eventDescription_' + id + '_classification'))
502                        it.isSamplingEvent = (params.containsKey('eventDescription_' + id + '_isSamplingEvent'))
503
504                        // validate eventDescription
505                        if (!it.validate()) {
506                                errors = true
507                                this.appendErrors(it, flash.errors, 'eventDescription_' + id + '_')
508                        }
509
510                        id++
511                }
512
513                return !errors
514        }
515
516        /**
517         * re-usable code for handling event grouping in a web flow
518         * @param Map LocalAttributeMap (the flow scope)
519         * @param Map localAttributeMap (the flash scope)
520         * @param Map GrailsParameterMap (the flow parameters = form data)
521         * @returns boolean
522         */
523        def handleEventGrouping(flow, flash, params) {
524                // walk through eventGroups
525                def g = 0
526                flow.eventGroups.each() {
527                        def e = 0
528                        def eventGroup = it
529
530                        // reset events
531                        eventGroup.events = new HashSet()
532
533                        // walk through events
534                        flow.events.each() {
535                                if (params.get('event_' + e + '_group_' + g) == 'on') {
536                                        eventGroup.addToEvents(it)
537                                }
538                                e++
539                        }
540                        g++
541                }
542        }
543
544        /**
545         * re-usable code for handling subject form data in a web flow
546         * @param Map LocalAttributeMap (the flow scope)
547         * @param Map localAttributeMap (the flash scope)
548         * @param Map GrailsParameterMap (the flow parameters = form data)
549         * @returns boolean
550         */
551        def handleSubjects(flow, flash, params) {
552                def names = [:];
553                def errors = false;
554                def id = 0;
555
556                // iterate through subject templates
557                flow.subjectTemplates.each() {
558                        def subjectTemplate = it.getValue().template
559                        def templateFields      = subjectTemplate.fields
560
561                        // iterate through subjects
562                        it.getValue().subjects.each() { subjectId ->
563                                flow.subjects[ subjectId ].name = params.get('subject_' + subjectId + '_name')
564                                flow.subjects[ subjectId ].species = Term.findByName(params.get('subject_' + subjectId + '_species'))
565
566                                // remember name and check for duplicates
567                                if (!names[ flow.subjects[ subjectId ].name ]) {
568                                        names[ flow.subjects[ subjectId ].name ] = [count: 1, first: 'subject_' + subjectId + '_name', firstId: subjectId]
569                                } else {
570                                        // duplicate name found, set error flag
571                                        names[ flow.subjects[ subjectId ].name ]['count']++
572
573                                        // second occurence?
574                                        if (names[ flow.subjects[ subjectId ].name ]['count'] == 2) {
575                                                // yeah, also mention the first
576                                                // occurrence in the error message
577                                                this.appendErrorMap(name: 'The subject name needs to be unique!', flash.errors, 'subject_' + names[ flow.subjects[ subjectId ].name ]['firstId'] + '_')
578                                        }
579
580                                        // add to error map
581                                        this.appendErrorMap([name: 'The subject name needs to be unique!'], flash.errors, 'subject_' + subjectId + '_')
582                                        errors = true
583                                }
584
585                                // iterate through template fields
586                                templateFields.each() { subjectField ->
587                                        def value = params.get('subject_' + subjectId + '_' + subjectField.name)
588
589                                        if (value) {
590                                                flow.subjects[ subjectId ].setFieldValue(subjectField.name, value)
591                                        }
592                                }
593
594                                // validate subject
595                                if (!flow.subjects[ subjectId ].validate()) {
596                                        errors = true
597                                        this.appendErrors(flow.subjects[ subjectId ], flash.errors)
598                                }
599                        }
600                }
601
602                return !errors
603        }
604
605        /**
606         * return the object from a map of objects by searching for a name
607         * @param String name
608         * @param Map map of objects
609         * @return Object
610         */
611        def getObjectByName(name, map) {
612                def result = null
613                map.each() {
614                        if (it.name == name) {
615                                result = it
616                        }
617                }
618
619                return result
620        }
621
622        /**
623         * transform domain class validation errors into a human readable
624         * linked hash map
625         * @param object validated domain class
626         * @returns object  linkedHashMap
627         */
628        def getHumanReadableErrors(object) {
629                def errors = [:]
630
631                object.errors.getAllErrors().each() {
632                        errors[it.getArguments()[0]] = it.getDefaultMessage()
633                }
634
635                return errors
636        }
637
638        /**
639         * append errors of a particular object to a map
640         * @param object
641         * @param map linkedHashMap
642         * @void
643         */
644        def appendErrors(object, map) {
645                this.appendErrorMap(this.getHumanReadableErrors(object), map)
646        }
647
648        def appendErrors(object, map, prepend) {
649                this.appendErrorMap(this.getHumanReadableErrors(object), map, prepend)
650        }
651
652        /**
653         * append errors of one map to another map
654         * @param map linkedHashMap
655         * @param map linkedHashMap
656         * @void
657         */
658        def appendErrorMap(map, mapToExtend) {
659                map.each() {key, value ->
660                        mapToExtend[key] = ['key': key, 'value': value, 'dynamic': false]
661                }
662        }
663
664        def appendErrorMap(map, mapToExtend, prepend) {
665                map.each() {key, value ->
666                        mapToExtend[prepend + key] = ['key': key, 'value': value, 'dynamic': true]
667                }
668        }
669}
Note: See TracBrowser for help on using the repository browser.