source: main/waeup.kofa/trunk/src/waeup/kofa/applicants/interfaces.py @ 16281

Last change on this file since 16281 was 16275, checked in by Henrik Bettermann, 4 years ago

Add ApplicantEmailSource (not used and tested in base package).

  • Property svn:keywords set to Id
File size: 21.9 KB
RevLine 
[5638]1## $Id: interfaces.py 16275 2020-10-09 07:49:18Z henrik $
[6076]2##
[6087]3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
[5638]4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
[6076]8##
[5638]9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
[6076]13##
[5638]14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
[6500]18"""Interfaces of the university application package.
[5638]19"""
[9115]20from datetime import datetime
[5866]21from grokcore.content.interfaces import IContainer
[7795]22from zc.sourcefactory.contextual import BasicContextualSourceFactory
[5638]23from zope import schema
[8613]24from zope.component import queryUtility, getUtility
[7263]25from zope.catalog.interfaces import ICatalog
[7795]26from zope.interface import Interface, Attribute, implements, directlyProvides
[7317]27from zope.schema.interfaces import (
28    ValidationError, ISource, IContextSourceBinder)
[8149]29from waeup.kofa.schema import TextLineChoice, FormattedDate
[7811]30from waeup.kofa.interfaces import (
[13235]31    IKofaObject, validate_email, validate_html,
[8033]32    SimpleKofaVocabulary)
[7811]33from waeup.kofa.interfaces import MessageFactory as _
34from waeup.kofa.payments.interfaces import IOnlinePayment
[8176]35from waeup.kofa.schema import PhoneNumber
[14011]36from waeup.kofa.schoolgrades import ResultEntryField
37from waeup.kofa.refereeentries import RefereeEntryField
[8767]38from waeup.kofa.students.vocabularies import GenderSource, RegNumberSource
[10831]39from waeup.kofa.university.vocabularies import (
40    AppCatSource, CertificateSource, SpecialApplicationSource)
[5638]41
[8611]42_marker = object() # a marker different from None
43
[9115]44def year_range():
45    curr_year = datetime.now().year
[13650]46    return range(curr_year - 4, curr_year + 5)
[9115]47
[7263]48class RegNumInSource(ValidationError):
49    """Registration number exists already
50    """
51    # The docstring of ValidationErrors is used as error description
52    # by zope.formlib.
53    pass
54
[8767]55class ApplicantRegNumberSource(RegNumberSource):
56    """A source that accepts any reg number if not used already by a
57    different applicant.
58    """
[7263]59    cat_name = 'applicants_catalog'
[7270]60    field_name = 'reg_number'
[7263]61    validation_error = RegNumInSource
[8767]62    comp_field = 'applicant_id'
[7263]63
64def contextual_reg_num_source(context):
[8767]65    source = ApplicantRegNumberSource(context)
[7263]66    return source
67directlyProvides(contextual_reg_num_source, IContextSourceBinder)
68
[16275]69class EmailInSource(ValidationError):
70    """An application record with email address exists.
71    """
72    # The docstring of ValidationErrors is used as error description
73    # by zope.formlib.
74    pass
[7795]75
[16275]76class ApplicantEmailSource(RegNumberSource):
77    """A source that accepts any email address if not used already by a
78    different applicant. This source is not used in the base package.
79    """
80    cat_name = 'applicants_catalog'
81    field_name = 'email'
82    validation_error = EmailInSource
83    comp_field = 'applicant_id'
84
85def contextual_email_source(context):
86    source = ApplicantEmailSource(context)
87    return source
88directlyProvides(contextual_email_source, IContextSourceBinder)
89
[7683]90class AppCatCertificateSource(CertificateSource):
[8607]91    """An application certificate source delivers all certificates
92    which belong to a certain application_category.
93
94    This source is meant to be used with Applicants.
95
96    The application category must match the application category of
97    the context parent, normally an applicants container.
[7683]98    """
[8607]99    def contains(self, context, value):
100        context_appcat = getattr(getattr(
[8611]101            context, '__parent__', None), 'application_category', _marker)
102        if context_appcat is _marker:
[8607]103            # If the context (applicant) has no application category,
104            # then it might be not part of a container (yet), for
105            # instance during imports. We consider this correct.
106            return True
107        if value.application_category == context_appcat:
108            return True
109        return False
110
[7683]111    def getValues(self, context):
112        appcat = getattr(getattr(context, '__parent__', None),
113                         'application_category', None)
114        catalog = getUtility(ICatalog, name='certificates_catalog')
115        result = catalog.searchResults(
116            application_category=(appcat,appcat))
[10208]117        resultlist = getUtility(
[13133]118            IApplicantsUtils).sortCertificates(context, result)
[10208]119        return resultlist
[7683]120
[10186]121    def getTitle(self, context, value):
122        return getUtility(
123            IApplicantsUtils).getCertTitle(context, value)
124
[7683]125class ApplicationTypeSource(BasicContextualSourceFactory):
126    """An application type source delivers screening types defined in the
127    portal.
128    """
129    def getValues(self, context):
[7688]130        appcats_dict = getUtility(
[7844]131            IApplicantsUtils).APP_TYPES_DICT
[15550]132        return [item[0] for item in sorted(appcats_dict.items(),
133                                           key=lambda item: item[1])]
[7683]134
135    def getToken(self, context, value):
136        return value
137
138    def getTitle(self, context, value):
[7688]139        appcats_dict = getUtility(
[7844]140            IApplicantsUtils).APP_TYPES_DICT
[7688]141        return appcats_dict[value][0]
[7683]142
[8773]143# Maybe FUTMinna still needs this ...
[7683]144#class ApplicationPinSource(BasicContextualSourceFactory):
145#    """An application pin source delivers PIN prefixes for application
146#    defined in the portal.
147#    """
148#    def getValues(self, context):
[7688]149#        apppins_dict = getUtility(
[7844]150#            IApplicantsUtils).APP_TYPES_DICT
[7688]151#        return sorted(appcats_dict.keys())
[7683]152#
153#    def getToken(self, context, value):
154#        return value
155#
156#    def getTitle(self, context, value):
[7688]157#        apppins_dict = getUtility(
[7844]158#            IApplicantsUtils).APP_TYPES_DICT
[7683]159#        return u"%s (%s)" % (
[7688]160#            apppins_dict[value][1],self.apppins_dict[value][0])
[7683]161
[8033]162application_modes_vocab = SimpleKofaVocabulary(
163    (_('Create Application Records'), 'create'),
164    (_('Update Application Records'), 'update'),
165    )
[6075]166
[7682]167class IApplicantsUtils(Interface):
168    """A collection of methods which are subject to customization.
169    """
[10184]170    APP_TYPES_DICT = Attribute('dict of application types')
[7844]171
[10184]172    def setPaymentDetails(container, payment):
173        """Set the payment data of an applicant.
174        """
175
176    def getApplicantsStatistics(container):
177        """Count applicants in containers.
178        """
179
[10187]180    def filterCertificates(context, resultset):
[10184]181        """Filter and sort certificates for AppCatCertificateSource.
182        """
183
[10186]184    def getCertTitle(context, value):
185        """Compose the titles in AppCatCertificateSource.
186        """
187
[16134]188    def isPictureEditable(container):
189        """False if applicants are not allowed to edit uploaded pictures.
190        """
191
[7819]192class IApplicantsRoot(IKofaObject, IContainer):
[13077]193    """A container for applicants containers.
[5645]194    """
[13077]195    description_dict = Attribute('Language translation dictionary with values in HTML format')
196    local_roles = Attribute('List of local role names')
197    logger_name = Attribute('Name of the logger')
198    logger_filename = Attribute('Name of the logger file')
[5638]199
[8388]200    description = schema.Text(
201        title = _(u'Human readable description in HTML format'),
202        required = False,
[13235]203        constraint=validate_html,
[8388]204        default = u'''This text can been seen by anonymous users.
205Here we put multi-lingual general information about the application procedure.
206>>de<<
207Dieser Text kann von anonymen Benutzern gelesen werden.
208Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
209        )
210
[7819]211class IApplicantsContainer(IKofaObject):
[13077]212    """An applicants container contains applicants.
[5638]213    """
[13077]214    statistics = Attribute('Applicant counts')
215    expired = Attribute('True if application has started but not ended')
[6069]216
[13077]217    description_dict = Attribute('Language translation dictionary with values in HTML format')
218    local_roles = Attribute('List of local role names')
[16133]219    picture_editable = Attribute('False if applicants are not allowed to edit uploaded pictures.')
[13077]220
221
[6069]222    code = schema.TextLine(
[7708]223        title = _(u'Code'),
[6069]224        required = True,
225        readonly = True,
[6076]226        )
[6096]227
[6087]228    title = schema.TextLine(
[7708]229        title = _(u'Title'),
[6087]230        required = True,
[8562]231        readonly = False,
[6096]232        )
233
[6087]234    prefix = schema.Choice(
[7708]235        title = _(u'Application Target'),
[6087]236        required = True,
[7683]237        source = ApplicationTypeSource(),
[6087]238        readonly = True,
239        )
[6076]240
[6087]241    year = schema.Choice(
[7708]242        title = _(u'Year of Entrance'),
[16190]243        required = False,
[6158]244        values = year_range(),
[16190]245        readonly = False,
[6096]246        )
[6087]247
[8033]248    mode = schema.Choice(
249        title = _(u'Application Mode'),
250        vocabulary = application_modes_vocab,
251        required = True,
252        )
253
[8773]254    # Maybe FUTMinna still needs this ...
[7376]255    #ac_prefix = schema.Choice(
256    #    title = u'Activation code prefix',
257    #    required = True,
258    #    default = None,
[7683]259    #    source = ApplicationPinSource(),
[7376]260    #    )
[6076]261
[6189]262    application_category = schema.Choice(
[7708]263        title = _(u'Category for the grouping of certificates'),
[6189]264        required = True,
[7681]265        source = AppCatSource(),
[6189]266        )
267
[5645]268    description = schema.Text(
[8365]269        title = _(u'Human readable description in HTML format'),
[5638]270        required = False,
[13235]271        constraint=validate_html,
[6518]272        default = u'''This text can been seen by anonymous users.
[8033]273Here we put multi-lingual information about the study courses provided, the application procedure and deadlines.
[7708]274>>de<<
275Dieser Text kann von anonymen Benutzern gelesen werden.
276Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
[5638]277        )
278
[8200]279    startdate = schema.Datetime(
[7708]280        title = _(u'Application Start Date'),
[5638]281        required = False,
[13031]282        description = _('Example: ') + u'2011-12-01 18:30:00+01:00',
[5638]283        )
284
[8200]285    enddate = schema.Datetime(
[7708]286        title = _(u'Application Closing Date'),
[5638]287        required = False,
[13031]288        description = _('Example: ') + u'2011-12-31 23:59:59+01:00',
[5638]289        )
290
[5645]291    strict_deadline = schema.Bool(
[7708]292        title = _(u'Forbid additions after deadline (enddate)'),
[7984]293        required = False,
[5645]294        default = True,
295        )
[5638]296
[8525]297    application_fee = schema.Float(
298        title = _(u'Application Fee'),
299        default = 0.0,
300        required = False,
301        )
302
[11869]303    application_slip_notice = schema.Text(
304        title = _(u'Human readable notice on application slip in HTML format'),
305        required = False,
[13235]306        constraint=validate_html,
[11869]307        )
308
[10097]309    hidden= schema.Bool(
310        title = _(u'Hide container'),
311        required = False,
[10098]312        default = False,
[10097]313        )
314
[15502]315    with_picture= schema.Bool(
316        title = _(u'With passport picture'),
317        required = False,
318        default = True,
319        )
320
[13077]321    def addApplicant(applicant):
322        """Add an applicant.
[5638]323        """
324
[13077]325    def writeLogMessage(view, comment):
[13167]326        """Add an INFO message to applicants.log.
[5638]327        """
[6073]328
[13077]329    def traverse(name):
330        """Deliver appropriate containers.
[9531]331        """
332
[6069]333class IApplicantsContainerAdd(IApplicantsContainer):
334    """An applicants container contains university applicants.
335    """
[6087]336    prefix = schema.Choice(
[7708]337        title = _(u'Application Target'),
[6069]338        required = True,
[7683]339        source = ApplicationTypeSource(),
[6069]340        readonly = False,
[6076]341        )
[6073]342
[6087]343    year = schema.Choice(
[7708]344        title = _(u'Year of Entrance'),
[16190]345        required = False,
[6158]346        values = year_range(),
[6087]347        readonly = False,
[6096]348        )
[6073]349
[15548]350    container_number = schema.Choice(
351        title = _(u'Container Number'),
[15552]352        values = range(1,100),
[15548]353        description = _(u'If set, this number will be added to the container '
354                         'prefix (e.g. app3). If not set, the year of entrance will be '
355                         'used (e.g. app2019).'),
356        required = False,
357        readonly = False,
358        )
359
[6096]360IApplicantsContainerAdd[
361    'prefix'].order =  IApplicantsContainer['prefix'].order
362IApplicantsContainerAdd[
363    'year'].order =  IApplicantsContainer['year'].order
[15548]364IApplicantsContainerAdd[
365    'container_number'].order =  IApplicantsContainer['year'].order
[6087]366
[13089]367class IApplicantBaseData(IKofaObject):
[13077]368    """This is a base interface of an applicant with no field
[7933]369    required. For use with processors, forms, etc., please use one of
[5753]370    the derived interfaces below, which set more fields to required
371    state, depending on use-case.
372    """
[13080]373    state = Attribute('Application state of an applicant')
[8052]374    history = Attribute('Object history, a list of messages')
375    display_fullname = Attribute('The fullname of an applicant')
[13080]376    application_number = Attribute('The key under which the record is stored')
[13216]377    container_code = Attribute('Code of the parent container plus additional information if record is used or not')
[13080]378    translated_state = Attribute('Real name of the application state')
379    special = Attribute('True if special application')
[13971]380    payments = Attribute('List of payment objects stored in the applicant container')
[13080]381
[8589]382    application_date = Attribute('UTC datetime of submission, used for export only')
[13211]383    password = Attribute('Encrypted password of an applicant')
[8052]384
[13080]385
[8983]386    suspended = schema.Bool(
387        title = _(u'Account suspended'),
388        default = False,
[9035]389        required = False,
[8983]390        )
391
[7240]392    applicant_id = schema.TextLine(
[7708]393        title = _(u'Applicant Id'),
[7240]394        required = False,
[7260]395        readonly = False,
[7240]396        )
[13077]397
[7270]398    reg_number = TextLineChoice(
[8033]399        title = _(u'Registration Number'),
[7263]400        readonly = False,
[16186]401        required = False,
[7263]402        source = contextual_reg_num_source,
[5753]403        )
[13077]404
[5753]405    firstname = schema.TextLine(
[7708]406        title = _(u'First Name'),
[6352]407        required = True,
[5753]408        )
[13077]409
[7356]410    middlename = schema.TextLine(
[7708]411        title = _(u'Middle Name'),
[5753]412        required = False,
413        )
[13077]414
[5753]415    lastname = schema.TextLine(
[7708]416        title = _(u'Last Name (Surname)'),
[6352]417        required = True,
[5753]418        )
[13077]419
[8149]420    date_of_birth = FormattedDate(
[7708]421        title = _(u'Date of Birth'),
[8540]422        required = False,
[8149]423        show_year = True,
[5753]424        )
[13077]425
[5753]426    sex = schema.Choice(
[15790]427        title = _(u'Gender'),
[5753]428        source = GenderSource(),
[6352]429        required = True,
[5753]430        )
[13077]431
[6341]432    email = schema.ASCIILine(
[7708]433        title = _(u'Email Address'),
[8033]434        required = False,
[6343]435        constraint=validate_email,
[5753]436        )
[13077]437
[8176]438    phone = PhoneNumber(
[7708]439        title = _(u'Phone'),
[7331]440        description = u'',
[5753]441        required = False,
442        )
[13077]443
[7262]444    course1 = schema.Choice(
[7708]445        title = _(u'1st Choice Course of Study'),
[8518]446        source = AppCatCertificateSource(),
[13882]447        required = False,
[7262]448        )
[13077]449
[7262]450    course2 = schema.Choice(
[7708]451        title = _(u'2nd Choice Course of Study'),
[8518]452        source = AppCatCertificateSource(),
[7262]453        required = False,
454        )
[13077]455
[8052]456    notice = schema.Text(
457        title = _(u'Notice'),
[5753]458        required = False,
459        )
[8533]460    student_id = schema.TextLine(
461        title = _(u'Student Id'),
462        required = False,
463        readonly = False,
464        )
[6248]465    course_admitted = schema.Choice(
[7708]466        title = _(u'Admitted Course of Study'),
[7347]467        source = CertificateSource(),
[5753]468        required = False,
469        )
[6302]470    locked = schema.Bool(
[7708]471        title = _(u'Form locked'),
[6302]472        default = False,
[10384]473        required = False,
[6302]474        )
[5753]475
[10831]476    special_application = schema.Choice(
477        title = _(u'Special Application'),
478        source = SpecialApplicationSource(),
479        required = False,
480        )
481
[14011]482class IApplicantTestData(IKofaObject):
[14040]483    """This interface is for demonstration and testing only.
[14011]484    It can be omitted in customized versions of Kofa.
485    """
486
487    school_grades = schema.List(
488        title = _(u'School Grades'),
489        value_type = ResultEntryField(),
490        required = False,
491        defaultFactory=list,
492        )
493
494    referees = schema.List(
495        title = _(u'Referees'),
496        value_type = RefereeEntryField(),
497        required = False,
498        defaultFactory=list,
499        )
500
501IApplicantTestData['school_grades'].order = IApplicantBaseData['course2'].order
502
503class IApplicant(IApplicantBaseData, IApplicantTestData):
[13077]504    """This is basically the applicant base data. Here we repeat the
[6195]505    fields from base data if we have to set the `required` attribute
506    to True (which is the default).
[5753]507    """
508
[8742]509    def writeLogMessage(view, comment):
[13167]510        """Add an INFO message to applicants.log.
[8052]511        """
512
[16228]513    def createStudent(view, graduated):
[13104]514        """Create a student object from applicant data and copy
[16228]515        passport image and application slip. If graduated is set True,
516        a graduated student is being created. This method is supposed
517        to be used for transcript applicants. It is tested but not
518        used in the base package.
[8014]519        """
[13089]520class ISpecialApplicant(IKofaObject):
[13077]521    """This reduced interface is for former students or students who are not
[13089]522    users of the portal but have to pay supplementary fees.
[10845]523    This interface is used in browser components only. Thus we can't add
524    fields here to the regular IApplicant interface here. We can
525    only 'customize' fields.
526    """
527
528    suspended = schema.Bool(
529        title = _(u'Account suspended'),
530        default = False,
531        required = False,
532        )
533
[11599]534    locked = schema.Bool(
535        title = _(u'Form locked'),
536        default = False,
537        required = False,
538        )
539
[10845]540    applicant_id = schema.TextLine(
541        title = _(u'Applicant Id'),
542        required = False,
543        readonly = False,
544        )
545
546    firstname = schema.TextLine(
547        title = _(u'First Name'),
548        required = True,
549        )
550
551    middlename = schema.TextLine(
552        title = _(u'Middle Name'),
553        required = False,
554        )
555
556    lastname = schema.TextLine(
557        title = _(u'Last Name (Surname)'),
558        required = True,
559        )
560
561    reg_number = TextLineChoice(
[10846]562        title = _(u'Identification Number'),
[15575]563        #description = _(u'Enter either registration or matriculation number.'),
[10845]564        readonly = False,
565        required = True,
566        source = contextual_reg_num_source,
567        )
568
569    date_of_birth = FormattedDate(
570        title = _(u'Date of Birth'),
571        required = False,
572        #date_format = u'%d/%m/%Y', # Use grok-instance-wide default
573        show_year = True,
574        )
575
576    email = schema.ASCIILine(
577        title = _(u'Email Address'),
578        required = True,
579        constraint=validate_email,
580        )
581
582    phone = PhoneNumber(
583        title = _(u'Phone'),
584        description = u'',
585        required = False,
586        )
587
588    special_application = schema.Choice(
589        title = _(u'Special Application'),
590        source = SpecialApplicationSource(),
591        required = True,
592        )
593
[8016]594class IApplicantEdit(IApplicant):
[13077]595    """This is an applicant interface for editing.
[5753]596
[6339]597    Here we can repeat the fields from base data and set the
598    `required` and `readonly` attributes to True to further restrict
[7262]599    the data access. Or we can allow only certain certificates to be
600    selected by choosing the appropriate source.
601
602    We cannot omit fields here. This has to be done in the
[6339]603    respective form page.
[6195]604    """
[7262]605
[8097]606    email = schema.ASCIILine(
607        title = _(u'Email Address'),
608        required = True,
609        constraint=validate_email,
610        )
[13077]611
[7262]612    course1 = schema.Choice(
[7708]613        title = _(u'1st Choice Course of Study'),
[7347]614        source = AppCatCertificateSource(),
[7262]615        required = True,
616        )
[13077]617
[7262]618    course2 = schema.Choice(
[7708]619        title = _(u'2nd Choice Course of Study'),
[7347]620        source = AppCatCertificateSource(),
[7262]621        required = False,
622        )
[13077]623
[6301]624    course_admitted = schema.Choice(
[7708]625        title = _(u'Admitted Course of Study'),
[7347]626        source = CertificateSource(),
[5753]627        required = False,
[6195]628        readonly = True,
[5753]629        )
[13077]630
[6195]631    notice = schema.Text(
[7708]632        title = _(u'Notice'),
[5753]633        required = False,
634        readonly = True,
635        )
[5758]636
[13089]637IApplicantEdit['email'].order = IApplicantEdit['sex'].order
[8097]638
[7268]639class IApplicantUpdateByRegNo(IApplicant):
[13077]640    """Skip regular reg_number validation if reg_number is used for finding
[7268]641    the applicant object.
642    """
[7270]643    reg_number = schema.TextLine(
[7268]644        title = u'Registration Number',
645        required = False,
646        )
647
[8037]648class IApplicantRegisterUpdate(IApplicant):
[13077]649    """This is a representation of an applicant for first-time registration.
[8778]650    This interface is used when applicants use the registration page to
[8037]651    update their records.
652    """
653    reg_number = schema.TextLine(
654        title = u'Registration Number',
655        required = True,
656        )
657
[11738]658    #firstname = schema.TextLine(
659    #    title = _(u'First Name'),
660    #    required = True,
661    #    )
662
663    lastname = schema.TextLine(
664        title = _(u'Last Name (Surname)'),
[8037]665        required = True,
666        )
667
668    email = schema.ASCIILine(
669        title = _(u'Email Address'),
670        required = True,
671        constraint=validate_email,
672        )
673
[7250]674class IApplicantOnlinePayment(IOnlinePayment):
675    """An applicant payment via payment gateways.
676    """
[8422]677
678    def doAfterApplicantPayment():
679        """Process applicant after payment was made.
680        """
681
[8453]682    def doAfterApplicantPaymentApproval():
683        """Process applicant after payment was approved.
684        """
685
[8422]686    def approveApplicantPayment():
687        """Approve payment and process applicant.
688        """
[13972]689
690class IApplicantRefereeReport(IKofaObject):
691    """A referee report.
692    """
693
694    r_id = Attribute('Report identifier')
[16243]695    email = Attribute('Referee email address taken from the mandate')
[13972]696
[13976]697    creation_date = schema.Datetime(
[16058]698        title = _(u'Report Creation Date'),
[13976]699        readonly = False,
700        required = False,
701        )
702
[13972]703    name = schema.TextLine(
[16058]704        title = _(u'Referee Name'),
[13972]705        required = True,
706        )
707
[16243]708    email_pref = schema.ASCIILine(
709        title = _(u'Preferred Email Address'),
710        required = False,
711        constraint=validate_email,
712        readonly = False,
[13972]713        )
714
715    phone = PhoneNumber(
[16058]716        title = _(u'Referee Phone'),
[13972]717        description = u'',
718        required = False,
719        )
720
721    report = schema.Text(
722        title = _(u'Report'),
723        required = False,
[13975]724        )
Note: See TracBrowser for help on using the repository browser.