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

Last change on this file since 16976 was 16976, checked in by Henrik Bettermann, 2 years ago

Send email after final submission of application form.

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