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

Last change on this file since 11107 was 7795, checked in by uli, 13 years ago

Merge changes from ulif-schoolgrades back into trunk.

  • Property svn:keywords set to Id
File size: 19.6 KB
RevLine 
[5638]1## $Id: interfaces.py 7795 2012-03-08 03:30:30Z uli $
[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"""
[5866]20from grokcore.content.interfaces import IContainer
[7795]21from zc.sourcefactory.basic import BasicSourceFactory
22from zc.sourcefactory.contextual import BasicContextualSourceFactory
[5638]23from zope import schema
[7683]24from zope.component import getUtilitiesFor, 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)
[7263]29from waeup.sirp.schema import TextLineChoice
[7250]30from waeup.sirp.interfaces import (
[7321]31    ISIRPObject, year_range, validate_email, academic_sessions_vocab)
[7708]32from waeup.sirp.interfaces import MessageFactory as _
[7795]33from waeup.sirp.payments.interfaces import IOnlinePayment
34from waeup.sirp.schoolgrades import ResultEntryField
35from waeup.sirp.students.vocabularies import (
36    lgas_vocab, CertificateSource, GenderSource)
[7436]37from waeup.sirp.university.vocabularies import (
[7681]38    course_levels, AppCatSource)
[5638]39
[7075]40#: Maximum upload size for applicant passport photographs (in bytes)
[7086]41MAX_UPLOAD_SIZE = 1024 * 20
[7075]42
[7263]43class RegNumInSource(ValidationError):
44    """Registration number exists already
45    """
46    # The docstring of ValidationErrors is used as error description
47    # by zope.formlib.
48    pass
49
50class RegNumberSource(object):
51    implements(ISource)
52    cat_name = 'applicants_catalog'
[7270]53    field_name = 'reg_number'
[7263]54    validation_error = RegNumInSource
55    def __init__(self, context):
56        self.context = context
57        return
58
59    def __contains__(self, value):
60        cat = queryUtility(ICatalog, self.cat_name)
61        if cat is None:
62            return True
63        kw = {self.field_name: (value, value)}
64        results = cat.searchResults(**kw)
65        for entry in results:
66            if entry.applicant_id != self.context.applicant_id:
67                # XXX: sources should simply return False.
68                #      But then we get some stupid error message in forms
69                #      when validation fails.
70                raise self.validation_error(value)
71                #return False
72        return True
73
74def contextual_reg_num_source(context):
75    source = RegNumberSource(context)
76    return source
77directlyProvides(contextual_reg_num_source, IContextSourceBinder)
78
[7795]79
[7683]80class AppCatCertificateSource(CertificateSource):
81    """An application certificate source delivers all courses which belong to
82    a certain application_category.
83    """
84    def getValues(self, context):
85        # appliction category not available when certificate was deleted.
86        # shouldn't that info be part of applicant info instead?
87        # when we cannot determine the appcat, we will display all courses.
88        appcat = getattr(getattr(context, '__parent__', None),
89                         'application_category', None)
90        catalog = getUtility(ICatalog, name='certificates_catalog')
91        result = catalog.searchResults(
92            application_category=(appcat,appcat))
93        result = sorted(result, key=lambda value: value.code)
94        curr_course = context.course1
95        if curr_course is not None and curr_course not in result:
96            # display also current course even if it is not catalogued
97            # (any more)
98            result = [curr_course,] + result
99        return result
100
101class ApplicationTypeSource(BasicContextualSourceFactory):
102    """An application type source delivers screening types defined in the
103    portal.
104    """
105    def getValues(self, context):
[7688]106        appcats_dict = getUtility(
[7683]107            IApplicantsUtils).getApplicationTypeDict()
[7688]108        return sorted(appcats_dict.keys())
[7683]109
110    def getToken(self, context, value):
111        return value
112
113    def getTitle(self, context, value):
[7688]114        appcats_dict = getUtility(
115            IApplicantsUtils).getApplicationTypeDict()
116        return appcats_dict[value][0]
[7683]117
118# Maybe Uniben still needs this ...
119#class ApplicationPinSource(BasicContextualSourceFactory):
120#    """An application pin source delivers PIN prefixes for application
121#    defined in the portal.
122#    """
123#    def getValues(self, context):
[7688]124#        apppins_dict = getUtility(
[7683]125#            IApplicantsUtils).getApplicationTypeDict()
[7688]126#        return sorted(appcats_dict.keys())
[7683]127#
128#    def getToken(self, context, value):
129#        return value
130#
131#    def getTitle(self, context, value):
[7688]132#        apppins_dict = getUtility(
133#            IApplicantsUtils).getApplicationTypeDict()
[7683]134#        return u"%s (%s)" % (
[7688]135#            apppins_dict[value][1],self.apppins_dict[value][0])
[7683]136
[6069]137class ApplicantContainerProviderSource(BasicSourceFactory):
[6075]138    """A source offering all available applicants container types.
139
140    The values returned by this source are names of utilities that can
141    create :class:`ApplicantContainer` instances. So, if you get a
142    name like ``'myactype'`` from this source, then you can do:
143
144      >>> from zope.component import getUtility
145      >>> p = getUtility(IApplicantsContainerProvider, name=myactype)
146      >>> my_applicants_container = p.factory()
147
148    Or you can access class-attributes like
149
150      >>> my_applicants_container.container_title
151      'Pretty'
[6076]152
[6069]153    """
154    def getValues(self):
[6075]155        """Returns a list of ``(<name>, <provider>)`` tuples.
[5753]156
[6075]157        Here ``<name>`` is the name under which an
158        :class:``ApplicantContainerProvider`` was registered as a
159        utility and ``<provider>`` is the utility itself.
160        """
161        return getUtilitiesFor(IApplicantsContainerProvider)
162
[6069]163    def getToken(self, value):
[6075]164        """Return the name of the ``(<name>, <provider>)`` tuple.
165        """
166        return value[0]
167
[6069]168    def getTitle(self, value):
[6075]169        """Get a 'title - description' string for a container type.
170        """
171        factory = value[1].factory
172        return "%s - %s" % (
173            factory.container_title, factory.container_description)
[6069]174
[7682]175class IApplicantsUtils(Interface):
176    """A collection of methods which are subject to customization.
177    """
[7683]178    pass
[7682]179
[7321]180class IApplicantsRoot(ISIRPObject, IContainer):
[5676]181    """A container for university applicants containers.
[5645]182    """
[5866]183    pass
[5638]184
[7321]185class IApplicantsContainer(ISIRPObject):
[5676]186    """An applicants container contains university applicants.
[5645]187
[5638]188    """
[6069]189
[6075]190    container_title = Attribute(
191        u'classattribute: title for type of container')
192    container_description = Attribute(
193        u'classattribute: description for type of container')
194
[6076]195
[6069]196    code = schema.TextLine(
[7708]197        title = _(u'Code'),
[6087]198        default = u'-',
[6069]199        required = True,
200        readonly = True,
[6076]201        )
[6096]202
[6087]203    title = schema.TextLine(
[7708]204        title = _(u'Title'),
[6087]205        required = True,
206        default = u'-',
207        readonly = True,
[6096]208        )
209
[6087]210    prefix = schema.Choice(
[7708]211        title = _(u'Application Target'),
[6087]212        required = True,
213        default = None,
[7683]214        source = ApplicationTypeSource(),
[6087]215        readonly = True,
216        )
[6076]217
[7436]218    entry_level = schema.Choice(
[7708]219        title = _(u'Entry Level'),
[7436]220        vocabulary = course_levels,
221        default = 100,
222        required = True,
223        )
224
[6087]225    year = schema.Choice(
[7708]226        title = _(u'Year of Entrance'),
[6087]227        required = True,
228        default = None,
[6158]229        values = year_range(),
[6087]230        readonly = True,
[6096]231        )
[6087]232
[6069]233    provider = schema.Choice(
[7708]234        title = _(u'Applicants Container Type'),
[6069]235        required = True,
236        default = None,
237        source = ApplicantContainerProviderSource(),
[6087]238        readonly = True,
[6076]239        )
[6158]240
[7683]241    # Maybe Uniben still needs this ...
[7376]242    #ac_prefix = schema.Choice(
243    #    title = u'Activation code prefix',
244    #    required = True,
245    #    default = None,
[7683]246    #    source = ApplicationPinSource(),
[7376]247    #    )
[6076]248
[6189]249    application_category = schema.Choice(
[7708]250        title = _(u'Category for the grouping of certificates'),
[6189]251        required = True,
252        default = None,
[7681]253        source = AppCatSource(),
[6189]254        )
255
[5645]256    description = schema.Text(
[7708]257        title = _(u'Human readable description in reST format'),
[5638]258        required = False,
[6518]259        default = u'''This text can been seen by anonymous users.
[7708]260Here we put mult-lingual information about the study courses provided, the application procedure and deadlines.
261>>de<<
262Dieser Text kann von anonymen Benutzern gelesen werden.
263Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
[5638]264        )
265
[7708]266    description_dict = schema.Dict(
267        title = u'Content as language dictionary with values in html format',
268        required = False,
269        default = {},
270        )
271
[5638]272    startdate = schema.Date(
[7708]273        title = _(u'Application Start Date'),
[5638]274        required = False,
275        default = None,
276        )
277
278    enddate = schema.Date(
[7708]279        title = _(u'Application Closing Date'),
[5638]280        required = False,
281        default = None,
282        )
283
[5645]284    strict_deadline = schema.Bool(
[7708]285        title = _(u'Forbid additions after deadline (enddate)'),
[5645]286        required = True,
287        default = True,
288        )
[5638]289
290    def archive(id=None):
[5676]291        """Create on-dist archive of applicants stored in this term.
[5638]292
[5676]293        If id is `None`, all applicants are archived.
[5638]294
295        If id contains a single id string, only the respective
[5676]296        applicants are archived.
[5638]297
298        If id contains a list of id strings all of the respective
[5676]299        applicants types are saved to disk.
[5638]300        """
301
302    def clear(id=None, archive=True):
[5676]303        """Remove applicants of type given by 'id'.
[5638]304
[5676]305        Optionally archive the applicants.
[6076]306
[5676]307        If id is `None`, all applicants are archived.
[5638]308
309        If id contains a single id string, only the respective
[5676]310        applicants are archived.
[5638]311
312        If id contains a list of id strings all of the respective
[5676]313        applicant types are saved to disk.
[5638]314
315        If `archive` is ``False`` none of the archive-handling is done
[5676]316        and respective applicants are simply removed from the
[5638]317        database.
318        """
[6073]319
[6069]320class IApplicantsContainerAdd(IApplicantsContainer):
321    """An applicants container contains university applicants.
322    """
[6087]323    prefix = schema.Choice(
[7708]324        title = _(u'Application Target'),
[6069]325        required = True,
[6087]326        default = None,
[7683]327        source = ApplicationTypeSource(),
[6069]328        readonly = False,
[6076]329        )
[6073]330
[6087]331    year = schema.Choice(
[7708]332        title = _(u'Year of Entrance'),
[6087]333        required = True,
334        default = None,
[6158]335        values = year_range(),
[6087]336        readonly = False,
[6096]337        )
[6073]338
[6087]339    provider = schema.Choice(
[7708]340        title = _(u'Applicants Container Type'),
[6087]341        required = True,
342        default = None,
343        source = ApplicantContainerProviderSource(),
344        readonly = False,
345        )
346
[6096]347IApplicantsContainerAdd[
348    'prefix'].order =  IApplicantsContainer['prefix'].order
349IApplicantsContainerAdd[
350    'year'].order =  IApplicantsContainer['year'].order
351IApplicantsContainerAdd[
352    'provider'].order =  IApplicantsContainer['provider'].order
[6087]353
[7321]354class IApplicantBaseData(ISIRPObject):
[5753]355    """The data for an applicant.
356
[7240]357    This is a base interface with no field
[5753]358    required. For use with importers, forms, etc., please use one of
359    the derived interfaces below, which set more fields to required
360    state, depending on use-case.
[7338]361
[7795]362    This base interface is also implemented by the
363    :class:`waeup.sirp.students.StudentApplication` class in the
364    students package. Thus, these are the data which are saved after
365    admission.
[5753]366    """
[7240]367    applicant_id = schema.TextLine(
[7708]368        title = _(u'Applicant Id'),
[7240]369        required = False,
[7260]370        readonly = False,
[7240]371        )
[7270]372    reg_number = TextLineChoice(
[7708]373        title = _(u'JAMB Registration Number'),
[7263]374        readonly = False,
375        required = True,
376        default = None,
377        source = contextual_reg_num_source,
[5753]378        )
[7376]379    #access_code = schema.TextLine(
380    #    title = u'Activation Code',
381    #    required = False,
382    #    readonly = True,
383    #    )
[5753]384    firstname = schema.TextLine(
[7708]385        title = _(u'First Name'),
[6352]386        required = True,
[5753]387        )
[7356]388    middlename = schema.TextLine(
[7708]389        title = _(u'Middle Name'),
[5753]390        required = False,
391        )
392    lastname = schema.TextLine(
[7708]393        title = _(u'Last Name (Surname)'),
[6352]394        required = True,
[5753]395        )
396    date_of_birth = schema.Date(
[7708]397        title = _(u'Date of Birth'),
[6352]398        required = True,
[5753]399        )
[6249]400    lga = schema.Choice(
401        source = lgas_vocab,
[7708]402        title = _(u'State/LGA'),
[6254]403        default = 'foreigner',
404        required = True,
[5753]405        )
406    sex = schema.Choice(
[7708]407        title = _(u'Sex'),
[5753]408        source = GenderSource(),
409        default = u'm',
[6352]410        required = True,
[5753]411        )
[6341]412    email = schema.ASCIILine(
[7708]413        title = _(u'Email Address'),
[7240]414        required = True,
[6343]415        constraint=validate_email,
[5753]416        )
[7324]417    phone = schema.TextLine(
[7708]418        title = _(u'Phone'),
[7331]419        description = u'',
[5753]420        required = False,
421        )
[7262]422    course1 = schema.Choice(
[7708]423        title = _(u'1st Choice Course of Study'),
[7347]424        source = CertificateSource(),
[7262]425        required = True,
426        )
427    course2 = schema.Choice(
[7708]428        title = _(u'2nd Choice Course of Study'),
[7347]429        source = CertificateSource(),
[7262]430        required = False,
431        )
[7795]432    school_grades = schema.List(
433        title = _(u'School Grades'),
434        value_type = ResultEntryField(),
435        required = True,
436        default = [],
437        )
[6322]438
[5753]439    #
[7338]440    # Data to be imported after screening
[5753]441    #
[6255]442    screening_score = schema.Int(
[7708]443        title = _(u'Screening Score'),
[5753]444        required = False,
445        )
446    screening_venue = schema.TextLine(
[7708]447        title = _(u'Screening Venue'),
[5753]448        required = False,
449        )
[6248]450    course_admitted = schema.Choice(
[7708]451        title = _(u'Admitted Course of Study'),
[7347]452        source = CertificateSource(),
[6254]453        default = None,
[5753]454        required = False,
455        )
[7347]456    notice = schema.Text(
[7708]457        title = _(u'Notice'),
[7347]458        required = False,
459        )
[7338]460
461class IApplicantProcessData(IApplicantBaseData):
462    """An applicant.
463
464    Here we add process attributes and methods to the base data.
465    """
466
467    history = Attribute('Object history, a list of messages.')
468    state = Attribute('The application state of an applicant')
[7364]469    display_fullname = Attribute('The fullname of an applicant')
[7338]470    application_date = Attribute('Date of submission, used for export only')
471    password = Attribute('Encrypted password of a applicant')
472    application_number = Attribute('The key under which the record is stored')
473
474    def loggerInfo(ob_class, comment):
475        """Adds an INFO message to the log file
476        """
477
[5753]478    student_id = schema.TextLine(
[7708]479        title = _(u'Student Id'),
[5753]480        required = False,
[7351]481        readonly = False,
[5753]482        )
[6302]483    locked = schema.Bool(
[7708]484        title = _(u'Form locked'),
[6302]485        default = False,
486        )
[5753]487
[7338]488class IApplicant(IApplicantProcessData):
[5753]489    """An applicant.
490
491    This is basically the applicant base data. Here we repeat the
[6195]492    fields from base data if we have to set the `required` attribute
493    to True (which is the default).
[5753]494    """
495
[7338]496class IApplicantEdit(IApplicantProcessData):
[6195]497    """An applicant.
[5753]498
[6339]499    Here we can repeat the fields from base data and set the
500    `required` and `readonly` attributes to True to further restrict
[7262]501    the data access. Or we can allow only certain certificates to be
502    selected by choosing the appropriate source.
503
504    We cannot omit fields here. This has to be done in the
[6339]505    respective form page.
[6195]506    """
[7262]507
508    course1 = schema.Choice(
[7708]509        title = _(u'1st Choice Course of Study'),
[7347]510        source = AppCatCertificateSource(),
[7262]511        required = True,
512        )
513    course2 = schema.Choice(
[7708]514        title = _(u'2nd Choice Course of Study'),
[7347]515        source = AppCatCertificateSource(),
[7262]516        required = False,
517        )
[6255]518    screening_score = schema.Int(
[7708]519        title = _(u'Screening Score'),
[6195]520        required = False,
[5941]521        readonly = True,
522        )
[6195]523    screening_venue = schema.TextLine(
[7708]524        title = _(u'Screening Venue'),
[5753]525        required = False,
526        readonly = True,
527        )
[6301]528    course_admitted = schema.Choice(
[7708]529        title = _(u'Admitted Course of Study'),
[7347]530        source = CertificateSource(),
[6301]531        default = None,
[5753]532        required = False,
[6195]533        readonly = True,
[5753]534        )
[6195]535    notice = schema.Text(
[7708]536        title = _(u'Notice'),
[5753]537        required = False,
538        readonly = True,
539        )
[5758]540
[7338]541    def createStudent():
542        """Create a student object from applicatnt data
543        and copy applicant object.
544        """
545
[7268]546class IApplicantUpdateByRegNo(IApplicant):
547    """Representation of an applicant.
548
[7270]549    Skip regular reg_number validation if reg_number is used for finding
[7268]550    the applicant object.
551    """
[7270]552    reg_number = schema.TextLine(
[7268]553        title = u'Registration Number',
554        default = None,
555        required = False,
556        )
557
[7250]558class IApplicantOnlinePayment(IOnlinePayment):
559    """An applicant payment via payment gateways.
560
561    """
562    p_year = schema.Choice(
[7708]563        title = _(u'Payment Session'),
[7250]564        source = academic_sessions_vocab,
565        required = False,
566        )
567
568IApplicantOnlinePayment['p_year'].order = IApplicantOnlinePayment[
569    'p_year'].order
570
[5846]571class IApplicantsContainerProvider(Interface):
[5820]572    """A provider for applicants containers.
573
574    Applicants container providers are meant to be looked up as
575    utilities. This way we can find all applicant container types
576    defined somewhere.
577
578    Each applicants container provider registered as utility provides
579    one container type and one should be able to call the `factory`
580    attribute to create an instance of the requested container type.
581
582    .. THE FOLLOWING SHOULD GO INTO SPHINX DOCS (and be tested)
[6076]583
[6500]584    Samples:
[6076]585
[5820]586    Given, you had an IApplicantsContainer implementation somewhere
587    and you would like to make it findable on request, then you would
588    normally create an appropriate provider utility like this::
589
590      import grok
[5846]591      from waeup.sirp.applicants.interfaces import IApplicantsContainerProvider
[5820]592
593      class MyContainerProvider(grok.GlobalUtility):
[5846]594          grok.implements(IApplicantsContainerProvider)
[5820]595          grok.name('MyContainerProvider') # Must be unique
596          factory = MyContainer # A class implementing IApplicantsContainer
597                                # or derivations thereof.
598
599    This utility would be registered on startup and could then be used
600    like this:
601
602      >>> from zope.component import getAllUtilitiesRegisteredFor
603      >>> from waeup.sirp.applicants.interfaces import (
[5846]604      ...     IApplicantsContainerProvider)
[5820]605      >>> all_providers = getAllUtilitiesRegisteredFor(
[5846]606      ...     IApplicantsContainerProvider)
[5820]607      >>> all_providers
608      [<MyContainerProvider object at 0x...>]
609
610    You could look up this specific provider by name:
611
612      >>> from zope.component import getUtility
[5846]613      >>> p = getUtility(IApplicantsContainerProvider, name='MyProvider')
[5820]614      >>> p
615      <MyContainerProvider object at 0x...>
[6076]616
[5820]617    An applicants container would then be created like this:
618
619      >>> provider = all_providers[0]
620      >>> container = provider.factory()
621      >>> container
622      <MyContainer object at 0x...>
623
624    """
625    factory = Attribute("A class that can create instances of the "
626                        "requested container type")
Note: See TracBrowser for help on using the repository browser.