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

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

Rename importers to processors.

  • Property svn:keywords set to Id
File size: 19.5 KB
Line 
1## $Id: interfaces.py 7933 2012-03-21 13:42:00Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
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.
8##
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.
13##
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##
18"""Interfaces of the university application package.
19"""
20from grokcore.content.interfaces import IContainer
21from zc.sourcefactory.basic import BasicSourceFactory
22from zc.sourcefactory.contextual import BasicContextualSourceFactory
23from zope import schema
24from zope.component import getUtilitiesFor, queryUtility, getUtility
25from zope.catalog.interfaces import ICatalog
26from zope.interface import Interface, Attribute, implements, directlyProvides
27from zope.schema.interfaces import (
28    ValidationError, ISource, IContextSourceBinder)
29from waeup.kofa.schema import TextLineChoice
30from waeup.kofa.interfaces import (
31    IKofaObject, year_range, validate_email, academic_sessions_vocab)
32from waeup.kofa.interfaces import MessageFactory as _
33from waeup.kofa.payments.interfaces import IOnlinePayment
34from waeup.kofa.schoolgrades import ResultEntryField
35from waeup.kofa.students.vocabularies import (
36    lgas_vocab, GenderSource)
37from waeup.kofa.university.vocabularies import (
38    course_levels, AppCatSource, CertificateSource)
39
40#: Maximum upload size for applicant passport photographs (in bytes)
41MAX_UPLOAD_SIZE = 1024 * 20
42
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'
53    field_name = 'reg_number'
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
79
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):
106        appcats_dict = getUtility(
107            IApplicantsUtils).APP_TYPES_DICT
108        return sorted(appcats_dict.keys())
109
110    def getToken(self, context, value):
111        return value
112
113    def getTitle(self, context, value):
114        appcats_dict = getUtility(
115            IApplicantsUtils).APP_TYPES_DICT
116        return appcats_dict[value][0]
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):
124#        apppins_dict = getUtility(
125#            IApplicantsUtils).APP_TYPES_DICT
126#        return sorted(appcats_dict.keys())
127#
128#    def getToken(self, context, value):
129#        return value
130#
131#    def getTitle(self, context, value):
132#        apppins_dict = getUtility(
133#            IApplicantsUtils).APP_TYPES_DICT
134#        return u"%s (%s)" % (
135#            apppins_dict[value][1],self.apppins_dict[value][0])
136
137class ApplicantContainerProviderSource(BasicSourceFactory):
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'
152
153    """
154    def getValues(self):
155        """Returns a list of ``(<name>, <provider>)`` tuples.
156
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
163    def getToken(self, value):
164        """Return the name of the ``(<name>, <provider>)`` tuple.
165        """
166        return value[0]
167
168    def getTitle(self, value):
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)
174
175class IApplicantsUtils(Interface):
176    """A collection of methods which are subject to customization.
177    """
178
179    APP_TYPES_DICT = Attribute(' dict of application types')
180
181class IApplicantsRoot(IKofaObject, IContainer):
182    """A container for university applicants containers.
183    """
184    pass
185
186class IApplicantsContainer(IKofaObject):
187    """An applicants container contains university applicants.
188
189    """
190
191    container_title = Attribute(
192        u'classattribute: title for type of container')
193    container_description = Attribute(
194        u'classattribute: description for type of container')
195
196
197    code = schema.TextLine(
198        title = _(u'Code'),
199        default = u'-',
200        required = True,
201        readonly = True,
202        )
203
204    title = schema.TextLine(
205        title = _(u'Title'),
206        required = True,
207        default = u'-',
208        readonly = True,
209        )
210
211    prefix = schema.Choice(
212        title = _(u'Application Target'),
213        required = True,
214        default = None,
215        source = ApplicationTypeSource(),
216        readonly = True,
217        )
218
219    entry_level = schema.Choice(
220        title = _(u'Entry Level'),
221        vocabulary = course_levels,
222        default = 100,
223        required = True,
224        )
225
226    year = schema.Choice(
227        title = _(u'Year of Entrance'),
228        required = True,
229        default = None,
230        values = year_range(),
231        readonly = True,
232        )
233
234    provider = schema.Choice(
235        title = _(u'Applicants Container Type'),
236        required = True,
237        default = None,
238        source = ApplicantContainerProviderSource(),
239        readonly = True,
240        )
241
242    # Maybe Uniben still needs this ...
243    #ac_prefix = schema.Choice(
244    #    title = u'Activation code prefix',
245    #    required = True,
246    #    default = None,
247    #    source = ApplicationPinSource(),
248    #    )
249
250    application_category = schema.Choice(
251        title = _(u'Category for the grouping of certificates'),
252        required = True,
253        default = None,
254        source = AppCatSource(),
255        )
256
257    description = schema.Text(
258        title = _(u'Human readable description in reST format'),
259        required = False,
260        default = u'''This text can been seen by anonymous users.
261Here we put mult-lingual information about the study courses provided, the application procedure and deadlines.
262>>de<<
263Dieser Text kann von anonymen Benutzern gelesen werden.
264Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
265        )
266
267    description_dict = Attribute(
268        """Content as language dictionary with values in HTML format.""")
269
270    startdate = schema.Date(
271        title = _(u'Application Start Date'),
272        required = False,
273        default = None,
274        )
275
276    enddate = schema.Date(
277        title = _(u'Application Closing Date'),
278        required = False,
279        default = None,
280        )
281
282    strict_deadline = schema.Bool(
283        title = _(u'Forbid additions after deadline (enddate)'),
284        required = True,
285        default = True,
286        )
287
288    def archive(id=None):
289        """Create on-dist archive of applicants stored in this term.
290
291        If id is `None`, all applicants are archived.
292
293        If id contains a single id string, only the respective
294        applicants are archived.
295
296        If id contains a list of id strings all of the respective
297        applicants types are saved to disk.
298        """
299
300    def clear(id=None, archive=True):
301        """Remove applicants of type given by 'id'.
302
303        Optionally archive the applicants.
304
305        If id is `None`, all applicants are archived.
306
307        If id contains a single id string, only the respective
308        applicants are archived.
309
310        If id contains a list of id strings all of the respective
311        applicant types are saved to disk.
312
313        If `archive` is ``False`` none of the archive-handling is done
314        and respective applicants are simply removed from the
315        database.
316        """
317
318class IApplicantsContainerAdd(IApplicantsContainer):
319    """An applicants container contains university applicants.
320    """
321    prefix = schema.Choice(
322        title = _(u'Application Target'),
323        required = True,
324        default = None,
325        source = ApplicationTypeSource(),
326        readonly = False,
327        )
328
329    year = schema.Choice(
330        title = _(u'Year of Entrance'),
331        required = True,
332        default = None,
333        values = year_range(),
334        readonly = False,
335        )
336
337    provider = schema.Choice(
338        title = _(u'Applicants Container Type'),
339        required = True,
340        default = None,
341        source = ApplicantContainerProviderSource(),
342        readonly = False,
343        )
344
345IApplicantsContainerAdd[
346    'prefix'].order =  IApplicantsContainer['prefix'].order
347IApplicantsContainerAdd[
348    'year'].order =  IApplicantsContainer['year'].order
349IApplicantsContainerAdd[
350    'provider'].order =  IApplicantsContainer['provider'].order
351
352class IApplicantBaseData(IKofaObject):
353    """The data for an applicant.
354
355    This is a base interface with no field
356    required. For use with processors, forms, etc., please use one of
357    the derived interfaces below, which set more fields to required
358    state, depending on use-case.
359
360    This base interface is also implemented by the
361    :class:`waeup.kofa.students.StudentApplication` class in the
362    students package. Thus, these are the data which are saved after
363    admission.
364    """
365    applicant_id = schema.TextLine(
366        title = _(u'Applicant Id'),
367        required = False,
368        readonly = False,
369        )
370    reg_number = TextLineChoice(
371        title = _(u'JAMB Registration Number'),
372        readonly = False,
373        required = True,
374        default = None,
375        source = contextual_reg_num_source,
376        )
377    #access_code = schema.TextLine(
378    #    title = u'Activation Code',
379    #    required = False,
380    #    readonly = True,
381    #    )
382    firstname = schema.TextLine(
383        title = _(u'First Name'),
384        required = True,
385        )
386    middlename = schema.TextLine(
387        title = _(u'Middle Name'),
388        required = False,
389        )
390    lastname = schema.TextLine(
391        title = _(u'Last Name (Surname)'),
392        required = True,
393        )
394    date_of_birth = schema.Date(
395        title = _(u'Date of Birth'),
396        required = True,
397        )
398    lga = schema.Choice(
399        source = lgas_vocab,
400        title = _(u'State/LGA'),
401        default = 'foreigner',
402        required = True,
403        )
404    sex = schema.Choice(
405        title = _(u'Sex'),
406        source = GenderSource(),
407        default = u'm',
408        required = True,
409        )
410    email = schema.ASCIILine(
411        title = _(u'Email Address'),
412        required = True,
413        constraint=validate_email,
414        )
415    phone = schema.TextLine(
416        title = _(u'Phone'),
417        description = u'',
418        required = False,
419        )
420    course1 = schema.Choice(
421        title = _(u'1st Choice Course of Study'),
422        source = CertificateSource(),
423        required = True,
424        )
425    course2 = schema.Choice(
426        title = _(u'2nd Choice Course of Study'),
427        source = CertificateSource(),
428        required = False,
429        )
430    school_grades = schema.List(
431        title = _(u'School Grades'),
432        value_type = ResultEntryField(),
433        required = True,
434        default = [],
435        )
436
437    #
438    # Data to be imported after screening
439    #
440    screening_score = schema.Int(
441        title = _(u'Screening Score'),
442        required = False,
443        )
444    screening_venue = schema.TextLine(
445        title = _(u'Screening Venue'),
446        required = False,
447        )
448    course_admitted = schema.Choice(
449        title = _(u'Admitted Course of Study'),
450        source = CertificateSource(),
451        default = None,
452        required = False,
453        )
454    notice = schema.Text(
455        title = _(u'Notice'),
456        required = False,
457        )
458
459class IApplicantProcessData(IApplicantBaseData):
460    """An applicant.
461
462    Here we add process attributes and methods to the base data.
463    """
464
465    history = Attribute('Object history, a list of messages')
466    state = Attribute('The application state of an applicant')
467    display_fullname = Attribute('The fullname of an applicant')
468    application_date = Attribute('Date of submission, used for export only')
469    password = Attribute('Encrypted password of a applicant')
470    application_number = Attribute('The key under which the record is stored')
471
472    def loggerInfo(ob_class, comment):
473        """Adds an INFO message to the log file
474        """
475
476    student_id = schema.TextLine(
477        title = _(u'Student Id'),
478        required = False,
479        readonly = False,
480        )
481    locked = schema.Bool(
482        title = _(u'Form locked'),
483        default = False,
484        )
485
486class IApplicant(IApplicantProcessData):
487    """An applicant.
488
489    This is basically the applicant base data. Here we repeat the
490    fields from base data if we have to set the `required` attribute
491    to True (which is the default).
492    """
493
494class IApplicantEdit(IApplicantProcessData):
495    """An applicant interface for editing.
496
497    Here we can repeat the fields from base data and set the
498    `required` and `readonly` attributes to True to further restrict
499    the data access. Or we can allow only certain certificates to be
500    selected by choosing the appropriate source.
501
502    We cannot omit fields here. This has to be done in the
503    respective form page.
504    """
505
506    course1 = schema.Choice(
507        title = _(u'1st Choice Course of Study'),
508        source = AppCatCertificateSource(),
509        required = True,
510        )
511    course2 = schema.Choice(
512        title = _(u'2nd Choice Course of Study'),
513        source = AppCatCertificateSource(),
514        required = False,
515        )
516    screening_score = schema.Int(
517        title = _(u'Screening Score'),
518        required = False,
519        readonly = True,
520        )
521    screening_venue = schema.TextLine(
522        title = _(u'Screening Venue'),
523        required = False,
524        readonly = True,
525        )
526    course_admitted = schema.Choice(
527        title = _(u'Admitted Course of Study'),
528        source = CertificateSource(),
529        default = None,
530        required = False,
531        readonly = True,
532        )
533    notice = schema.Text(
534        title = _(u'Notice'),
535        required = False,
536        readonly = True,
537        )
538
539    def createStudent():
540        """Create a student object from applicatnt data
541        and copy applicant object.
542        """
543
544class IApplicantUpdateByRegNo(IApplicant):
545    """Representation of an applicant.
546
547    Skip regular reg_number validation if reg_number is used for finding
548    the applicant object.
549    """
550    reg_number = schema.TextLine(
551        title = u'Registration Number',
552        default = None,
553        required = False,
554        )
555
556class IApplicantOnlinePayment(IOnlinePayment):
557    """An applicant payment via payment gateways.
558
559    """
560    p_year = schema.Choice(
561        title = _(u'Payment Session'),
562        source = academic_sessions_vocab,
563        required = False,
564        )
565
566IApplicantOnlinePayment['p_year'].order = IApplicantOnlinePayment[
567    'p_year'].order
568
569class IApplicantsContainerProvider(Interface):
570    """A provider for applicants containers.
571
572    Applicants container providers are meant to be looked up as
573    utilities. This way we can find all applicant container types
574    defined somewhere.
575
576    Each applicants container provider registered as utility provides
577    one container type and one should be able to call the `factory`
578    attribute to create an instance of the requested container type.
579
580    .. THE FOLLOWING SHOULD GO INTO SPHINX DOCS (and be tested)
581
582    Samples:
583
584    Given, you had an IApplicantsContainer implementation somewhere
585    and you would like to make it findable on request, then you would
586    normally create an appropriate provider utility like this::
587
588      import grok
589      from waeup.kofa.applicants.interfaces import IApplicantsContainerProvider
590
591      class MyContainerProvider(grok.GlobalUtility):
592          grok.implements(IApplicantsContainerProvider)
593          grok.name('MyContainerProvider') # Must be unique
594          factory = MyContainer # A class implementing IApplicantsContainer
595                                # or derivations thereof.
596
597    This utility would be registered on startup and could then be used
598    like this:
599
600      >>> from zope.component import getAllUtilitiesRegisteredFor
601      >>> from waeup.kofa.applicants.interfaces import (
602      ...     IApplicantsContainerProvider)
603      >>> all_providers = getAllUtilitiesRegisteredFor(
604      ...     IApplicantsContainerProvider)
605      >>> all_providers
606      [<MyContainerProvider object at 0x...>]
607
608    You could look up this specific provider by name:
609
610      >>> from zope.component import getUtility
611      >>> p = getUtility(IApplicantsContainerProvider, name='MyProvider')
612      >>> p
613      <MyContainerProvider object at 0x...>
614
615    An applicants container would then be created like this:
616
617      >>> provider = all_providers[0]
618      >>> container = provider.factory()
619      >>> container
620      <MyContainer object at 0x...>
621
622    """
623    factory = Attribute("A class that can create instances of the "
624                        "requested container type")
Note: See TracBrowser for help on using the repository browser.