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

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

Log changes when saving ApplicantsContainerManageFormPage?.

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