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

Last change on this file since 16995 was 16976, checked in by Henrik Bettermann, 3 years ago

Send email after final submission of application form.

  • Property svn:keywords set to Id
File size: 22.0 KB
RevLine 
[5638]1## $Id: interfaces.py 16976 2022-06-29 10:24:59Z 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
[16746]46    return range(curr_year - 6, 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
[16976]321    send_email= schema.Bool(
322        title = _(u'Send email after submission'),
323        required = False,
324        default = False,
325        )
326
[13077]327    def addApplicant(applicant):
328        """Add an applicant.
[5638]329        """
330
[13077]331    def writeLogMessage(view, comment):
[13167]332        """Add an INFO message to applicants.log.
[5638]333        """
[6073]334
[13077]335    def traverse(name):
336        """Deliver appropriate containers.
[9531]337        """
338
[6069]339class IApplicantsContainerAdd(IApplicantsContainer):
340    """An applicants container contains university applicants.
341    """
[6087]342    prefix = schema.Choice(
[7708]343        title = _(u'Application Target'),
[6069]344        required = True,
[7683]345        source = ApplicationTypeSource(),
[6069]346        readonly = False,
[6076]347        )
[6073]348
[6087]349    year = schema.Choice(
[7708]350        title = _(u'Year of Entrance'),
[16190]351        required = False,
[6158]352        values = year_range(),
[6087]353        readonly = False,
[6096]354        )
[6073]355
[15548]356    container_number = schema.Choice(
357        title = _(u'Container Number'),
[15552]358        values = range(1,100),
[15548]359        description = _(u'If set, this number will be added to the container '
360                         'prefix (e.g. app3). If not set, the year of entrance will be '
361                         'used (e.g. app2019).'),
362        required = False,
363        readonly = False,
364        )
365
[6096]366IApplicantsContainerAdd[
367    'prefix'].order =  IApplicantsContainer['prefix'].order
368IApplicantsContainerAdd[
369    'year'].order =  IApplicantsContainer['year'].order
[15548]370IApplicantsContainerAdd[
371    'container_number'].order =  IApplicantsContainer['year'].order
[6087]372
[13089]373class IApplicantBaseData(IKofaObject):
[13077]374    """This is a base interface of an applicant with no field
[7933]375    required. For use with processors, forms, etc., please use one of
[5753]376    the derived interfaces below, which set more fields to required
377    state, depending on use-case.
378    """
[13080]379    state = Attribute('Application state of an applicant')
[8052]380    history = Attribute('Object history, a list of messages')
381    display_fullname = Attribute('The fullname of an applicant')
[13080]382    application_number = Attribute('The key under which the record is stored')
[13216]383    container_code = Attribute('Code of the parent container plus additional information if record is used or not')
[13080]384    translated_state = Attribute('Real name of the application state')
385    special = Attribute('True if special application')
[13971]386    payments = Attribute('List of payment objects stored in the applicant container')
[13080]387
[8589]388    application_date = Attribute('UTC datetime of submission, used for export only')
[13211]389    password = Attribute('Encrypted password of an applicant')
[8052]390
[13080]391
[8983]392    suspended = schema.Bool(
393        title = _(u'Account suspended'),
394        default = False,
[9035]395        required = False,
[8983]396        )
397
[7240]398    applicant_id = schema.TextLine(
[7708]399        title = _(u'Applicant Id'),
[7240]400        required = False,
[7260]401        readonly = False,
[7240]402        )
[13077]403
[7270]404    reg_number = TextLineChoice(
[8033]405        title = _(u'Registration Number'),
[7263]406        readonly = False,
[16186]407        required = False,
[7263]408        source = contextual_reg_num_source,
[5753]409        )
[13077]410
[5753]411    firstname = schema.TextLine(
[7708]412        title = _(u'First Name'),
[6352]413        required = True,
[5753]414        )
[13077]415
[7356]416    middlename = schema.TextLine(
[7708]417        title = _(u'Middle Name'),
[5753]418        required = False,
419        )
[13077]420
[5753]421    lastname = schema.TextLine(
[7708]422        title = _(u'Last Name (Surname)'),
[6352]423        required = True,
[5753]424        )
[13077]425
[8149]426    date_of_birth = FormattedDate(
[7708]427        title = _(u'Date of Birth'),
[8540]428        required = False,
[8149]429        show_year = True,
[5753]430        )
[13077]431
[5753]432    sex = schema.Choice(
[15790]433        title = _(u'Gender'),
[5753]434        source = GenderSource(),
[6352]435        required = True,
[5753]436        )
[13077]437
[6341]438    email = schema.ASCIILine(
[7708]439        title = _(u'Email Address'),
[8033]440        required = False,
[6343]441        constraint=validate_email,
[5753]442        )
[13077]443
[8176]444    phone = PhoneNumber(
[7708]445        title = _(u'Phone'),
[7331]446        description = u'',
[5753]447        required = False,
448        )
[13077]449
[7262]450    course1 = schema.Choice(
[7708]451        title = _(u'1st Choice Course of Study'),
[8518]452        source = AppCatCertificateSource(),
[13882]453        required = False,
[7262]454        )
[13077]455
[7262]456    course2 = schema.Choice(
[7708]457        title = _(u'2nd Choice Course of Study'),
[8518]458        source = AppCatCertificateSource(),
[7262]459        required = False,
460        )
[13077]461
[8052]462    notice = schema.Text(
463        title = _(u'Notice'),
[5753]464        required = False,
465        )
[8533]466    student_id = schema.TextLine(
467        title = _(u'Student Id'),
468        required = False,
469        readonly = False,
470        )
[6248]471    course_admitted = schema.Choice(
[7708]472        title = _(u'Admitted Course of Study'),
[7347]473        source = CertificateSource(),
[5753]474        required = False,
475        )
[6302]476    locked = schema.Bool(
[7708]477        title = _(u'Form locked'),
[6302]478        default = False,
[10384]479        required = False,
[6302]480        )
[5753]481
[10831]482    special_application = schema.Choice(
483        title = _(u'Special Application'),
484        source = SpecialApplicationSource(),
485        required = False,
486        )
487
[14011]488class IApplicantTestData(IKofaObject):
[14040]489    """This interface is for demonstration and testing only.
[14011]490    It can be omitted in customized versions of Kofa.
491    """
492
493    school_grades = schema.List(
494        title = _(u'School Grades'),
495        value_type = ResultEntryField(),
496        required = False,
497        defaultFactory=list,
498        )
499
500    referees = schema.List(
501        title = _(u'Referees'),
502        value_type = RefereeEntryField(),
503        required = False,
504        defaultFactory=list,
505        )
506
507IApplicantTestData['school_grades'].order = IApplicantBaseData['course2'].order
508
509class IApplicant(IApplicantBaseData, IApplicantTestData):
[13077]510    """This is basically the applicant base data. Here we repeat the
[6195]511    fields from base data if we have to set the `required` attribute
512    to True (which is the default).
[5753]513    """
514
[8742]515    def writeLogMessage(view, comment):
[13167]516        """Add an INFO message to applicants.log.
[8052]517        """
518
[16228]519    def createStudent(view, graduated):
[13104]520        """Create a student object from applicant data and copy
[16228]521        passport image and application slip. If graduated is set True,
522        a graduated student is being created. This method is supposed
523        to be used for transcript applicants. It is tested but not
524        used in the base package.
[8014]525        """
[13089]526class ISpecialApplicant(IKofaObject):
[13077]527    """This reduced interface is for former students or students who are not
[13089]528    users of the portal but have to pay supplementary fees.
[10845]529    This interface is used in browser components only. Thus we can't add
530    fields here to the regular IApplicant interface here. We can
531    only 'customize' fields.
532    """
533
534    suspended = schema.Bool(
535        title = _(u'Account suspended'),
536        default = False,
537        required = False,
538        )
539
[11599]540    locked = schema.Bool(
541        title = _(u'Form locked'),
542        default = False,
543        required = False,
544        )
545
[10845]546    applicant_id = schema.TextLine(
547        title = _(u'Applicant Id'),
548        required = False,
549        readonly = False,
550        )
551
552    firstname = schema.TextLine(
553        title = _(u'First Name'),
554        required = True,
555        )
556
557    middlename = schema.TextLine(
558        title = _(u'Middle Name'),
559        required = False,
560        )
561
562    lastname = schema.TextLine(
563        title = _(u'Last Name (Surname)'),
564        required = True,
565        )
566
567    reg_number = TextLineChoice(
[10846]568        title = _(u'Identification Number'),
[15575]569        #description = _(u'Enter either registration or matriculation number.'),
[10845]570        readonly = False,
571        required = True,
572        source = contextual_reg_num_source,
573        )
574
575    date_of_birth = FormattedDate(
576        title = _(u'Date of Birth'),
577        required = False,
578        #date_format = u'%d/%m/%Y', # Use grok-instance-wide default
579        show_year = True,
580        )
581
582    email = schema.ASCIILine(
583        title = _(u'Email Address'),
584        required = True,
585        constraint=validate_email,
586        )
587
588    phone = PhoneNumber(
589        title = _(u'Phone'),
590        description = u'',
591        required = False,
592        )
593
594    special_application = schema.Choice(
595        title = _(u'Special Application'),
596        source = SpecialApplicationSource(),
597        required = True,
598        )
599
[8016]600class IApplicantEdit(IApplicant):
[13077]601    """This is an applicant interface for editing.
[5753]602
[6339]603    Here we can repeat the fields from base data and set the
604    `required` and `readonly` attributes to True to further restrict
[7262]605    the data access. Or we can allow only certain certificates to be
606    selected by choosing the appropriate source.
607
608    We cannot omit fields here. This has to be done in the
[6339]609    respective form page.
[6195]610    """
[7262]611
[8097]612    email = schema.ASCIILine(
613        title = _(u'Email Address'),
614        required = True,
615        constraint=validate_email,
616        )
[13077]617
[7262]618    course1 = schema.Choice(
[7708]619        title = _(u'1st Choice Course of Study'),
[7347]620        source = AppCatCertificateSource(),
[7262]621        required = True,
622        )
[13077]623
[7262]624    course2 = schema.Choice(
[7708]625        title = _(u'2nd Choice Course of Study'),
[7347]626        source = AppCatCertificateSource(),
[7262]627        required = False,
628        )
[13077]629
[6301]630    course_admitted = schema.Choice(
[7708]631        title = _(u'Admitted Course of Study'),
[7347]632        source = CertificateSource(),
[5753]633        required = False,
[6195]634        readonly = True,
[5753]635        )
[13077]636
[6195]637    notice = schema.Text(
[7708]638        title = _(u'Notice'),
[5753]639        required = False,
640        readonly = True,
641        )
[5758]642
[13089]643IApplicantEdit['email'].order = IApplicantEdit['sex'].order
[8097]644
[7268]645class IApplicantUpdateByRegNo(IApplicant):
[13077]646    """Skip regular reg_number validation if reg_number is used for finding
[7268]647    the applicant object.
648    """
[7270]649    reg_number = schema.TextLine(
[7268]650        title = u'Registration Number',
651        required = False,
652        )
653
[8037]654class IApplicantRegisterUpdate(IApplicant):
[13077]655    """This is a representation of an applicant for first-time registration.
[8778]656    This interface is used when applicants use the registration page to
[8037]657    update their records.
658    """
659    reg_number = schema.TextLine(
660        title = u'Registration Number',
661        required = True,
662        )
663
[11738]664    #firstname = schema.TextLine(
665    #    title = _(u'First Name'),
666    #    required = True,
667    #    )
668
669    lastname = schema.TextLine(
670        title = _(u'Last Name (Surname)'),
[8037]671        required = True,
672        )
673
674    email = schema.ASCIILine(
675        title = _(u'Email Address'),
676        required = True,
677        constraint=validate_email,
678        )
679
[7250]680class IApplicantOnlinePayment(IOnlinePayment):
681    """An applicant payment via payment gateways.
682    """
[8422]683
684    def doAfterApplicantPayment():
685        """Process applicant after payment was made.
686        """
687
[8453]688    def doAfterApplicantPaymentApproval():
689        """Process applicant after payment was approved.
690        """
691
[8422]692    def approveApplicantPayment():
693        """Approve payment and process applicant.
694        """
[13972]695
696class IApplicantRefereeReport(IKofaObject):
697    """A referee report.
698    """
699
700    r_id = Attribute('Report identifier')
[16243]701    email = Attribute('Referee email address taken from the mandate')
[13972]702
[13976]703    creation_date = schema.Datetime(
[16058]704        title = _(u'Report Creation Date'),
[13976]705        readonly = False,
706        required = False,
707        )
708
[13972]709    name = schema.TextLine(
[16058]710        title = _(u'Referee Name'),
[13972]711        required = True,
712        )
713
[16243]714    email_pref = schema.ASCIILine(
715        title = _(u'Preferred Email Address'),
716        required = False,
717        constraint=validate_email,
718        readonly = False,
[13972]719        )
720
721    phone = PhoneNumber(
[16058]722        title = _(u'Referee Phone'),
[13972]723        description = u'',
724        required = False,
725        )
726
727    report = schema.Text(
728        title = _(u'Report'),
729        required = False,
[13975]730        )
Note: See TracBrowser for help on using the repository browser.