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

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

Save email address provided by mandate when referee report
is created. Add RefereeReportManageFormPage (no button available).

  • Property svn:keywords set to Id
File size: 21.2 KB
Line 
1## $Id: interfaces.py 16243 2020-09-23 19:42:07Z 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 = False,
224        values = year_range(),
225        readonly = False,
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 = False,
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 = False,
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(view, graduated):
494        """Create a student object from applicant data and copy
495        passport image and application slip. If graduated is set True,
496        a graduated student is being created. This method is supposed
497        to be used for transcript applicants. It is tested but not
498        used in the base package.
499        """
500class ISpecialApplicant(IKofaObject):
501    """This reduced interface is for former students or students who are not
502    users of the portal but have to pay supplementary fees.
503    This interface is used in browser components only. Thus we can't add
504    fields here to the regular IApplicant interface here. We can
505    only 'customize' fields.
506    """
507
508    suspended = schema.Bool(
509        title = _(u'Account suspended'),
510        default = False,
511        required = False,
512        )
513
514    locked = schema.Bool(
515        title = _(u'Form locked'),
516        default = False,
517        required = False,
518        )
519
520    applicant_id = schema.TextLine(
521        title = _(u'Applicant Id'),
522        required = False,
523        readonly = False,
524        )
525
526    firstname = schema.TextLine(
527        title = _(u'First Name'),
528        required = True,
529        )
530
531    middlename = schema.TextLine(
532        title = _(u'Middle Name'),
533        required = False,
534        )
535
536    lastname = schema.TextLine(
537        title = _(u'Last Name (Surname)'),
538        required = True,
539        )
540
541    reg_number = TextLineChoice(
542        title = _(u'Identification Number'),
543        #description = _(u'Enter either registration or matriculation number.'),
544        readonly = False,
545        required = True,
546        source = contextual_reg_num_source,
547        )
548
549    date_of_birth = FormattedDate(
550        title = _(u'Date of Birth'),
551        required = False,
552        #date_format = u'%d/%m/%Y', # Use grok-instance-wide default
553        show_year = True,
554        )
555
556    email = schema.ASCIILine(
557        title = _(u'Email Address'),
558        required = True,
559        constraint=validate_email,
560        )
561
562    phone = PhoneNumber(
563        title = _(u'Phone'),
564        description = u'',
565        required = False,
566        )
567
568    special_application = schema.Choice(
569        title = _(u'Special Application'),
570        source = SpecialApplicationSource(),
571        required = True,
572        )
573
574class IApplicantEdit(IApplicant):
575    """This is an applicant interface for editing.
576
577    Here we can repeat the fields from base data and set the
578    `required` and `readonly` attributes to True to further restrict
579    the data access. Or we can allow only certain certificates to be
580    selected by choosing the appropriate source.
581
582    We cannot omit fields here. This has to be done in the
583    respective form page.
584    """
585
586    email = schema.ASCIILine(
587        title = _(u'Email Address'),
588        required = True,
589        constraint=validate_email,
590        )
591
592    course1 = schema.Choice(
593        title = _(u'1st Choice Course of Study'),
594        source = AppCatCertificateSource(),
595        required = True,
596        )
597
598    course2 = schema.Choice(
599        title = _(u'2nd Choice Course of Study'),
600        source = AppCatCertificateSource(),
601        required = False,
602        )
603
604    course_admitted = schema.Choice(
605        title = _(u'Admitted Course of Study'),
606        source = CertificateSource(),
607        required = False,
608        readonly = True,
609        )
610
611    notice = schema.Text(
612        title = _(u'Notice'),
613        required = False,
614        readonly = True,
615        )
616
617IApplicantEdit['email'].order = IApplicantEdit['sex'].order
618
619class IApplicantUpdateByRegNo(IApplicant):
620    """Skip regular reg_number validation if reg_number is used for finding
621    the applicant object.
622    """
623    reg_number = schema.TextLine(
624        title = u'Registration Number',
625        required = False,
626        )
627
628class IApplicantRegisterUpdate(IApplicant):
629    """This is a representation of an applicant for first-time registration.
630    This interface is used when applicants use the registration page to
631    update their records.
632    """
633    reg_number = schema.TextLine(
634        title = u'Registration Number',
635        required = True,
636        )
637
638    #firstname = schema.TextLine(
639    #    title = _(u'First Name'),
640    #    required = True,
641    #    )
642
643    lastname = schema.TextLine(
644        title = _(u'Last Name (Surname)'),
645        required = True,
646        )
647
648    email = schema.ASCIILine(
649        title = _(u'Email Address'),
650        required = True,
651        constraint=validate_email,
652        )
653
654class IApplicantOnlinePayment(IOnlinePayment):
655    """An applicant payment via payment gateways.
656    """
657
658    def doAfterApplicantPayment():
659        """Process applicant after payment was made.
660        """
661
662    def doAfterApplicantPaymentApproval():
663        """Process applicant after payment was approved.
664        """
665
666    def approveApplicantPayment():
667        """Approve payment and process applicant.
668        """
669
670class IApplicantRefereeReport(IKofaObject):
671    """A referee report.
672    """
673
674    r_id = Attribute('Report identifier')
675    email = Attribute('Referee email address taken from the mandate')
676
677    creation_date = schema.Datetime(
678        title = _(u'Report Creation Date'),
679        readonly = False,
680        required = False,
681        )
682
683    name = schema.TextLine(
684        title = _(u'Referee Name'),
685        required = True,
686        )
687
688    email_pref = schema.ASCIILine(
689        title = _(u'Preferred Email Address'),
690        required = False,
691        constraint=validate_email,
692        readonly = False,
693        )
694
695    phone = PhoneNumber(
696        title = _(u'Referee Phone'),
697        description = u'',
698        required = False,
699        )
700
701    report = schema.Text(
702        title = _(u'Report'),
703        required = False,
704        )
Note: See TracBrowser for help on using the repository browser.