source: main/waeup.sirp/trunk/src/waeup/sirp/applicants/interfaces.py @ 6189

Last change on this file since 6189 was 6189, checked in by Henrik Bettermann, 13 years ago

Select application_category for applicants containers.

Implement removal of applicants.

File size: 25.0 KB
Line 
1##
2## interfaces.py
3## Login : <uli@pu.smp.net>
4## Started on  Sun Jan 16 15:30:01 2011 Uli Fouquet
5## $Id$
6##
7## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
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.
12##
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.
17##
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##
22"""Interfaces regarding student applicants and related components.
23"""
24import os
25import waeup.sirp.browser
26from datetime import datetime
27from grokcore.content.interfaces import IContainer
28from zc.sourcefactory.basic import BasicSourceFactory
29from zope import schema
30from zope.component import getUtility, getUtilitiesFor
31from zope.interface import Interface, Attribute
32from zope.pluggableauth.interfaces import IPrincipalInfo
33from zope.security.interfaces import IGroupClosureAwarePrincipal as IPrincipal
34from waeup.sirp.image.schema import ImageFile
35from waeup.sirp.image.image import WAeUPImageFile
36from waeup.sirp.interfaces import IWAeUPObject, SimpleWAeUPVocabulary
37from waeup.sirp.university.vocabularies import application_categories
38
39
40IMAGE_PATH = os.path.join(
41    os.path.dirname(waeup.sirp.browser.__file__),
42    'static'
43    )
44DEFAULT_PASSPORT_IMAGE_MALE = WAeUPImageFile(
45    'passport.jpg',
46    open(os.path.join(IMAGE_PATH, 'placeholder_m.jpg')).read(),
47    )
48DEFAULT_PASSPORT_IMAGE_FEMALE = WAeUPImageFile(
49    'passport.jpg',
50    open(os.path.join(IMAGE_PATH, 'placeholder_f.jpg')).read(),
51    )
52
53#: Types of applications we support.
54APPLICATION_TYPES = (
55    ('General Studies', 'app','APP'),
56    ('Pre-NCE Programme', 'prence','PRE'),
57    ('Post UME Screening Test', 'pume','PUME'),
58    ('Post UDE Screening', 'pude','PUDE'),
59    ('Part Time Degree in Education', 'sandwich','SAND'),
60    ('Part-Time Degree Programmes', 'pt','PTP'),
61    ('Diploma Programmes', 'dp','DPP'),
62    ('PCE Screening', 'pce','PCE'),
63    ('Certificate Programmes', 'ct','CTP'),
64    ('Common Entry Screening Test', 'cest','CEST'),
65    )
66
67#: A :class:`waeup.sirp.interfaces.SimpleWAeUPVocabulary` of supported
68#: application or screening types.
69application_types_vocab = SimpleWAeUPVocabulary(
70    *[(x[0],x[1]) for x in APPLICATION_TYPES])
71application_pins_vocab = SimpleWAeUPVocabulary(
72    *[(u"%s (%s)" % (x[2],x[0]),x[2]) for x in APPLICATION_TYPES])
73
74def year_range():
75    curr_year = datetime.now().year
76    return range(curr_year - 2, curr_year + 5)
77
78class GenderSource(BasicSourceFactory):
79    """A gender source delivers basically a mapping
80       ``{'m': 'male', 'f': 'female'}``
81
82       Using a source, we make sure that the tokens (which are
83       stored/expected for instance from CSV files) are something one
84       can expect and not cryptic IntIDs.
85    """
86    def getValues(self):
87        return ['m', 'f']
88
89    def getToken(self, value):
90        return value[0].lower()
91
92    def getTitle(self, value):
93        if value == 'm':
94            return 'male'
95        if value == 'f':
96            return 'female'
97
98class ApplicantContainerProviderSource(BasicSourceFactory):
99    """A source offering all available applicants container types.
100
101    The values returned by this source are names of utilities that can
102    create :class:`ApplicantContainer` instances. So, if you get a
103    name like ``'myactype'`` from this source, then you can do:
104
105      >>> from zope.component import getUtility
106      >>> p = getUtility(IApplicantsContainerProvider, name=myactype)
107      >>> my_applicants_container = p.factory()
108
109    Or you can access class-attributes like
110
111      >>> my_applicants_container.container_title
112      'Pretty'
113
114    """
115    def getValues(self):
116        """Returns a list of ``(<name>, <provider>)`` tuples.
117
118        Here ``<name>`` is the name under which an
119        :class:``ApplicantContainerProvider`` was registered as a
120        utility and ``<provider>`` is the utility itself.
121        """
122        return getUtilitiesFor(IApplicantsContainerProvider)
123
124    def getToken(self, value):
125        """Return the name of the ``(<name>, <provider>)`` tuple.
126        """
127        return value[0]
128
129    def getTitle(self, value):
130        """Get a 'title - description' string for a container type.
131        """
132        factory = value[1].factory
133        return "%s - %s" % (
134            factory.container_title, factory.container_description)
135
136class IResultEntry(IWAeUPObject):
137    subject = schema.TextLine(
138        title = u'Subject',
139        description = u'The subject',
140        required=False,
141        )
142    score = schema.TextLine(
143        title = u'Score',
144        description = u'The score',
145        required=False,
146        )
147
148class IApplicantsRoot(IWAeUPObject, IContainer):
149    """A container for university applicants containers.
150    """
151    pass
152
153
154class IApplicantsContainer(IWAeUPObject):
155    """An applicants container contains university applicants.
156
157    """
158
159    container_title = Attribute(
160        u'classattribute: title for type of container')
161    container_description = Attribute(
162        u'classattribute: description for type of container')
163
164
165    code = schema.TextLine(
166        title = u'Code',
167        default = u'-',
168        required = True,
169        readonly = True,
170        )
171
172    title = schema.TextLine(
173        title = u'Title',
174        required = True,
175        default = u'-',
176        readonly = True,
177        )
178
179    prefix = schema.Choice(
180        title = u'Application target',
181        required = True,
182        default = None,
183        source = application_types_vocab,
184        readonly = True,
185        )
186
187    year = schema.Choice(
188        title = u'Year of entrance',
189        required = True,
190        default = None,
191        values = year_range(),
192        readonly = True,
193        )
194
195    provider = schema.Choice(
196        title = u'Applicants container type',
197        required = True,
198        default = None,
199        source = ApplicantContainerProviderSource(),
200        readonly = True,
201        )
202
203    ac_prefix = schema.Choice(
204        title = u'Access code prefix',
205        required = True,
206        default = None,
207        source = application_pins_vocab,
208        )
209
210    application_category = schema.Choice(
211        title = u'Category for the grouping of study courses',
212        required = True,
213        default = None,
214        source = application_categories,
215        )
216
217    description = schema.Text(
218        title = u'Human readable description in reST format',
219        required = False,
220        default = u'No description yet.'
221        )
222
223    startdate = schema.Date(
224        title = u'Date when the application period starts',
225        required = False,
226        default = None,
227        )
228
229    enddate = schema.Date(
230        title = u'Date when the application period ends',
231        required = False,
232        default = None,
233        )
234
235    strict_deadline = schema.Bool(
236        title = u'Forbid additions after deadline (enddate)',
237        required = True,
238        default = True,
239        )
240
241    def archive(id=None):
242        """Create on-dist archive of applicants stored in this term.
243
244        If id is `None`, all applicants are archived.
245
246        If id contains a single id string, only the respective
247        applicants are archived.
248
249        If id contains a list of id strings all of the respective
250        applicants types are saved to disk.
251        """
252
253    def clear(id=None, archive=True):
254        """Remove applicants of type given by 'id'.
255
256        Optionally archive the applicants.
257
258        If id is `None`, all applicants are archived.
259
260        If id contains a single id string, only the respective
261        applicants are archived.
262
263        If id contains a list of id strings all of the respective
264        applicant types are saved to disk.
265
266        If `archive` is ``False`` none of the archive-handling is done
267        and respective applicants are simply removed from the
268        database.
269        """
270
271class IApplicantsContainerAdd(IApplicantsContainer):
272    """An applicants container contains university applicants.
273    """
274    prefix = schema.Choice(
275        title = u'Application target',
276        required = True,
277        default = None,
278        source = application_types_vocab,
279        readonly = False,
280        )
281
282    year = schema.Choice(
283        title = u'Year of entrance',
284        required = True,
285        default = None,
286        values = year_range(),
287        readonly = False,
288        )
289
290    provider = schema.Choice(
291        title = u'Applicants container type',
292        required = True,
293        default = None,
294        source = ApplicantContainerProviderSource(),
295        readonly = False,
296        )
297
298IApplicantsContainerAdd[
299    'prefix'].order =  IApplicantsContainer['prefix'].order
300IApplicantsContainerAdd[
301    'year'].order =  IApplicantsContainer['year'].order
302IApplicantsContainerAdd[
303    'provider'].order =  IApplicantsContainer['provider'].order
304
305class IApplicantBaseData(IWAeUPObject):
306    """The data for an applicant.
307
308    This is a base interface with no field (except ``reg_no``)
309    required. For use with importers, forms, etc., please use one of
310    the derived interfaces below, which set more fields to required
311    state, depending on use-case.
312    """
313    reg_no = schema.TextLine(
314        title = u'JAMB Registration Number',
315        )
316    access_code = schema.TextLine(
317        title = u'Access Code',
318        required = False,
319        )
320    serial = schema.TextLine(
321        title = u'Serial Number',
322        required = False,
323        )
324    course1 = schema.TextLine(
325        title = u'1st Choice Course of Study',
326        required = False,
327        )
328    course2 = schema.TextLine(
329        title = u'2nd Choice Course of Study',
330        required = False,
331        )
332    course3 = schema.TextLine(
333        title = u'3rd Choice Course of Study',
334        required = False,
335        )
336    firstname = schema.TextLine(
337        title = u'First Name',
338        required = False,
339        )
340    middlenames = schema.TextLine(
341        title = u'Middle Names',
342        required = False,
343        )
344    lastname = schema.TextLine(
345        title = u'Surname/Full Name',
346        required = False,
347        )
348    date_of_birth = schema.Date(
349        title = u'Date of Birth',
350        required = False,
351        )
352    lga = schema.TextLine(
353        # XXX: should be choice
354        title = u'State/LGA (confirmed by applicant)',
355        required = False,
356        )
357    sex = schema.Choice(
358        title = u'Sex',
359        source = GenderSource(),
360        default = u'm',
361        required = False,
362        )
363    email = schema.TextLine(
364        title = u'Email',
365        required = False,
366        )
367    phone = schema.TextLine(
368        title = u'Phone',
369        required = False,
370        )
371    passport = ImageFile(
372        title = u'Passport Photograph',
373        default = DEFAULT_PASSPORT_IMAGE_MALE,
374        required = True,
375        #max_size = 20480,
376        )
377    aos = schema.TextLine(
378        # XXX: should be choice
379        title = u'Area of Specialisation',
380        required = False,
381        )
382    subj1 = schema.TextLine(
383        # XXX: should be choice
384        title = u'1st Choice of Study',
385        required = False,
386        )
387    subj2 = schema.TextLine(
388        # XXX: should be choice
389        title = u'2nd Choice of Study',
390        required = False,
391        )
392    subj3 = schema.TextLine(
393        # XXX: should be choice
394        title = u'3rd Choice of Study',
395        required = False,
396        )
397    #
398    # Higher Educational Data
399    #
400    hq_matric_no = schema.TextLine(
401        title = u'Former Matric Number',
402        required = False,
403        )
404    hq_type = schema.TextLine(
405        title = u'Higher Qualification',
406        required = False,
407        )
408    hq_grade = schema.TextLine(
409        title = u'Higher Qualification Grade',
410        required = False,
411        )
412    hq_school = schema.TextLine(
413        title = u'School Attended',
414        required = False,
415        )
416    hq_session = schema.TextLine(
417        title = u'Session Obtained',
418        required = False,
419        )
420    hq_disc = schema.TextLine(
421        title = u'Discipline',
422        required = False,
423        )
424    #
425    # First sitting data
426    #
427    fst_sit_fname = schema.TextLine(
428        title = u'Full Name',
429        required = False,
430        )
431    fst_sit_no = schema.TextLine(
432        title = u'Exam Number',
433        required = False,
434        )
435    fst_sit_date = schema.Date(
436        title = u'Exam Date (dd/mm/yyyy)',
437        required = False,
438        )
439    fst_sit_type = schema.TextLine(
440        # XXX: Should be choice
441        title = u'Exam Type',
442        required = False,
443        )
444    fst_sit_results = schema.List(
445        title = u'Results',
446        required = False,
447        value_type = schema.Object(
448            title = u'Entries',
449            schema = IResultEntry,
450            required = False,
451            )
452        )
453    scd_sit_fname = schema.TextLine(
454        title = u'Full Name',
455        required = False,
456        )
457    scd_sit_no = schema.TextLine(
458        title = u'Exam Number',
459        required = False,
460        )
461    scd_sit_date = schema.Date(
462        title = u'Exam Date (dd/mm/yyyy)',
463        required = False,
464        )
465    scd_sit_type = schema.TextLine(
466        # XXX: Should be choice
467        title = u'Exam Type',
468        required = False,
469        )
470    scd_sit_results = schema.TextLine(
471        # XXX: Should be nested list of choices
472        title = u'Results',
473        required = False,
474        )
475    #
476    # JAMB scores
477    #
478    eng_score = schema.TextLine(
479        title = u"'English' score",
480        required = False,
481        )
482    subj1score = schema.TextLine(
483        title = u'1st Choice of Study Score',
484        required = False,
485        )
486    subj2score = schema.TextLine(
487        title = u'2nd Choice of Study Score',
488        required = False,
489        )
490    subj3score = schema.TextLine(
491        title = u'3rd Choice of Study Score',
492        required = False,
493        )
494    # XXX: Total score???
495
496    #
497    # Application Data
498    #
499    application_date = schema.Date(
500        title = u'Application Date',
501        required = False,
502        )
503    status = schema.TextLine(
504        # XXX: should be 'status' type
505        title = u'Application Status',
506        required = False,
507        )
508    screening_date = schema.Date(
509        title = u'Screening Date',
510        required = False,
511        )
512    screening_type = schema.TextLine(
513        # XXX: schould be choice
514        title = u'Screening Type',
515        required = False,
516        )
517    screening_score = schema.TextLine(
518        title = u'Screening Score',
519        required = False,
520        )
521    screening_venue = schema.TextLine(
522        title = u'Screening Venue',
523        required = False,
524        )
525    total_score = schema.TextLine(
526        title = u'Total Score',
527        required = False,
528        )
529    course_admitted = schema.TextLine(
530        # XXX: should be choice
531        title = u'Admitted Course of Study',
532        required = False,
533        )
534    department = schema.TextLine(
535        # XXX: if we have a course, dept. is not necessary
536        title = u'Department',
537        required = False,
538        )
539    faculty = schema.TextLine(
540        # XXX: if we have a course, faculty is not necessary
541        title = u'Faculty',
542        required = False,
543        )
544    entry_session = schema.TextLine(
545        # XXX: should be choice, should have sensible default: upcoming session
546        title = u'Entry Session',
547        required = False,
548        )
549    notice = schema.Text(
550        title = u'Notice',
551        required = False,
552        )
553    student_id = schema.TextLine(
554        title = u'Student ID',
555        required = False,
556        )
557    import_record_no = schema.TextLine(
558        title = u'Import Record No.',
559        required = False,
560        )
561    imported_by = schema.TextLine(
562        title = u'Imported By',
563        required = False,
564        )
565    import_date = schema.Datetime(
566        title = u'Import Date',
567        required = False,
568        )
569    import_from = schema.TextLine(
570        title = u'Import Source',
571        required = False,
572        )
573    confirm_passport = schema.Bool(
574        title = u"Confirmation that photograph represents applicant ticked.",
575        default = False,
576        required = True,
577        )
578
579
580class IApplicant(IApplicantBaseData):
581    """An applicant.
582
583    This is basically the applicant base data. Here we repeat the
584    fields from base data only with the `required` attribute of
585    required attributes set to True (which is the default).
586    """
587    locked = schema.Bool(
588        title = u'Form locked',
589        default = False,
590        readonly = True,
591        )
592    access_code = schema.TextLine(
593        title = u'Access Code',
594        )
595    course1 = schema.TextLine(
596        title = u'1st Choice Course of Study',
597        )
598    firstname = schema.TextLine(
599        title = u'First Name',
600        )
601    middlenames = schema.TextLine(
602        title = u'Middle Names',
603        )
604    lastname = schema.TextLine(
605        title = u'Surname/Full Name',
606        )
607    date_of_birth = schema.Date(
608        title = u'Date of Birth',
609        )
610    jamb_state = schema.TextLine(
611        title = u'State (provided by JAMB)',
612        )
613    jamb_lga = schema.TextLine(
614        title = u'LGA (provided by JAMB)',
615        )
616    lga = schema.TextLine(
617        # XXX: should be choice
618        title = u'State/LGA (confirmed by applicant)',
619        )
620    sex = schema.Choice(
621        title = u'Sex',
622        source = GenderSource(),
623        default = u'm',
624        )
625    #passport = schema.Bool(
626    #    title = u'Passport Photograph',
627    #    default = True,
628    #    )
629    passport = ImageFile(
630        title = u'Passport Photograph',
631        default = DEFAULT_PASSPORT_IMAGE_MALE,
632        required = True ,
633        #max_size = 20480,
634        )
635    #
636    # Higher Educational Data
637    #
638
639    #
640    # First sitting data
641    #
642    fst_sit_fname = schema.TextLine(
643        title = u'Full Name',
644        )
645
646    #
647    # Second sitting data
648    #
649    scd_sit_fname = schema.TextLine(
650        title = u'Full Name',
651        )
652    #
653    # Application Data
654    #
655    application_date = schema.Date(
656        title = u'Application Date',
657        )
658    status = schema.TextLine(
659        # XXX: should be 'status' type
660        title = u'Application Status',
661        )
662    screening_date = schema.Date(
663        title = u'Screening Date',
664        )
665    screening_type = schema.TextLine(
666        # XXX: schould be choice
667        title = u'Screening Type',
668        )
669    screening_score = schema.TextLine(
670        title = u'Screening Score',
671        )
672    entry_session = schema.TextLine(
673        # XXX: should be choice
674        # XXX: should have sensible default: upcoming session
675        title = u'Entry Session',
676        )
677    import_record_no = schema.TextLine(
678        title = u'Import Record No.',
679        )
680    imported_by = schema.TextLine(
681        title = u'Imported By',
682        )
683    import_date = schema.Datetime(
684        title = u'Import Date',
685        )
686    import_from = schema.TextLine(
687        title = u'Import Source',
688        )
689
690class IApplicantPDEEditData(IWAeUPObject):
691    """The data set presented to PDE applicants.
692    """
693    locked = schema.Bool(
694        title = u'Form locked',
695        default = False,
696        readonly = True,
697        )
698    access_code = schema.TextLine(
699        title = u'Access Code',
700        readonly = True,
701        )
702    course1 = schema.TextLine(
703        title = u'1st Choice Course of Study',
704        readonly = True,
705        )
706    course2 = schema.TextLine(
707        title = u'2nd Choice Course of Study',
708        required = False,
709        )
710    course3 = schema.TextLine(
711        title = u'3rd Choice Course of Study',
712        required = False,
713        )
714    lastname = schema.TextLine(
715        title = u'Name',
716        readonly = True,
717        )
718    date_of_birth = schema.Date(
719        title = u'Date of Birth',
720        required = True,
721        )
722    lga = schema.TextLine(
723        # XXX: should be choice
724        title = u'State/LGA (confirmed by applicant)',
725        required = False,
726        )
727    email = schema.TextLine(
728        title = u'Email',
729        required = False,
730        )
731    phone = schema.TextLine(
732        title = u'Phone',
733        required = False,
734        )
735    aos = schema.TextLine(
736        # XXX: should be choice
737        title = u'Area of Specialisation',
738        required = False,
739        )
740    subj1 = schema.TextLine(
741        # XXX: should be choice
742        title = u'1st Choice of Study',
743        readonly = True,
744        )
745    subj2 = schema.TextLine(
746        # XXX: should be choice
747        title = u'2nd Choice of Study',
748        required = False,
749        )
750    subj3 = schema.TextLine(
751        # XXX: should be choice
752        title = u'3rd Choice of Study',
753        required = False,
754        )
755
756    #
757    # Application Data
758    #
759    application_date = schema.Date(
760        title = u'Application Date',
761        readonly = True,
762        )
763    status = schema.TextLine(
764        # XXX: should be 'status' type
765        title = u'Application Status',
766        readonly = True,
767        )
768    screening_date = schema.Date(
769        title = u'Screening Date',
770        readonly = True,
771        )
772    passport = ImageFile(
773        title = u'Passport Photograph',
774        default = DEFAULT_PASSPORT_IMAGE_MALE,
775        required = True,
776        )
777    confirm_passport = schema.Bool(
778        title = u"Confirmation that photograph represents applicant ticked.",
779        default = False,
780        required = True,
781        )
782
783
784class IApplicantPrincipalInfo(IPrincipalInfo):
785    """Infos about principals that are applicants.
786    """
787    access_code = Attribute("The Access Code the user purchased")
788
789class IApplicantPrincipal(IPrincipal):
790    """A principal that is an applicant.
791
792    This interface extends zope.security.interfaces.IPrincipal and
793    requires also an `id` and other attributes defined there.
794    """
795    access_code = schema.TextLine(
796        title = u'Access Code',
797        description = u'The access code purchased by the user.',
798        required = True,
799        readonly = True)
800
801class IApplicantsFormChallenger(Interface):
802    """A challenger that uses a browser form to collect applicant
803       credentials.
804    """
805    loginpagename = schema.TextLine(
806        title = u'Loginpagename',
807        description = u"""Name of the login form used by challenger.
808
809        The form must provide an ``access_code`` input field.
810        """)
811
812    accesscode_field = schema.TextLine(
813        title = u'Access code field',
814        description = u'''Field of the login page which is looked up for
815                          access_code''',
816        default = u'access_code',
817        )
818
819
820class IApplicantSessionCredentials(Interface):
821    """Interface for storing and accessing applicant credentials in a
822       session.
823    """
824
825    def __init__(access_code):
826        """Create applicant session credentials."""
827
828    def getAccessCode():
829        """Return the access code."""
830
831
832class IApplicantsContainerProvider(Interface):
833    """A provider for applicants containers.
834
835    Applicants container providers are meant to be looked up as
836    utilities. This way we can find all applicant container types
837    defined somewhere.
838
839    Each applicants container provider registered as utility provides
840    one container type and one should be able to call the `factory`
841    attribute to create an instance of the requested container type.
842
843    .. THE FOLLOWING SHOULD GO INTO SPHINX DOCS (and be tested)
844
845    Samples
846    *******
847
848    Given, you had an IApplicantsContainer implementation somewhere
849    and you would like to make it findable on request, then you would
850    normally create an appropriate provider utility like this::
851
852      import grok
853      from waeup.sirp.applicants.interfaces import IApplicantsContainerProvider
854
855      class MyContainerProvider(grok.GlobalUtility):
856          grok.implements(IApplicantsContainerProvider)
857          grok.name('MyContainerProvider') # Must be unique
858          factory = MyContainer # A class implementing IApplicantsContainer
859                                # or derivations thereof.
860
861    This utility would be registered on startup and could then be used
862    like this:
863
864      >>> from zope.component import getAllUtilitiesRegisteredFor
865      >>> from waeup.sirp.applicants.interfaces import (
866      ...     IApplicantsContainerProvider)
867      >>> all_providers = getAllUtilitiesRegisteredFor(
868      ...     IApplicantsContainerProvider)
869      >>> all_providers
870      [<MyContainerProvider object at 0x...>]
871
872    You could look up this specific provider by name:
873
874      >>> from zope.component import getUtility
875      >>> p = getUtility(IApplicantsContainerProvider, name='MyProvider')
876      >>> p
877      <MyContainerProvider object at 0x...>
878
879    An applicants container would then be created like this:
880
881      >>> provider = all_providers[0]
882      >>> container = provider.factory()
883      >>> container
884      <MyContainer object at 0x...>
885
886    """
887    factory = Attribute("A class that can create instances of the "
888                        "requested container type")
Note: See TracBrowser for help on using the repository browser.