source: main/waeup.kofa/trunk/src/waeup/kofa/applicants/applicant.py @ 16278

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

Emergency fix because we have changed the email field type in IOU. A better solution has to be found.

  • Property svn:keywords set to Id
File size: 19.1 KB
Line 
1## $Id: applicant.py 16278 2020-10-12 09:36:54Z 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##
18import os
19import grok
20from cStringIO import StringIO
21from grok import index
22from hurry.query import Eq, Text
23from hurry.query.query import Query
24from zope.component import getUtility, createObject, getAdapter
25from zope.component.interfaces import IFactory
26from zope.event import notify
27from zope.securitypolicy.interfaces import IPrincipalRoleManager
28from zope.interface import implementedBy
29from zope.schema.interfaces import (
30    RequiredMissing, ConstraintNotSatisfied, WrongType)
31from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
32from waeup.kofa.image import KofaImageFile
33from reportlab.platypus.doctemplate import LayoutError
34from waeup.kofa.imagestorage import DefaultFileStoreHandler
35from waeup.kofa.interfaces import (
36    IObjectHistory, IFileStoreHandler, IFileStoreNameChooser, IKofaUtils,
37    IExtFileStore, IPDF, IUserAccount, IUniversity)
38from waeup.kofa.interfaces import MessageFactory as _
39from waeup.kofa.students.vocabularies import RegNumNotInSource
40from waeup.kofa.students.studycourse import StudentStudyCourse
41from waeup.kofa.utils.helpers import attrs_to_fields
42from waeup.kofa.applicants.interfaces import (
43    IApplicant, IApplicantEdit, ISpecialApplicant, IApplicantsUtils)
44from waeup.kofa.applicants.workflow import application_states_dict
45from waeup.kofa.applicants.payment import ApplicantOnlinePayment
46from waeup.kofa.applicants.refereereport import ApplicantRefereeReport
47
48def search(query=None, searchtype=None, view=None):
49    if searchtype in ('fullname',):
50        results = Query().searchResults(
51            Text(('applicants_catalog', searchtype), query))
52    else:
53        results = Query().searchResults(
54            Eq(('applicants_catalog', searchtype), query))
55    return results
56
57class Applicant(grok.Container):
58    grok.implements(IApplicant, IApplicantEdit, ISpecialApplicant)
59    grok.provides(IApplicant)
60
61    applicant_student_mapping = [
62        ('firstname', 'firstname'),
63        ('middlename', 'middlename'),
64        ('lastname', 'lastname'),
65        ('sex', 'sex'),
66        ('date_of_birth', 'date_of_birth'),
67        ('email', 'email'),
68        ('phone', 'phone'),
69        ]
70
71    applicant_graduated_mapping = [
72        ('firstname', 'firstname'),
73        ('middlename', 'middlename'),
74        ('lastname', 'lastname'),
75        ('sex', 'sex'),
76        ('date_of_birth', 'date_of_birth'),
77        ('email', 'email'),
78        ('phone', 'phone'),
79        ]
80
81    def __init__(self):
82        super(Applicant, self).__init__()
83        self.password = None
84        self.application_date = None
85        self.applicant_id = None
86        return
87
88    @property
89    def payments(self):
90        payments = [value for value in self.values()
91            if isinstance(value, ApplicantOnlinePayment)]
92        return payments
93
94    @property
95    def refereereports(self):
96        reports = [value for value in self.values()
97            if isinstance(value, ApplicantRefereeReport)]
98        return reports
99
100    def writeLogMessage(self, view, message):
101        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
102        self.__parent__.__parent__.logger.info(
103            '%s - %s - %s' % (ob_class, self.applicant_id, message))
104        return
105
106    @property
107    def state(self):
108        return IWorkflowState(self).getState()
109
110    @property
111    def container_code(self):
112        try:
113            code = self.__parent__.code
114        except AttributeError:  # in unit tests
115            return
116        if (self.password,
117            self.firstname,
118            self.lastname,
119            self.email) == (None, None, None, None):
120            return code + '-'
121        return code + '+'
122
123    @property
124    def translated_state(self):
125        try:
126            state = application_states_dict[self.state]
127        except LookupError:  # in unit tests
128            return
129        return state
130
131    @property
132    def history(self):
133        history = IObjectHistory(self)
134        return history
135
136    @property
137    def special(self):
138        try:
139            special = self.__parent__.prefix.startswith('special')
140        except AttributeError:  # in unit tests
141            return
142        return special
143
144    @property
145    def application_number(self):
146        try:
147            return self.applicant_id.split('_')[1]
148        except AttributeError:
149            return None
150
151    @property
152    def display_fullname(self):
153        middlename = getattr(self, 'middlename', None)
154        kofa_utils = getUtility(IKofaUtils)
155        return kofa_utils.fullname(self.firstname, self.lastname, middlename)
156
157    def _setStudyCourseAttributes(self, studycourse):
158        studycourse.entry_mode = self.course_admitted.study_mode
159        studycourse.current_level = self.course_admitted.start_level
160        studycourse.certificate = self.course_admitted
161        studycourse.entry_session = self.__parent__.year
162        studycourse.current_session = self.__parent__.year
163        return
164
165    def _setGraduatedStudyCourseAttributes(self, studycourse):
166        studycourse.entry_mode = self.course_studied.study_mode
167        studycourse.current_level = self.course_studied.start_level
168        studycourse.certificate = self.course_studied
169        studycourse.entry_session = self.__parent__.year
170        studycourse.current_session = self.__parent__.year
171        return
172
173    def createStudent(self, view=None, graduated=False):
174        """Create a student, fill with base data, create an application slip,
175        copy applicant data and files.
176        """
177        site = grok.getSite()
178        # Is applicant in the correct state?
179        if graduated:
180            if self.state != 'processed':
181                return False, _('Applicant has not yet been processed.')
182            certificate = getattr(self, 'course_studied', None)
183            mapping = self.applicant_graduated_mapping
184        else:
185            if self.state != 'admitted':
186                return False, _('Applicant has not yet been admitted.')
187            certificate = getattr(self, 'course_admitted', None)
188            mapping = self.applicant_student_mapping
189        # Does registration number exist?
190        student = createObject(u'waeup.Student')
191        try:
192            student.reg_number = self.reg_number
193        except RegNumNotInSource:
194            # Reset _curr_stud_id
195            site['students']._curr_stud_id -= 1
196            return False, _('Registration Number exists.')
197        # Has the course_admitted/course_studied field been properly filled?
198        if certificate is None:
199            # Reset _curr_stud_id
200            site['students']._curr_stud_id -= 1
201            return False, _('No study course provided.')
202        # Set student attributes
203        try:
204            for item in mapping:
205                setattr(student, item[1], getattr(self, item[0], None))
206        except RequiredMissing, err:
207            site['students']._curr_stud_id -= 1
208            return False, 'RequiredMissing: %s' % err
209        except WrongType, err:
210            if item[0] == 'email':
211                setattr(student, item[1], str(getattr(self, item[0], None)))
212            else:
213                return False, 'WrongType: %s' % err
214        except:
215            site['students']._curr_stud_id -= 1
216            return False, 'Unknown Error'
217        # Prove if the certificate still exists
218        try:
219            StudentStudyCourse().certificate = certificate
220        except ConstraintNotSatisfied, err:
221            # Reset _curr_stud_id
222            site['students']._curr_stud_id -= 1
223            return False, 'ConstraintNotSatisfied: %s' % certificate.code
224        # Finally prove if an application slip can be created
225        try:
226            test_applicant_slip = getAdapter(self, IPDF, name='application_slip')(
227                view=view)
228        except IOError:
229            site['students']._curr_stud_id -= 1
230            return False, _('IOError: Application Slip could not be created.')
231        except LayoutError, err:
232            site['students']._curr_stud_id -= 1
233            return False, _('Reportlab LayoutError: %s' % err)
234        except AttributeError, err:
235            site['students']._curr_stud_id -= 1
236            return False, _('Reportlab AttributeError: ${a}', mapping = {'a':err})
237        except:
238            site['students']._curr_stud_id -= 1
239            return False, _('Unknown Reportlab error')
240        # Add student
241        site['students'].addStudent(student)
242        # Save student_id
243        self.student_id = student.student_id
244        if graduated:
245            # Set state
246            IWorkflowState(student).setState('graduated')
247            # Save the certificate and set study course attributes
248            self._setGraduatedStudyCourseAttributes(student['studycourse'])
249        else:
250            # Fire transitions
251            IWorkflowInfo(self).fireTransition('create')
252            IWorkflowInfo(student).fireTransition('admit')
253            # Save the certificate and set study course attributes
254            self._setStudyCourseAttributes(student['studycourse'])
255        # Set password
256        IUserAccount(student).setPassword(self.application_number)
257        self._copyPassportImage(student)
258        self._copyFiles(student)
259        # Update the catalog
260        notify(grok.ObjectModifiedEvent(student))
261        # Save application slip
262        applicant_slip = getAdapter(self, IPDF, name='application_slip')(
263            view=view)
264        self._saveApplicationPDF(student, applicant_slip, view=view)
265        return True, _('Student ${a} created', mapping = {'a':student.student_id})
266
267    def _saveApplicationPDF(self, student, applicant_slip, view=None):
268        """Create an application slip as PDF and store it in student folder.
269        """
270        file_store = getUtility(IExtFileStore)
271        file_id = IFileStoreNameChooser(student).chooseName(
272            attr="application_slip.pdf")
273        file_store.createFile(file_id, StringIO(applicant_slip))
274        return
275
276    def _copyPassportImage(self, student):
277        """Copy any passport image over to student location.
278        """
279        file_store = getUtility(IExtFileStore)
280        appl_file = file_store.getFileByContext(self)
281        if appl_file is None:
282            return
283        stud_file_id = IFileStoreNameChooser(student).chooseName(
284            attr="passport.jpg")
285        file_store.createFile(stud_file_id, appl_file)
286        return
287
288    def _copyFiles(self, student):
289        """Copy all other files over to student location. Not
290        used in base package but tested with fake file.
291        """
292        file_store = getUtility(IExtFileStore)
293        filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
294        for filename in filenames:
295            appl_file = file_store.getFileByContext(self, attr=filename[1])
296            if appl_file is None:
297                continue
298            stud_file_id = IFileStoreNameChooser(student).chooseName(
299                attr=filename[1])
300            file_store.createFile(stud_file_id, appl_file)
301        return
302
303# Set all attributes of Applicant required in IApplicant as field
304# properties. Doing this, we do not have to set initial attributes
305# ourselves and as a bonus we get free validation when an attribute is
306# set.
307Applicant = attrs_to_fields(Applicant)
308
309class ApplicantsCatalog(grok.Indexes):
310    """A catalog indexing :class:`Applicant` instances in the ZODB.
311    """
312    grok.site(IUniversity)
313    grok.name('applicants_catalog')
314    grok.context(IApplicant)
315
316    fullname = index.Text(attribute='display_fullname')
317    applicant_id = index.Field(attribute='applicant_id')
318    reg_number = index.Field(attribute='reg_number')
319    email = index.Field(attribute='email')
320    state = index.Field(attribute='state')
321    container_code = index.Field(attribute='container_code')
322
323class ApplicantFactory(grok.GlobalUtility):
324    """A factory for applicants.
325    """
326    grok.implements(IFactory)
327    grok.name(u'waeup.Applicant')
328    title = u"Create a new applicant.",
329    description = u"This factory instantiates new applicant instances."
330
331    def __call__(self, *args, **kw):
332        return Applicant()
333
334    def getInterfaces(self):
335        return implementedBy(Applicant)
336
337
338#: The file id marker for applicant passport images
339APPLICANT_IMAGE_STORE_NAME = 'img-applicant'
340
341class ApplicantImageNameChooser(grok.Adapter):
342    """A file id chooser for :class:`Applicant` objects.
343
344    `context` is an :class:`Applicant` instance.
345
346    The :class:`ApplicantImageNameChooser` can build/check file ids
347    for :class:`Applicant` objects suitable for use with
348    :class:`ExtFileStore` instances. The delivered file_id contains
349    the file id marker for :class:`Applicant` object and the
350    registration number or access code of the context applicant. Also
351    the name of the connected applicant container will be part of the
352    generated file id.
353
354    This chooser is registered as an adapter providing
355    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
356
357    File store name choosers like this one are only convenience
358    components to ease the task of creating file ids for applicant
359    objects. You are nevertheless encouraged to use them instead of
360    manually setting up filenames for applicants.
361
362    .. seealso:: :mod:`waeup.kofa.imagestorage`
363
364    """
365    grok.context(IApplicant)
366    grok.implements(IFileStoreNameChooser)
367
368    def checkName(self, name=None, attr=None):
369        """Check whether the given name is a valid file id for the context.
370
371        Returns ``True`` only if `name` equals the result of
372        :meth:`chooseName`.
373
374        The `attr` parameter is not taken into account for
375        :class:`Applicant` context as the single passport image is the
376        only file we store for applicants.
377        """
378        return name == self.chooseName()
379
380    def chooseName(self, name=None, attr=None):
381        """Get a valid file id for applicant context.
382
383        *Example:*
384
385        For an applicant with applicant_id. ``'app2001_1234'``
386        and stored in an applicants container called
387        ``'mycontainer'``, this chooser would create:
388
389          ``'__img-applicant__mycontainer/app2001_1234.jpg'``
390
391        meaning that the passport image of this applicant would be
392        stored in the site-wide file storage in path:
393
394          ``mycontainer/app2001_1234.jpg``
395
396        If the context applicant has no parent, ``'_default'`` is used
397        as parent name.
398
399        In the beginning the `attr` parameter was not taken into account for
400        :class:`Applicant` context as the single passport image was the
401        only file we store for applicants. Meanwhile many universities require
402        uploads of other documents too. Now we store passport image
403        files without attribute but all other documents with.
404
405        """
406        parent_name = getattr(
407            getattr(self.context, '__parent__', None),
408            '__name__', '_default')
409        if attr is None or attr == 'passport.jpg':
410            marked_filename = '__%s__%s/%s.jpg' % (
411                APPLICANT_IMAGE_STORE_NAME,
412                parent_name, self.context.applicant_id)
413        else:
414            basename, ext = os.path.splitext(attr)
415            marked_filename = '__%s__%s/%s_%s%s' % (
416                APPLICANT_IMAGE_STORE_NAME,
417                parent_name, basename, self.context.applicant_id, ext)
418        return marked_filename
419
420
421class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
422    """Applicant specific image handling.
423
424    This handler knows in which path in a filestore to store applicant
425    images and how to turn this kind of data into some (browsable)
426    file object.
427
428    It is called from the global file storage, when it wants to
429    get/store a file with a file id starting with
430    ``__img-applicant__`` (the marker string for applicant images).
431
432    Like each other file store handler it does not handle the files
433    really (this is done by the global file store) but only computes
434    paths and things like this.
435    """
436    grok.implements(IFileStoreHandler)
437    grok.name(APPLICANT_IMAGE_STORE_NAME)
438
439    def pathFromFileID(self, store, root, file_id):
440        """All applicants images are filed in directory ``applicants``.
441        """
442        marker, filename, basename, ext = store.extractMarker(file_id)
443        sub_root = os.path.join(root, 'applicants')
444        return super(ApplicantImageStoreHandler, self).pathFromFileID(
445            store, sub_root, basename)
446
447    def createFile(self, store, root, filename, file_id, file):
448        """Create a browsable file-like object.
449        """
450        # call super method to ensure that any old files with
451        # different filename extension are deleted.
452        file, path, file_obj =  super(
453            ApplicantImageStoreHandler, self).createFile(
454            store, root,  filename, file_id, file)
455        return file, path, KofaImageFile(
456            file_obj.filename, file_obj.data)
457
458@grok.subscribe(IApplicant, grok.IObjectAddedEvent)
459def handle_applicant_added(applicant, event):
460    """If an applicant is added local and site roles are assigned.
461    """
462    role_manager = IPrincipalRoleManager(applicant)
463    role_manager.assignRoleToPrincipal(
464        'waeup.local.ApplicationOwner', applicant.applicant_id)
465    # Assign current principal the global Applicant role
466    role_manager = IPrincipalRoleManager(grok.getSite())
467    role_manager.assignRoleToPrincipal(
468        'waeup.Applicant', applicant.applicant_id)
469    if applicant.state is None:
470        IWorkflowInfo(applicant).fireTransition('init')
471
472    # Assign global applicant role for new applicant (alternative way)
473    #account = IUserAccount(applicant)
474    #account.roles = ['waeup.Applicant']
475
476    return
477
478@grok.subscribe(IApplicant, grok.IObjectRemovedEvent)
479def handle_applicant_removed(applicant, event):
480    """If an applicant is removed a message is logged, passport images are
481    deleted and the global role is unset.
482    """
483    comment = 'Application record removed'
484    target = applicant.applicant_id
485    try:
486        grok.getSite()['applicants'].logger.info('%s - %s' % (
487            target, comment))
488    except KeyError:
489        # If we delete an entire university instance there won't be
490        # an applicants subcontainer
491        return
492    # Remove any passport image.
493    file_store = getUtility(IExtFileStore)
494    file_store.deleteFileByContext(applicant)
495    # Remove all other files too
496    filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
497    for filename in filenames:
498        file_store.deleteFileByContext(applicant, attr=filename[1])
499    # Remove global role
500    role_manager = IPrincipalRoleManager(grok.getSite())
501    role_manager.unsetRoleForPrincipal(
502        'waeup.Applicant', applicant.applicant_id)
503    return
Note: See TracBrowser for help on using the repository browser.