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

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

Add BalancePaymentAddFormPage which can only be opened by managers.
No button is provided in base package.

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