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

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