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

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

Fix sources.

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