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

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