source: main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py @ 6306

Last change on this file since 6306 was 6305, checked in by Henrik Bettermann, 14 years ago

Fill message attribute with workflow messages and display on forms.

File size: 21.2 KB
RevLine 
[5273]1##
2## browser.py
3## Login : <uli@pu.smp.net>
[6153]4## Started on  Sun Jun 27 11:03:10 2010 Uli Fouquet & Henrik Bettermann
[5273]5## $Id$
[6078]6##
[6063]7## Copyright (C) 2010 Uli Fouquet & Henrik Bettermann
[5273]8## This program is free software; you can redistribute it and/or modify
9## it under the terms of the GNU General Public License as published by
10## the Free Software Foundation; either version 2 of the License, or
11## (at your option) any later version.
[6078]12##
[5273]13## This program is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16## GNU General Public License for more details.
[6078]17##
[5273]18## You should have received a copy of the GNU General Public License
19## along with this program; if not, write to the Free Software
20## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21##
[5824]22"""UI components for basic applicants and related components.
[5273]23"""
[6082]24import sys
[5273]25import grok
26
[6305]27from datetime import datetime
[6082]28from zope.component import getUtility
[6081]29from zope.formlib.widget import CustomWidgetFactory
[6082]30from zope.interface import Invalid
[5937]31from zope.securitypolicy.interfaces import IPrincipalRoleManager
[6153]32from zope.traversing.browser import absoluteURL
[6081]33
[6303]34from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
35
[5273]36from waeup.sirp.browser import (
37    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage,
38    WAeUPDisplayFormPage, NullValidator)
[6081]39from waeup.sirp.browser.breadcrumbs import Breadcrumb
[6103]40from waeup.sirp.browser.layout import NullValidator
[6013]41from waeup.sirp.browser.resources import datepicker, tabs, datatable
[6153]42from waeup.sirp.browser.viewlets import (
43    ManageActionButton, PrimaryNavTab, LeftSidebarLink
44    )
[6081]45from waeup.sirp.image.browser.widget import (
46    ThumbnailWidget, EncodingImageFileWidget,
[5822]47    )
[6184]48from waeup.sirp.interfaces import IWAeUPObject, ILocalRolesAssignable
[6254]49from waeup.sirp.university.interfaces import ICertificate
[6054]50from waeup.sirp.widgets.datewidget import (
51    FriendlyDateWidget, FriendlyDateDisplayWidget)
[6084]52from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
[5303]53from waeup.sirp.widgets.objectwidget import (
[5301]54    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
[5303]55from waeup.sirp.widgets.multilistwidget import (
[5273]56    MultiListWidget, MultiListDisplayWidget)
[6081]57
[6153]58from waeup.sirp.applicants import ResultEntry, Applicant, get_applicant_data
[6081]59from waeup.sirp.applicants.interfaces import (
[6196]60    IApplicant, IApplicantPrincipal,IApplicantEdit,
[6081]61    IApplicantsRoot, IApplicantsContainer, IApplicantsContainerProvider,
[6087]62    IApplicantsContainerAdd, application_types_vocab
[5686]63    )
[6184]64from waeup.sirp.browser.pages import add_local_role, del_local_roles
65from waeup.sirp.permissions import get_users_with_local_roles, getRoles
[6303]66from waeup.sirp.applicants.workflow import create_workflow, INITIALIZED, STARTED
[5320]67
[5273]68results_widget = CustomWidgetFactory(
[5301]69    WAeUPObjectWidget, ResultEntry)
[5273]70
71results_display_widget = CustomWidgetFactory(
[5301]72    WAeUPObjectDisplayWidget, ResultEntry)
[5273]73
74list_results_widget = CustomWidgetFactory(
75    MultiListWidget, subwidget=results_widget)
76
77list_results_display_widget = CustomWidgetFactory(
78    MultiListDisplayWidget, subwidget=results_display_widget)
79
[6305]80TRANSITION_OBJECTS = create_workflow()
81
82TRANSITION_DICT = dict([
83    (transition_object.transition_id,transition_object.title)
84    for transition_object in TRANSITION_OBJECTS])
85
[6067]86class ApplicantsRootPage(WAeUPPage):
[5822]87    grok.context(IApplicantsRoot)
88    grok.name('index')
[6153]89    grok.require('waeup.Public')
[5822]90    title = 'Applicants'
[6069]91    label = 'Application Section'
[5843]92    pnav = 3
[6012]93
94    def update(self):
[6067]95        super(ApplicantsRootPage, self).update()
[6012]96        datatable.need()
97        return
98
[5828]99class ManageApplicantsRootActionButton(ManageActionButton):
100    grok.context(IApplicantsRoot)
[6067]101    grok.view(ApplicantsRootPage)
[6198]102    grok.require('waeup.manageApplications')
[6069]103    text = 'Manage application section'
[5828]104
[6069]105class ApplicantsRootManageFormPage(WAeUPEditFormPage):
[5828]106    grok.context(IApplicantsRoot)
107    grok.name('manage')
[6107]108    grok.template('applicantsrootmanagepage')
[6069]109    title = 'Applicants'
110    label = 'Manage application section'
[5843]111    pnav = 3
[6198]112    grok.require('waeup.manageApplications')
[6069]113    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
[6184]114    tabtwoactions1 = ['Remove selected local roles']
115    tabtwoactions2 = ['Add local role']
[6069]116    subunits = 'Applicants Containers'
[6078]117
[6069]118    def update(self):
119        tabs.need()
[6108]120        datatable.need()
[6069]121        return super(ApplicantsRootManageFormPage, self).update()
[5828]122
[6184]123    def getLocalRoles(self):
124        roles = ILocalRolesAssignable(self.context)
125        return roles()
126
127    def getUsers(self):
128        """Get a list of all users.
129        """
130        for key, val in grok.getSite()['users'].items():
131            url = self.url(val)
132            yield(dict(url=url, name=key, val=val))
133
134    def getUsersWithLocalRoles(self):
135        return get_users_with_local_roles(self.context)
136
[6069]137    # ToDo: Show warning message before deletion
138    @grok.action('Remove selected')
139    def delApplicantsContainers(self, **data):
140        form = self.request.form
141        child_id = form['val_id']
142        if not isinstance(child_id, list):
143            child_id = [child_id]
144        deleted = []
145        for id in child_id:
146            try:
147                del self.context[id]
148                deleted.append(id)
149            except:
150                self.flash('Could not delete %s: %s: %s' % (
151                        id, sys.exc_info()[0], sys.exc_info()[1]))
152        if len(deleted):
153            self.flash('Successfully removed: %s' % ', '.join(deleted))
[6078]154        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
155        return
[5828]156
[6069]157    @grok.action('Add applicants container', validator=NullValidator)
158    def addApplicantsContainer(self, **data):
159        self.redirect(self.url(self.context, '@@add'))
[6078]160        return
161
[6069]162    @grok.action('Cancel', validator=NullValidator)
163    def cancel(self, **data):
164        self.redirect(self.url(self.context))
[6078]165        return
166
[6184]167    @grok.action('Add local role', validator=NullValidator)
168    def addLocalRole(self, **data):
169        return add_local_role(self,2, **data)
170
171    @grok.action('Remove selected local roles')
172    def delLocalRoles(self, **data):
173        return del_local_roles(self,2,**data)
174
[6069]175class ApplicantsContainerAddFormPage(WAeUPAddFormPage):
[5822]176    grok.context(IApplicantsRoot)
[6198]177    grok.require('waeup.manageApplications')
[5822]178    grok.name('add')
[6107]179    grok.template('applicantscontaineraddpage')
[6069]180    title = 'Applicants'
181    label = 'Add applicants container'
[5843]182    pnav = 3
[6078]183
[6103]184    form_fields = grok.AutoFields(
185        IApplicantsContainerAdd).omit('code').omit('title')
[6083]186    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
187    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
[6078]188
[6083]189    def update(self):
190        datepicker.need() # Enable jQuery datepicker in date fields.
[6110]191        #from waeup.sirp.browser.resources import jqueryui
192        #jqueryui.need()
[6083]193        return super(ApplicantsContainerAddFormPage, self).update()
194
[6069]195    @grok.action('Add applicants container')
196    def addApplicantsContainer(self, **data):
[6103]197        year = data['year']
198        code = u'%s%s' % (data['prefix'], year)
199        prefix = application_types_vocab.getTerm(data['prefix'])
200        title = u'%s %s/%s' % (prefix.title, year, year + 1)
[6087]201        if code in self.context.keys():
[6105]202            self.flash(
203                'An applicants container for the same application '
204                'type and entrance year exists already in the database.')
[5822]205            return
206        # Add new applicants container...
[6083]207        provider = data['provider'][1]
[5822]208        container = provider.factory()
[6069]209        self.applyData(container, **data)
[6087]210        container.code = code
211        container.title = title
212        self.context[code] = container
[6105]213        self.flash('Added: "%s".' % code)
[6069]214        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
[5822]215        return
[6078]216
[6103]217    @grok.action('Cancel', validator=NullValidator)
[6069]218    def cancel(self, **data):
[6103]219        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
[6078]220
[5845]221class ApplicantsRootBreadcrumb(Breadcrumb):
222    """A breadcrumb for applicantsroot.
223    """
224    grok.context(IApplicantsRoot)
[6153]225    title = u'Application Section'
[6078]226
[5845]227class ApplicantsContainerBreadcrumb(Breadcrumb):
228    """A breadcrumb for applicantscontainers.
229    """
230    grok.context(IApplicantsContainer)
[6153]231   
232class ApplicantBreadcrumb(Breadcrumb):
233    """A breadcrumb for applicants.
234    """
235    grok.context(IApplicant)
236   
237    @property
238    def title(self):
239        """Get a title for a context.
240        """
241        return self.context.access_code
[5828]242
243class ApplicantsTab(PrimaryNavTab):
[6153]244    """Applicants tab in primary navigation.
[5828]245    """
[6078]246
[5828]247    grok.context(IWAeUPObject)
248    grok.order(3)
[6198]249    grok.require('waeup.manageApplications')
[5828]250    grok.template('primarynavtab')
251
[5843]252    pnav = 3
[5828]253    tab_title = u'Applicants'
254
255    @property
256    def link_target(self):
257        return self.view.application_url('applicants')
258
[6029]259class ApplicantsContainerPage(WAeUPDisplayFormPage):
[5830]260    """The standard view for regular applicant containers.
261    """
262    grok.context(IApplicantsContainer)
263    grok.name('index')
[6153]264    grok.require('waeup.Public')
[6029]265    grok.template('applicantscontainerpage')
[5850]266    pnav = 3
[6053]267
[6105]268    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
[6054]269    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
270    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
[6084]271    form_fields['description'].custom_widget = ReSTDisplayWidget
[6053]272
[5837]273    @property
274    def title(self):
[6087]275        return "Applicants Container: %s" % self.context.title
[5837]276
277    @property
278    def label(self):
[6087]279        return self.context.title
[5830]280
[6107]281class ApplicantsContainerManageActionButton(ManageActionButton):
[5832]282    grok.context(IApplicantsContainer)
283    grok.view(ApplicantsContainerPage)
[6198]284    grok.require('waeup.manageApplications')
[6070]285    text = 'Manage applicants container'
[5832]286
287
[6107]288class ApplicantsContainerManageFormPage(WAeUPEditFormPage):
[5837]289    grok.context(IApplicantsContainer)
[5850]290    grok.name('manage')
[6107]291    grok.template('applicantscontainermanagepage')
[6105]292    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
293    taboneactions = ['Save','Cancel']
294    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
[6184]295    tabthreeactions1 = ['Remove selected local roles']
296    tabthreeactions2 = ['Add local role']
[5844]297    # Use friendlier date widget...
[6054]298    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
299    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
[6198]300    grok.require('waeup.manageApplications')
[5850]301
302    @property
303    def title(self):
[6087]304        return "Applicants Container: %s" % self.context.title
[6078]305
[5850]306    @property
307    def label(self):
[6087]308        return 'Manage applicants container'
[5850]309
[5845]310    pnav = 3
[5837]311
312    def update(self):
[5850]313        datepicker.need() # Enable jQuery datepicker in date fields.
[5982]314        tabs.need()
[6015]315        datatable.need()  # Enable jQurey datatables for contents listing
[6107]316        return super(ApplicantsContainerManageFormPage, self).update()
[5837]317
[6184]318    def getLocalRoles(self):
319        roles = ILocalRolesAssignable(self.context)
320        return roles()
321
322    def getUsers(self):
323        """Get a list of all users.
324        """
325        for key, val in grok.getSite()['users'].items():
326            url = self.url(val)
327            yield(dict(url=url, name=key, val=val))
328
329    def getUsersWithLocalRoles(self):
330        return get_users_with_local_roles(self.context)
331
[5850]332    @grok.action('Save')
[5837]333    def apply(self, **data):
334        self.applyData(self.context, **data)
335        self.flash('Data saved.')
336        return
[6078]337
[6105]338    # ToDo: Show warning message before deletion
339    @grok.action('Remove selected')
340    def delApplicant(self, **data):
[6189]341        form = self.request.form
342        if form.has_key('val_id'):
343            child_id = form['val_id']
344        else:
345            self.flash('No applicant selected!')
346            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
347            return
348        if not isinstance(child_id, list):
349            child_id = [child_id]
350        deleted = []
351        for id in child_id:
352            try:
353                del self.context[id]
354                deleted.append(id)
355            except:
356                self.flash('Could not delete %s: %s: %s' % (
357                        id, sys.exc_info()[0], sys.exc_info()[1]))
358        if len(deleted):
359            self.flash('Successfully removed: %s' % ', '.join(deleted))
360        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
361        return
[6105]362
363    @grok.action('Add applicant', validator=NullValidator)
364    def addApplicant(self, **data):
365        return self.flash('Manual addition of applicants not yet implemented!')
366
367    @grok.action('Cancel', validator=NullValidator)
[5837]368    def cancel(self, **data):
369        self.redirect(self.url(self.context))
370        return
[5886]371
[6184]372    @grok.action('Add local role', validator=NullValidator)
373    def addLocalRole(self, **data):
374        return add_local_role(self,3, **data)
[6105]375
[6184]376    @grok.action('Remove selected local roles')
377    def delLocalRoles(self, **data):
378        return del_local_roles(self,3,**data)
379
380
[5886]381class LoginApplicant(WAeUPPage):
382    grok.context(IApplicantsContainer)
383    grok.name('login')
[6153]384    grok.require('waeup.Public')
[5886]385
[6110]386    @property
387    def title(self):
388        return u"Applicant Login: %s" % self.context.title
[6078]389
[5886]390    @property
391    def label(self):
[6110]392        return u'Login for applicants only'
[5886]393
394    pnav = 3
[6110]395   
396    @property
397    def ac_prefix(self):
398        return self.context.ac_prefix
[6078]399
[5896]400    def update(self, SUBMIT=None):
401        self.ac_series = self.request.form.get('form.ac_series', None)
402        self.ac_number = self.request.form.get('form.ac_number', None)
[5886]403        if SUBMIT is None:
404            return
[5894]405        if self.request.principal.id == 'zope.anybody':
[6105]406            self.flash('Entered credentials are invalid.')
[5886]407            return
[5894]408        if not IApplicantPrincipal.providedBy(self.request.principal):
409            # Don't care if user is already authenticated as non-applicant
410            return
[5905]411        pin = self.request.principal.access_code
412        if pin not in self.context.keys():
413            # Create applicant record
414            applicant = Applicant()
415            applicant.access_code = pin
416            self.context[pin] = applicant
[5937]417        # Assign current principal the owner role on created applicant
418        # record
[6184]419        role_manager = IPrincipalRoleManager(self.context[pin])
[5937]420        role_manager.assignRoleToPrincipal(
[6043]421            'waeup.local.ApplicationOwner', self.request.principal.id)
[6303]422        state = IWorkflowState(self.context[pin]).getState()
423        if state == INITIALIZED:
424            IWorkflowInfo(self.context[pin]).fireTransition('start')
[6305]425            timestamp = datetime.now().strftime("%d/%m/%Y %H:%M")
426            transition_title = TRANSITION_DICT['start']
427            self.context[pin].messages += '<br />%s - %s' % (timestamp,transition_title)
[5937]428        self.redirect(self.url(self.context[pin], 'edit'))
[5886]429        return
[6153]430       
431class AccessCodeLink(LeftSidebarLink):
432    grok.order(1)
433    grok.require('waeup.Public')
[5886]434
[6153]435    def render(self):
436        if not IApplicantPrincipal.providedBy(self.request.principal):
437            return ''
438        access_code = getattr(self.request.principal,'access_code',None)
439        if access_code:
440            applicant_object = get_applicant_data(access_code)
441            url = absoluteURL(applicant_object, self.request)
[6198]442            return u'<div class="portlet"><a href="%s/edit">%s</a></div>' % (
[6153]443                url,access_code)
444        return ''
445
[5273]446class DisplayApplicant(WAeUPDisplayFormPage):
447    grok.context(IApplicant)
448    grok.name('index')
[6198]449    grok.require('waeup.handleApplication')
[6254]450    form_fields = grok.AutoFields(IApplicant).omit('locked').omit('course_admitted')
[6196]451    #form_fields['fst_sit_results'].custom_widget = list_results_display_widget
[5919]452    form_fields['passport'].custom_widget = ThumbnailWidget
[6054]453    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
[5273]454    label = 'Applicant'
[6254]455    grok.template('form_display')
[5843]456    pnav = 3
[5273]457
[6196]458    @property
459    def title(self):
460        return '%s' % self.context.access_code
461
462    @property
463    def label(self):
464        container_title = self.context.__parent__.title
465        return '%s Application Record' % container_title
466
[6254]467    @property
468    def getCourseAdmitted(self):
469        course_admitted = self.context.course_admitted
470        #import pdb; pdb.set_trace()
471        if ICertificate.providedBy(course_admitted):
472            url = self.url(course_admitted)
473            title = course_admitted.title
474            code = course_admitted.code
475            return '<a href="%s">%s (%s)</a>' %(url,title,code)
476        return 'not yet admitted'
477
[6303]478    @property
479    def getApplicationState(self):
480        state = IWorkflowState(self.context).getState()
481        return state
482
[6198]483class ApplicantsManageActionButton(ManageActionButton):
484    grok.context(IApplicant)
485    grok.view(DisplayApplicant)
486    grok.require('waeup.manageApplications')
487    text = 'Edit application record'
488    target = 'edit_full'
489
[6196]490class EditApplicantFull(WAeUPEditFormPage):
491    """A full edit view for applicant data.
492    """
493    grok.context(IApplicant)
494    grok.name('edit_full')
[6198]495    grok.require('waeup.manageApplications')
[6196]496    form_fields = grok.AutoFields(IApplicant)   #.omit('locked')
497    form_fields['passport'].custom_widget = EncodingImageFileWidget
498    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
499    grok.template('form_edit')
500    pnav = 3
501
502    def update(self):
503        datepicker.need() # Enable jQuery datepicker in date fields.
504        super(EditApplicantFull, self).update()
505        return
506
507    @property
508    def title(self):
509        return self.context.access_code
510
511    @property
512    def label(self):
513        container_title = self.context.__parent__.title
514        return '%s Application Form' % container_title
515
[6303]516    @property
517    def getTransitions(self):
518        allowed_transitions_ids = IWorkflowInfo(self.context).getManualTransitionIds()
519        null_transition = [{'name': '', 'title':'No transition'}]
520        transitions = null_transition + [dict(
521            name=transition_object.transition_id,
522            title=transition_object.title)
[6305]523            for transition_object in TRANSITION_OBJECTS
[6303]524            if transition_object.transition_id in allowed_transitions_ids]
525        #import pdb; pdb.set_trace()
526        return transitions
527
528    @property
529    def getApplicationState(self):
530        state = IWorkflowState(self.context).getState()
531        return state
532
[6196]533    @grok.action('Save')
534    def save(self, **data):
535        self.applyData(self.context, **data)
536        self.context._p_changed = True
[6303]537        form = self.request.form
538        if form.has_key('transition') and form['transition']:
[6305]539            transition_id = form['transition']
540            IWorkflowInfo(self.context).fireTransition(transition_id)
541            #import pdb; pdb.set_trace()
542            timestamp = datetime.now().strftime("%d/%m/%Y %H:%M")
543            transition_title = TRANSITION_DICT[transition_id]
544            self.context.messages += '<br />%s - %s' % (timestamp,transition_title)
[6196]545        self.flash('Form has been saved.')
546        return
547
548class EditApplicantStudent(EditApplicantFull):
[5982]549    """An applicant-centered edit view for applicant data.
550    """
[6196]551    grok.context(IApplicantEdit)
[5273]552    grok.name('edit')
[6198]553    grok.require('waeup.handleApplication')
[6196]554    form_fields = grok.AutoFields(IApplicantEdit).omit('locked')
[5686]555    form_fields['passport'].custom_widget = EncodingImageFileWidget
[6054]556    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[6196]557    grok.template('form_edit')
[5484]558
[5941]559    def emitLockMessage(self):
[6105]560        self.flash('The requested form is locked (read-only).')
[5941]561        self.redirect(self.url(self.context))
562        return
[6078]563
[5686]564    def update(self):
[5941]565        if self.context.locked:
[6198]566            self.redirect(self.url(self.context))
[5941]567            return
[6040]568        datepicker.need() # Enable jQuery datepicker in date fields.
[5982]569        super(EditApplicantStudent, self).update()
[5686]570        return
[5952]571
[6196]572    def dataNotComplete(self):
573        if self.context.confirm_passport is not True:
574            return 'Passport confirmation box not ticked.'
575        if len(self.errors) > 0:
576            return 'Form has errors.'
577        return False
[5952]578
[6303]579    @property
580    def getTransitions(self):
581        return None
582
[5273]583    @grok.action('Save')
584    def save(self, **data):
[5941]585        if self.context.locked:
586            self.emitLockMessage()
587            return
[5273]588        self.applyData(self.context, **data)
589        self.context._p_changed = True
[6196]590        self.flash('Form has been saved.')
[5273]591        return
592
[5484]593    @grok.action('Final Submit')
594    def finalsubmit(self, **data):
[5941]595        if self.context.locked:
596            self.emitLockMessage()
597            return
[5273]598        self.applyData(self.context, **data)
[5484]599        self.context._p_changed = True
[6196]600        if self.dataNotComplete():
601            self.flash(self.dataNotComplete())
[5941]602            return
[6303]603        state = IWorkflowState(self.context).getState()
604        # Cannot happen but anyway ...
605        if state != STARTED:
606            self.flash('This form cannot be submitted.')
607            return
608        IWorkflowInfo(self.context).fireTransition('submit')
[6305]609        timestamp = datetime.now().strftime("%d/%m/%Y %H:%M")
610        transition_title = TRANSITION_DICT['submit']
611        self.context.messages += '<br />%s - %s' % (timestamp,transition_title)
[5941]612        self.context.locked = True
[6196]613        self.flash('Form has been submitted.')
614        self.redirect(self.url(self.context))
[5273]615        return
[5941]616
Note: See TracBrowser for help on using the repository browser.