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

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