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

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

Merge the vocabularies module with the interfaces module. I don't see any other way to avoid conflicting imports since we have now IApplicantsUtils defined in interfaces and needed in vocabularies.

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