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

Last change on this file since 16170 was 16134, checked in by Henrik Bettermann, 4 years ago

Ease customization.

  • Property svn:keywords set to Id
File size: 20.9 KB
Line 
1## $Id: interfaces.py 16134 2020-06-28 18:01:52Z 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.schema import TextLineChoice, FormattedDate
30from waeup.kofa.interfaces import (
31    IKofaObject, validate_email, validate_html,
32    SimpleKofaVocabulary)
33from waeup.kofa.interfaces import MessageFactory as _
34from waeup.kofa.payments.interfaces import IOnlinePayment
35from waeup.kofa.schema import PhoneNumber
36from waeup.kofa.schoolgrades import ResultEntryField
37from waeup.kofa.refereeentries import RefereeEntryField
38from waeup.kofa.students.vocabularies import GenderSource, RegNumberSource
39from waeup.kofa.university.vocabularies import (
40    AppCatSource, CertificateSource, SpecialApplicationSource)
41
42_marker = object() # a marker different from None
43
44def year_range():
45    curr_year = datetime.now().year
46    return range(curr_year - 4, curr_year + 5)
47
48class RegNumInSource(ValidationError):
49    """Registration number exists already
50    """
51    # The docstring of ValidationErrors is used as error description
52    # by zope.formlib.
53    pass
54
55class ApplicantRegNumberSource(RegNumberSource):
56    """A source that accepts any reg number if not used already by a
57    different applicant.
58    """
59    cat_name = 'applicants_catalog'
60    field_name = 'reg_number'
61    validation_error = RegNumInSource
62    comp_field = 'applicant_id'
63
64def contextual_reg_num_source(context):
65    source = ApplicantRegNumberSource(context)
66    return source
67directlyProvides(contextual_reg_num_source, IContextSourceBinder)
68
69
70class AppCatCertificateSource(CertificateSource):
71    """An application certificate source delivers all certificates
72    which belong to a certain application_category.
73
74    This source is meant to be used with Applicants.
75
76    The application category must match the application category of
77    the context parent, normally an applicants container.
78    """
79    def contains(self, context, value):
80        context_appcat = getattr(getattr(
81            context, '__parent__', None), 'application_category', _marker)
82        if context_appcat is _marker:
83            # If the context (applicant) has no application category,
84            # then it might be not part of a container (yet), for
85            # instance during imports. We consider this correct.
86            return True
87        if value.application_category == context_appcat:
88            return True
89        return False
90
91    def getValues(self, context):
92        appcat = getattr(getattr(context, '__parent__', None),
93                         'application_category', None)
94        catalog = getUtility(ICatalog, name='certificates_catalog')
95        result = catalog.searchResults(
96            application_category=(appcat,appcat))
97        resultlist = getUtility(
98            IApplicantsUtils).sortCertificates(context, result)
99        return resultlist
100
101    def getTitle(self, context, value):
102        return getUtility(
103            IApplicantsUtils).getCertTitle(context, value)
104
105class ApplicationTypeSource(BasicContextualSourceFactory):
106    """An application type source delivers screening types defined in the
107    portal.
108    """
109    def getValues(self, context):
110        appcats_dict = getUtility(
111            IApplicantsUtils).APP_TYPES_DICT
112        return [item[0] for item in sorted(appcats_dict.items(),
113                                           key=lambda item: item[1])]
114
115    def getToken(self, context, value):
116        return value
117
118    def getTitle(self, context, value):
119        appcats_dict = getUtility(
120            IApplicantsUtils).APP_TYPES_DICT
121        return appcats_dict[value][0]
122
123# Maybe FUTMinna still needs this ...
124#class ApplicationPinSource(BasicContextualSourceFactory):
125#    """An application pin source delivers PIN prefixes for application
126#    defined in the portal.
127#    """
128#    def getValues(self, context):
129#        apppins_dict = getUtility(
130#            IApplicantsUtils).APP_TYPES_DICT
131#        return sorted(appcats_dict.keys())
132#
133#    def getToken(self, context, value):
134#        return value
135#
136#    def getTitle(self, context, value):
137#        apppins_dict = getUtility(
138#            IApplicantsUtils).APP_TYPES_DICT
139#        return u"%s (%s)" % (
140#            apppins_dict[value][1],self.apppins_dict[value][0])
141
142application_modes_vocab = SimpleKofaVocabulary(
143    (_('Create Application Records'), 'create'),
144    (_('Update Application Records'), 'update'),
145    )
146
147class IApplicantsUtils(Interface):
148    """A collection of methods which are subject to customization.
149    """
150    APP_TYPES_DICT = Attribute('dict of application types')
151
152    def setPaymentDetails(container, payment):
153        """Set the payment data of an applicant.
154        """
155
156    def getApplicantsStatistics(container):
157        """Count applicants in containers.
158        """
159
160    def filterCertificates(context, resultset):
161        """Filter and sort certificates for AppCatCertificateSource.
162        """
163
164    def getCertTitle(context, value):
165        """Compose the titles in AppCatCertificateSource.
166        """
167
168    def isPictureEditable(container):
169        """False if applicants are not allowed to edit uploaded pictures.
170        """
171
172class IApplicantsRoot(IKofaObject, IContainer):
173    """A container for applicants containers.
174    """
175    description_dict = Attribute('Language translation dictionary with values in HTML format')
176    local_roles = Attribute('List of local role names')
177    logger_name = Attribute('Name of the logger')
178    logger_filename = Attribute('Name of the logger file')
179
180    description = schema.Text(
181        title = _(u'Human readable description in HTML format'),
182        required = False,
183        constraint=validate_html,
184        default = u'''This text can been seen by anonymous users.
185Here we put multi-lingual general information about the application procedure.
186>>de<<
187Dieser Text kann von anonymen Benutzern gelesen werden.
188Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
189        )
190
191class IApplicantsContainer(IKofaObject):
192    """An applicants container contains applicants.
193    """
194    statistics = Attribute('Applicant counts')
195    expired = Attribute('True if application has started but not ended')
196
197    description_dict = Attribute('Language translation dictionary with values in HTML format')
198    local_roles = Attribute('List of local role names')
199    picture_editable = Attribute('False if applicants are not allowed to edit uploaded pictures.')
200
201
202    code = schema.TextLine(
203        title = _(u'Code'),
204        required = True,
205        readonly = True,
206        )
207
208    title = schema.TextLine(
209        title = _(u'Title'),
210        required = True,
211        readonly = False,
212        )
213
214    prefix = schema.Choice(
215        title = _(u'Application Target'),
216        required = True,
217        source = ApplicationTypeSource(),
218        readonly = True,
219        )
220
221    year = schema.Choice(
222        title = _(u'Year of Entrance'),
223        required = True,
224        values = year_range(),
225        readonly = True,
226        )
227
228    mode = schema.Choice(
229        title = _(u'Application Mode'),
230        vocabulary = application_modes_vocab,
231        required = True,
232        )
233
234    # Maybe FUTMinna still needs this ...
235    #ac_prefix = schema.Choice(
236    #    title = u'Activation code prefix',
237    #    required = True,
238    #    default = None,
239    #    source = ApplicationPinSource(),
240    #    )
241
242    application_category = schema.Choice(
243        title = _(u'Category for the grouping of certificates'),
244        required = True,
245        source = AppCatSource(),
246        )
247
248    description = schema.Text(
249        title = _(u'Human readable description in HTML format'),
250        required = False,
251        constraint=validate_html,
252        default = u'''This text can been seen by anonymous users.
253Here we put multi-lingual information about the study courses provided, the application procedure and deadlines.
254>>de<<
255Dieser Text kann von anonymen Benutzern gelesen werden.
256Hier koennen mehrsprachige Informationen fuer Antragsteller hinterlegt werden.'''
257        )
258
259    startdate = schema.Datetime(
260        title = _(u'Application Start Date'),
261        required = False,
262        description = _('Example: ') + u'2011-12-01 18:30:00+01:00',
263        )
264
265    enddate = schema.Datetime(
266        title = _(u'Application Closing Date'),
267        required = False,
268        description = _('Example: ') + u'2011-12-31 23:59:59+01:00',
269        )
270
271    strict_deadline = schema.Bool(
272        title = _(u'Forbid additions after deadline (enddate)'),
273        required = False,
274        default = True,
275        )
276
277    application_fee = schema.Float(
278        title = _(u'Application Fee'),
279        default = 0.0,
280        required = False,
281        )
282
283    application_slip_notice = schema.Text(
284        title = _(u'Human readable notice on application slip in HTML format'),
285        required = False,
286        constraint=validate_html,
287        )
288
289    hidden= schema.Bool(
290        title = _(u'Hide container'),
291        required = False,
292        default = False,
293        )
294
295    with_picture= schema.Bool(
296        title = _(u'With passport picture'),
297        required = False,
298        default = True,
299        )
300
301    def addApplicant(applicant):
302        """Add an applicant.
303        """
304
305    def writeLogMessage(view, comment):
306        """Add an INFO message to applicants.log.
307        """
308
309    def traverse(name):
310        """Deliver appropriate containers.
311        """
312
313class IApplicantsContainerAdd(IApplicantsContainer):
314    """An applicants container contains university applicants.
315    """
316    prefix = schema.Choice(
317        title = _(u'Application Target'),
318        required = True,
319        source = ApplicationTypeSource(),
320        readonly = False,
321        )
322
323    year = schema.Choice(
324        title = _(u'Year of Entrance'),
325        required = True,
326        values = year_range(),
327        readonly = False,
328        )
329
330    container_number = schema.Choice(
331        title = _(u'Container Number'),
332        values = range(1,100),
333        description = _(u'If set, this number will be added to the container '
334                         'prefix (e.g. app3). If not set, the year of entrance will be '
335                         'used (e.g. app2019).'),
336        required = False,
337        readonly = False,
338        )
339
340IApplicantsContainerAdd[
341    'prefix'].order =  IApplicantsContainer['prefix'].order
342IApplicantsContainerAdd[
343    'year'].order =  IApplicantsContainer['year'].order
344IApplicantsContainerAdd[
345    'container_number'].order =  IApplicantsContainer['year'].order
346
347class IApplicantBaseData(IKofaObject):
348    """This is a base interface of an applicant with no field
349    required. For use with processors, forms, etc., please use one of
350    the derived interfaces below, which set more fields to required
351    state, depending on use-case.
352    """
353    state = Attribute('Application state of an applicant')
354    history = Attribute('Object history, a list of messages')
355    display_fullname = Attribute('The fullname of an applicant')
356    application_number = Attribute('The key under which the record is stored')
357    container_code = Attribute('Code of the parent container plus additional information if record is used or not')
358    translated_state = Attribute('Real name of the application state')
359    special = Attribute('True if special application')
360    payments = Attribute('List of payment objects stored in the applicant container')
361
362    application_date = Attribute('UTC datetime of submission, used for export only')
363    password = Attribute('Encrypted password of an applicant')
364
365
366    suspended = schema.Bool(
367        title = _(u'Account suspended'),
368        default = False,
369        required = False,
370        )
371
372    applicant_id = schema.TextLine(
373        title = _(u'Applicant Id'),
374        required = False,
375        readonly = False,
376        )
377
378    reg_number = TextLineChoice(
379        title = _(u'Registration Number'),
380        readonly = False,
381        required = True,
382        source = contextual_reg_num_source,
383        )
384
385    firstname = schema.TextLine(
386        title = _(u'First Name'),
387        required = True,
388        )
389
390    middlename = schema.TextLine(
391        title = _(u'Middle Name'),
392        required = False,
393        )
394
395    lastname = schema.TextLine(
396        title = _(u'Last Name (Surname)'),
397        required = True,
398        )
399
400    date_of_birth = FormattedDate(
401        title = _(u'Date of Birth'),
402        required = False,
403        show_year = True,
404        )
405
406    sex = schema.Choice(
407        title = _(u'Gender'),
408        source = GenderSource(),
409        required = True,
410        )
411
412    email = schema.ASCIILine(
413        title = _(u'Email Address'),
414        required = False,
415        constraint=validate_email,
416        )
417
418    phone = PhoneNumber(
419        title = _(u'Phone'),
420        description = u'',
421        required = False,
422        )
423
424    course1 = schema.Choice(
425        title = _(u'1st Choice Course of Study'),
426        source = AppCatCertificateSource(),
427        required = False,
428        )
429
430    course2 = schema.Choice(
431        title = _(u'2nd Choice Course of Study'),
432        source = AppCatCertificateSource(),
433        required = False,
434        )
435
436    notice = schema.Text(
437        title = _(u'Notice'),
438        required = False,
439        )
440    student_id = schema.TextLine(
441        title = _(u'Student Id'),
442        required = False,
443        readonly = False,
444        )
445    course_admitted = schema.Choice(
446        title = _(u'Admitted Course of Study'),
447        source = CertificateSource(),
448        required = False,
449        )
450    locked = schema.Bool(
451        title = _(u'Form locked'),
452        default = False,
453        required = False,
454        )
455
456    special_application = schema.Choice(
457        title = _(u'Special Application'),
458        source = SpecialApplicationSource(),
459        required = False,
460        )
461
462class IApplicantTestData(IKofaObject):
463    """This interface is for demonstration and testing only.
464    It can be omitted in customized versions of Kofa.
465    """
466
467    school_grades = schema.List(
468        title = _(u'School Grades'),
469        value_type = ResultEntryField(),
470        required = False,
471        defaultFactory=list,
472        )
473
474    referees = schema.List(
475        title = _(u'Referees'),
476        value_type = RefereeEntryField(),
477        required = False,
478        defaultFactory=list,
479        )
480
481IApplicantTestData['school_grades'].order = IApplicantBaseData['course2'].order
482
483class IApplicant(IApplicantBaseData, IApplicantTestData):
484    """This is basically the applicant base data. Here we repeat the
485    fields from base data if we have to set the `required` attribute
486    to True (which is the default).
487    """
488
489    def writeLogMessage(view, comment):
490        """Add an INFO message to applicants.log.
491        """
492
493    def createStudent():
494        """Create a student object from applicant data and copy
495        passport image and application slip.
496        """
497
498class ISpecialApplicant(IKofaObject):
499    """This reduced interface is for former students or students who are not
500    users of the portal but have to pay supplementary fees.
501    This interface is used in browser components only. Thus we can't add
502    fields here to the regular IApplicant interface here. We can
503    only 'customize' fields.
504    """
505
506    suspended = schema.Bool(
507        title = _(u'Account suspended'),
508        default = False,
509        required = False,
510        )
511
512    locked = schema.Bool(
513        title = _(u'Form locked'),
514        default = False,
515        required = False,
516        )
517
518    applicant_id = schema.TextLine(
519        title = _(u'Applicant Id'),
520        required = False,
521        readonly = False,
522        )
523
524    firstname = schema.TextLine(
525        title = _(u'First Name'),
526        required = True,
527        )
528
529    middlename = schema.TextLine(
530        title = _(u'Middle Name'),
531        required = False,
532        )
533
534    lastname = schema.TextLine(
535        title = _(u'Last Name (Surname)'),
536        required = True,
537        )
538
539    reg_number = TextLineChoice(
540        title = _(u'Identification Number'),
541        #description = _(u'Enter either registration or matriculation number.'),
542        readonly = False,
543        required = True,
544        source = contextual_reg_num_source,
545        )
546
547    date_of_birth = FormattedDate(
548        title = _(u'Date of Birth'),
549        required = False,
550        #date_format = u'%d/%m/%Y', # Use grok-instance-wide default
551        show_year = True,
552        )
553
554    email = schema.ASCIILine(
555        title = _(u'Email Address'),
556        required = True,
557        constraint=validate_email,
558        )
559
560    phone = PhoneNumber(
561        title = _(u'Phone'),
562        description = u'',
563        required = False,
564        )
565
566    special_application = schema.Choice(
567        title = _(u'Special Application'),
568        source = SpecialApplicationSource(),
569        required = True,
570        )
571
572class IApplicantEdit(IApplicant):
573    """This is an applicant interface for editing.
574
575    Here we can repeat the fields from base data and set the
576    `required` and `readonly` attributes to True to further restrict
577    the data access. Or we can allow only certain certificates to be
578    selected by choosing the appropriate source.
579
580    We cannot omit fields here. This has to be done in the
581    respective form page.
582    """
583
584    email = schema.ASCIILine(
585        title = _(u'Email Address'),
586        required = True,
587        constraint=validate_email,
588        )
589
590    course1 = schema.Choice(
591        title = _(u'1st Choice Course of Study'),
592        source = AppCatCertificateSource(),
593        required = True,
594        )
595
596    course2 = schema.Choice(
597        title = _(u'2nd Choice Course of Study'),
598        source = AppCatCertificateSource(),
599        required = False,
600        )
601
602    course_admitted = schema.Choice(
603        title = _(u'Admitted Course of Study'),
604        source = CertificateSource(),
605        required = False,
606        readonly = True,
607        )
608
609    notice = schema.Text(
610        title = _(u'Notice'),
611        required = False,
612        readonly = True,
613        )
614
615IApplicantEdit['email'].order = IApplicantEdit['sex'].order
616
617class IApplicantUpdateByRegNo(IApplicant):
618    """Skip regular reg_number validation if reg_number is used for finding
619    the applicant object.
620    """
621    reg_number = schema.TextLine(
622        title = u'Registration Number',
623        required = False,
624        )
625
626class IApplicantRegisterUpdate(IApplicant):
627    """This is a representation of an applicant for first-time registration.
628    This interface is used when applicants use the registration page to
629    update their records.
630    """
631    reg_number = schema.TextLine(
632        title = u'Registration Number',
633        required = True,
634        )
635
636    #firstname = schema.TextLine(
637    #    title = _(u'First Name'),
638    #    required = True,
639    #    )
640
641    lastname = schema.TextLine(
642        title = _(u'Last Name (Surname)'),
643        required = True,
644        )
645
646    email = schema.ASCIILine(
647        title = _(u'Email Address'),
648        required = True,
649        constraint=validate_email,
650        )
651
652class IApplicantOnlinePayment(IOnlinePayment):
653    """An applicant payment via payment gateways.
654    """
655
656    def doAfterApplicantPayment():
657        """Process applicant after payment was made.
658        """
659
660    def doAfterApplicantPaymentApproval():
661        """Process applicant after payment was approved.
662        """
663
664    def approveApplicantPayment():
665        """Approve payment and process applicant.
666        """
667
668class IApplicantRefereeReport(IKofaObject):
669    """A referee report.
670    """
671
672    r_id = Attribute('Report identifier')
673
674    creation_date = schema.Datetime(
675        title = _(u'Report Creation Date'),
676        readonly = False,
677        required = False,
678        )
679
680    name = schema.TextLine(
681        title = _(u'Referee Name'),
682        required = True,
683        )
684
685    email = schema.ASCIILine(
686        title = _(u'Referee Email Address'),
687        required = True,
688        constraint=validate_email,
689        )
690
691    phone = PhoneNumber(
692        title = _(u'Referee Phone'),
693        description = u'',
694        required = False,
695        )
696
697    report = schema.Text(
698        title = _(u'Report'),
699        required = False,
700        )
Note: See TracBrowser for help on using the repository browser.