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

Last change on this file since 10625 was 10384, checked in by Henrik Bettermann, 12 years ago

'locked' is not required.

  • Property svn:keywords set to Id
File size: 16.8 KB
RevLine 
[5638]1## $Id: interfaces.py 10384 2013-06-25 15:41:53Z 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)
[9217]29from waeup.kofa.browser.interfaces import IApplicantBase
[8149]30from waeup.kofa.schema import TextLineChoice, FormattedDate
[7811]31from waeup.kofa.interfaces import (
[9115]32    IKofaObject, validate_email,
[8033]33    SimpleKofaVocabulary)
[7811]34from waeup.kofa.interfaces import MessageFactory as _
35from waeup.kofa.payments.interfaces import IOnlinePayment
[8176]36from waeup.kofa.schema import PhoneNumber
[8767]37from waeup.kofa.students.vocabularies import GenderSource, RegNumberSource
[8613]38from waeup.kofa.university.vocabularies import AppCatSource, CertificateSource
[5638]39
[7075]40#: Maximum upload size for applicant passport photographs (in bytes)
[7086]41MAX_UPLOAD_SIZE = 1024 * 20
[7075]42
[8611]43_marker = object() # a marker different from None
44
[9115]45def year_range():
46    curr_year = datetime.now().year
47    return range(curr_year - 2, curr_year + 5)
48
[7263]49class RegNumInSource(ValidationError):
50    """Registration number exists already
51    """
52    # The docstring of ValidationErrors is used as error description
53    # by zope.formlib.
54    pass
55
[8767]56class ApplicantRegNumberSource(RegNumberSource):
57    """A source that accepts any reg number if not used already by a
58    different applicant.
59    """
[7263]60    cat_name = 'applicants_catalog'
[7270]61    field_name = 'reg_number'
[7263]62    validation_error = RegNumInSource
[8767]63    comp_field = 'applicant_id'
[7263]64
65def contextual_reg_num_source(context):
[8767]66    source = ApplicantRegNumberSource(context)
[7263]67    return source
68directlyProvides(contextual_reg_num_source, IContextSourceBinder)
69
[7795]70
[7683]71class AppCatCertificateSource(CertificateSource):
[8607]72    """An application certificate source delivers all certificates
73    which belong to a certain application_category.
74
75    This source is meant to be used with Applicants.
76
77    The application category must match the application category of
78    the context parent, normally an applicants container.
[7683]79    """
[8607]80    def contains(self, context, value):
81        context_appcat = getattr(getattr(
[8611]82            context, '__parent__', None), 'application_category', _marker)
83        if context_appcat is _marker:
[8607]84            # If the context (applicant) has no application category,
85            # then it might be not part of a container (yet), for
86            # instance during imports. We consider this correct.
87            return True
88        if value.application_category == context_appcat:
89            return True
90        return False
91
[7683]92    def getValues(self, context):
93        appcat = getattr(getattr(context, '__parent__', None),
94                         'application_category', None)
95        catalog = getUtility(ICatalog, name='certificates_catalog')
96        result = catalog.searchResults(
97            application_category=(appcat,appcat))
[10208]98        resultlist = getUtility(
[10187]99            IApplicantsUtils).filterCertificates(context, result)
[10208]100        return resultlist
[7683]101
[10186]102    def getTitle(self, context, value):
103        return getUtility(
104            IApplicantsUtils).getCertTitle(context, value)
105
[7683]106class ApplicationTypeSource(BasicContextualSourceFactory):
107    """An application type source delivers screening types defined in the
108    portal.
109    """
110    def getValues(self, context):
[7688]111        appcats_dict = getUtility(
[7844]112            IApplicantsUtils).APP_TYPES_DICT
[7688]113        return sorted(appcats_dict.keys())
[7683]114
115    def getToken(self, context, value):
116        return value
117
118    def getTitle(self, context, value):
[7688]119        appcats_dict = getUtility(
[7844]120            IApplicantsUtils).APP_TYPES_DICT
[7688]121        return appcats_dict[value][0]
[7683]122
[8773]123# Maybe FUTMinna still needs this ...
[7683]124#class ApplicationPinSource(BasicContextualSourceFactory):
125#    """An application pin source delivers PIN prefixes for application
126#    defined in the portal.
127#    """
128#    def getValues(self, context):
[7688]129#        apppins_dict = getUtility(
[7844]130#            IApplicantsUtils).APP_TYPES_DICT
[7688]131#        return sorted(appcats_dict.keys())
[7683]132#
133#    def getToken(self, context, value):
134#        return value
135#
136#    def getTitle(self, context, value):
[7688]137#        apppins_dict = getUtility(
[7844]138#            IApplicantsUtils).APP_TYPES_DICT
[7683]139#        return u"%s (%s)" % (
[7688]140#            apppins_dict[value][1],self.apppins_dict[value][0])
[7683]141
[8033]142application_modes_vocab = SimpleKofaVocabulary(
143    (_('Create Application Records'), 'create'),
144    (_('Update Application Records'), 'update'),
145    )
[6075]146
[7682]147class IApplicantsUtils(Interface):
148    """A collection of methods which are subject to customization.
149    """
150
[10184]151    APP_TYPES_DICT = Attribute('dict of application types')
[7844]152
[10184]153    def setPaymentDetails(container, payment):
154        """Set the payment data of an applicant.
155        """
156
157    def getApplicantsStatistics(container):
158        """Count applicants in containers.
159        """
160
[10187]161    def filterCertificates(context, resultset):
[10184]162        """Filter and sort certificates for AppCatCertificateSource.
163        """
164
[10186]165    def getCertTitle(context, value):
166        """Compose the titles in AppCatCertificateSource.
167        """
168
[7819]169class IApplicantsRoot(IKofaObject, IContainer):
[5676]170    """A container for university applicants containers.
[5645]171    """
[5638]172
[8388]173    description = schema.Text(
174        title = _(u'Human readable description in HTML format'),
175        required = False,
176        default = u'''This text can been seen by anonymous users.
177Here we put multi-lingual general information about the application procedure.
178>>de<<
179Dieser Text kann von anonymen Benutzern gelesen werden.
180Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
181        )
182
183    description_dict = Attribute(
184        """Content as language dictionary with values in HTML format.""")
185
[7819]186class IApplicantsContainer(IKofaObject):
[5676]187    """An applicants container contains university applicants.
[5645]188
[5638]189    """
[6069]190
191    code = schema.TextLine(
[7708]192        title = _(u'Code'),
[6069]193        required = True,
194        readonly = True,
[6076]195        )
[6096]196
[6087]197    title = schema.TextLine(
[7708]198        title = _(u'Title'),
[6087]199        required = True,
[8562]200        readonly = False,
[6096]201        )
202
[6087]203    prefix = schema.Choice(
[7708]204        title = _(u'Application Target'),
[6087]205        required = True,
[7683]206        source = ApplicationTypeSource(),
[6087]207        readonly = True,
208        )
[6076]209
[6087]210    year = schema.Choice(
[7708]211        title = _(u'Year of Entrance'),
[6087]212        required = True,
[6158]213        values = year_range(),
[6087]214        readonly = True,
[6096]215        )
[6087]216
[8033]217    mode = schema.Choice(
218        title = _(u'Application Mode'),
219        vocabulary = application_modes_vocab,
220        required = True,
221        )
222
[8773]223    # Maybe FUTMinna still needs this ...
[7376]224    #ac_prefix = schema.Choice(
225    #    title = u'Activation code prefix',
226    #    required = True,
227    #    default = None,
[7683]228    #    source = ApplicationPinSource(),
[7376]229    #    )
[6076]230
[6189]231    application_category = schema.Choice(
[7708]232        title = _(u'Category for the grouping of certificates'),
[6189]233        required = True,
[7681]234        source = AppCatSource(),
[6189]235        )
236
[5645]237    description = schema.Text(
[8365]238        title = _(u'Human readable description in HTML format'),
[5638]239        required = False,
[6518]240        default = u'''This text can been seen by anonymous users.
[8033]241Here we put multi-lingual information about the study courses provided, the application procedure and deadlines.
[7708]242>>de<<
243Dieser Text kann von anonymen Benutzern gelesen werden.
244Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
[5638]245        )
246
[7903]247    description_dict = Attribute(
248        """Content as language dictionary with values in HTML format.""")
[7708]249
[8200]250    startdate = schema.Datetime(
[7708]251        title = _(u'Application Start Date'),
[5638]252        required = False,
[8203]253        description = _('Example:') + u'2011-12-01 18:30:00+01:00',
[5638]254        )
255
[8200]256    enddate = schema.Datetime(
[7708]257        title = _(u'Application Closing Date'),
[5638]258        required = False,
[8203]259        description = _('Example:') + u'2011-12-31 23:59:59+01:00',
[5638]260        )
261
[5645]262    strict_deadline = schema.Bool(
[7708]263        title = _(u'Forbid additions after deadline (enddate)'),
[7984]264        required = False,
[5645]265        default = True,
266        )
[5638]267
[8525]268    application_fee = schema.Float(
269        title = _(u'Application Fee'),
270        default = 0.0,
271        required = False,
272        )
273
[10097]274    hidden= schema.Bool(
275        title = _(u'Hide container'),
276        required = False,
[10098]277        default = False,
[10097]278        )
279
[5638]280    def archive(id=None):
[5676]281        """Create on-dist archive of applicants stored in this term.
[5638]282
[5676]283        If id is `None`, all applicants are archived.
[5638]284
285        If id contains a single id string, only the respective
[5676]286        applicants are archived.
[5638]287
288        If id contains a list of id strings all of the respective
[5676]289        applicants types are saved to disk.
[5638]290        """
291
292    def clear(id=None, archive=True):
[5676]293        """Remove applicants of type given by 'id'.
[5638]294
[5676]295        Optionally archive the applicants.
[6076]296
[5676]297        If id is `None`, all applicants are archived.
[5638]298
299        If id contains a single id string, only the respective
[5676]300        applicants are archived.
[5638]301
302        If id contains a list of id strings all of the respective
[5676]303        applicant types are saved to disk.
[5638]304
305        If `archive` is ``False`` none of the archive-handling is done
[5676]306        and respective applicants are simply removed from the
[5638]307        database.
308        """
[6073]309
[9531]310    def writeLogMessage(view, comment):
311        """Adds an INFO message to the log file
312        """
313
[6069]314class IApplicantsContainerAdd(IApplicantsContainer):
315    """An applicants container contains university applicants.
316    """
[6087]317    prefix = schema.Choice(
[7708]318        title = _(u'Application Target'),
[6069]319        required = True,
[7683]320        source = ApplicationTypeSource(),
[6069]321        readonly = False,
[6076]322        )
[6073]323
[6087]324    year = schema.Choice(
[7708]325        title = _(u'Year of Entrance'),
[6087]326        required = True,
[6158]327        values = year_range(),
[6087]328        readonly = False,
[6096]329        )
[6073]330
[6096]331IApplicantsContainerAdd[
332    'prefix'].order =  IApplicantsContainer['prefix'].order
333IApplicantsContainerAdd[
334    'year'].order =  IApplicantsContainer['year'].order
[6087]335
[9217]336class IApplicantBaseData(IApplicantBase):
[5753]337    """The data for an applicant.
338
[7240]339    This is a base interface with no field
[7933]340    required. For use with processors, forms, etc., please use one of
[5753]341    the derived interfaces below, which set more fields to required
342    state, depending on use-case.
343    """
[8052]344
345    history = Attribute('Object history, a list of messages')
[8286]346    state = Attribute('The application state of an applicant')
[8052]347    display_fullname = Attribute('The fullname of an applicant')
[8589]348    application_date = Attribute('UTC datetime of submission, used for export only')
[8052]349    password = Attribute('Encrypted password of a applicant')
350    application_number = Attribute('The key under which the record is stored')
351
[8983]352    suspended = schema.Bool(
353        title = _(u'Account suspended'),
354        default = False,
[9035]355        required = False,
[8983]356        )
357
[7240]358    applicant_id = schema.TextLine(
[7708]359        title = _(u'Applicant Id'),
[7240]360        required = False,
[7260]361        readonly = False,
[7240]362        )
[7270]363    reg_number = TextLineChoice(
[8033]364        title = _(u'Registration Number'),
[7263]365        readonly = False,
366        required = True,
367        source = contextual_reg_num_source,
[5753]368        )
[7376]369    #access_code = schema.TextLine(
370    #    title = u'Activation Code',
371    #    required = False,
372    #    readonly = True,
373    #    )
[5753]374    firstname = schema.TextLine(
[7708]375        title = _(u'First Name'),
[6352]376        required = True,
[5753]377        )
[7356]378    middlename = schema.TextLine(
[7708]379        title = _(u'Middle Name'),
[5753]380        required = False,
381        )
382    lastname = schema.TextLine(
[7708]383        title = _(u'Last Name (Surname)'),
[6352]384        required = True,
[5753]385        )
[8149]386    date_of_birth = FormattedDate(
[7708]387        title = _(u'Date of Birth'),
[8540]388        required = False,
[8154]389        #date_format = u'%d/%m/%Y', # Use grok-instance-wide default
[8149]390        show_year = True,
[5753]391        )
392    sex = schema.Choice(
[7708]393        title = _(u'Sex'),
[5753]394        source = GenderSource(),
[6352]395        required = True,
[5753]396        )
[6341]397    email = schema.ASCIILine(
[7708]398        title = _(u'Email Address'),
[8033]399        required = False,
[6343]400        constraint=validate_email,
[5753]401        )
[8176]402    phone = PhoneNumber(
[7708]403        title = _(u'Phone'),
[7331]404        description = u'',
[5753]405        required = False,
406        )
[7262]407    course1 = schema.Choice(
[7708]408        title = _(u'1st Choice Course of Study'),
[8518]409        source = AppCatCertificateSource(),
[7262]410        required = True,
411        )
412    course2 = schema.Choice(
[7708]413        title = _(u'2nd Choice Course of Study'),
[8518]414        source = AppCatCertificateSource(),
[7262]415        required = False,
416        )
[8044]417    #school_grades = schema.List(
418    #    title = _(u'School Grades'),
419    #    value_type = ResultEntryField(),
420    #    required = False,
421    #    default = [],
422    #    )
[6322]423
[8052]424    notice = schema.Text(
425        title = _(u'Notice'),
[5753]426        required = False,
427        )
[8533]428    student_id = schema.TextLine(
429        title = _(u'Student Id'),
430        required = False,
431        readonly = False,
432        )
[6248]433    course_admitted = schema.Choice(
[7708]434        title = _(u'Admitted Course of Study'),
[7347]435        source = CertificateSource(),
[5753]436        required = False,
437        )
[6302]438    locked = schema.Bool(
[7708]439        title = _(u'Form locked'),
[6302]440        default = False,
[10384]441        required = False,
[6302]442        )
[5753]443
[8052]444class IApplicant(IApplicantBaseData):
[5753]445    """An applicant.
446
447    This is basically the applicant base data. Here we repeat the
[6195]448    fields from base data if we have to set the `required` attribute
449    to True (which is the default).
[5753]450    """
451
[8742]452    def writeLogMessage(view, comment):
[8052]453        """Adds an INFO message to the log file
454        """
455
[8014]456    def createStudent():
457        """Create a student object from applicatnt data
458        and copy applicant object.
459        """
460
[8016]461class IApplicantEdit(IApplicant):
[7867]462    """An applicant interface for editing.
[5753]463
[6339]464    Here we can repeat the fields from base data and set the
465    `required` and `readonly` attributes to True to further restrict
[7262]466    the data access. Or we can allow only certain certificates to be
467    selected by choosing the appropriate source.
468
469    We cannot omit fields here. This has to be done in the
[6339]470    respective form page.
[6195]471    """
[7262]472
[8097]473    email = schema.ASCIILine(
474        title = _(u'Email Address'),
475        required = True,
476        constraint=validate_email,
477        )
[7262]478    course1 = schema.Choice(
[7708]479        title = _(u'1st Choice Course of Study'),
[7347]480        source = AppCatCertificateSource(),
[7262]481        required = True,
482        )
483    course2 = schema.Choice(
[7708]484        title = _(u'2nd Choice Course of Study'),
[7347]485        source = AppCatCertificateSource(),
[7262]486        required = False,
487        )
[6301]488    course_admitted = schema.Choice(
[7708]489        title = _(u'Admitted Course of Study'),
[7347]490        source = CertificateSource(),
[5753]491        required = False,
[6195]492        readonly = True,
[5753]493        )
[6195]494    notice = schema.Text(
[7708]495        title = _(u'Notice'),
[5753]496        required = False,
497        readonly = True,
498        )
[5758]499
[8097]500IApplicantEdit['email'].order = IApplicantEdit[
501    'sex'].order
502
[7268]503class IApplicantUpdateByRegNo(IApplicant):
504    """Representation of an applicant.
505
[7270]506    Skip regular reg_number validation if reg_number is used for finding
[7268]507    the applicant object.
508    """
[7270]509    reg_number = schema.TextLine(
[7268]510        title = u'Registration Number',
511        required = False,
512        )
513
[8037]514class IApplicantRegisterUpdate(IApplicant):
515    """Representation of an applicant for first-time registration.
516
[8778]517    This interface is used when applicants use the registration page to
[8037]518    update their records.
519    """
520    reg_number = schema.TextLine(
521        title = u'Registration Number',
522        required = True,
523        )
524
525    firstname = schema.TextLine(
526        title = _(u'First Name'),
527        required = True,
528        )
529
530    email = schema.ASCIILine(
531        title = _(u'Email Address'),
532        required = True,
533        constraint=validate_email,
534        )
535
[7250]536class IApplicantOnlinePayment(IOnlinePayment):
537    """An applicant payment via payment gateways.
538
539    """
[8422]540
541    def doAfterApplicantPayment():
542        """Process applicant after payment was made.
543
544        """
545
[8453]546    def doAfterApplicantPaymentApproval():
547        """Process applicant after payment was approved.
548
549        """
550
[8422]551    def approveApplicantPayment():
552        """Approve payment and process applicant.
553
554        """
Note: See TracBrowser for help on using the repository browser.