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

Last change on this file since 16545 was 16545, checked in by Henrik Bettermann, 3 years ago

Enable applicants to upload also additional jpg files.

  • Property svn:keywords set to Id
File size: 19.4 KB
Line 
1## $Id: applicant.py 16545 2021-07-13 14:08:06Z 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                if item[0] == 'email':
206                    # The field type in the application section might be
207                    # TextLineChoice to guarantee uniqueness of email addresses.
208                    setattr(student, item[1], str(getattr(self, item[0], None)))
209                else:
210                    setattr(student, item[1], getattr(self, item[0], None))
211        except RequiredMissing, err:
212            site['students']._curr_stud_id -= 1
213            return False, 'RequiredMissing: %s' % err
214        except WrongType, err:
215            site['students']._curr_stud_id -= 1
216            return False, 'WrongType: %s' % err
217        except:
218            site['students']._curr_stud_id -= 1
219            return False, 'Unknown Error'
220        # Prove if the certificate still exists
221        try:
222            StudentStudyCourse().certificate = certificate
223        except ConstraintNotSatisfied, err:
224            # Reset _curr_stud_id
225            site['students']._curr_stud_id -= 1
226            return False, 'ConstraintNotSatisfied: %s' % certificate.code
227        # Finally prove if an application slip can be created
228        try:
229            test_applicant_slip = getAdapter(self, IPDF, name='application_slip')(
230                view=view)
231        except IOError:
232            site['students']._curr_stud_id -= 1
233            return False, _('IOError: Application Slip could not be created.')
234        except LayoutError, err:
235            site['students']._curr_stud_id -= 1
236            return False, _('Reportlab LayoutError: %s' % err)
237        except AttributeError, err:
238            site['students']._curr_stud_id -= 1
239            return False, _('Reportlab AttributeError: ${a}', mapping = {'a':err})
240        except:
241            site['students']._curr_stud_id -= 1
242            return False, _('Unknown Reportlab error')
243        # Add student
244        site['students'].addStudent(student)
245        # Save student_id
246        self.student_id = student.student_id
247        if graduated:
248            # Set state
249            IWorkflowState(student).setState('graduated')
250            # Save the certificate and set study course attributes
251            self._setGraduatedStudyCourseAttributes(student['studycourse'])
252        else:
253            # Fire transitions
254            IWorkflowInfo(self).fireTransition('create')
255            IWorkflowInfo(student).fireTransition('admit')
256            # Save the certificate and set study course attributes
257            self._setStudyCourseAttributes(student['studycourse'])
258        # Set password
259        IUserAccount(student).setPassword(self.application_number)
260        self._copyPassportImage(student)
261        self._copyFiles(student)
262        # Update the catalog
263        notify(grok.ObjectModifiedEvent(student))
264        # Save application slip
265        applicant_slip = getAdapter(self, IPDF, name='application_slip')(
266            view=view)
267        self._saveApplicationPDF(student, applicant_slip, view=view)
268        return True, _('Student ${a} created', mapping = {'a':student.student_id})
269
270    def _saveApplicationPDF(self, student, applicant_slip, view=None):
271        """Create an application slip as PDF and store it in student folder.
272        """
273        file_store = getUtility(IExtFileStore)
274        file_id = IFileStoreNameChooser(student).chooseName(
275            attr="application_slip.pdf")
276        file_store.createFile(file_id, StringIO(applicant_slip))
277        return
278
279    def _copyPassportImage(self, student):
280        """Copy any passport image over to student location.
281        """
282        file_store = getUtility(IExtFileStore)
283        appl_file = file_store.getFileByContext(self)
284        if appl_file is None:
285            return
286        stud_file_id = IFileStoreNameChooser(student).chooseName(
287            attr="passport.jpg")
288        file_store.createFile(stud_file_id, appl_file)
289        return
290
291    def _copyFiles(self, student):
292        """Copy all other files over to student location. Not
293        used in base package but tested with fake file.
294        """
295        file_store = getUtility(IExtFileStore)
296        filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
297        for filename in filenames:
298            appl_file = file_store.getFileByContext(self, attr=filename[1])
299            if appl_file is None:
300                continue
301            ext = os.path.splitext(appl_file.name)[1]
302            stud_file_id = IFileStoreNameChooser(student).chooseName(
303                attr=filename[1] + ext)
304            file_store.createFile(stud_file_id, appl_file)
305        return
306
307# Set all attributes of Applicant required in IApplicant as field
308# properties. Doing this, we do not have to set initial attributes
309# ourselves and as a bonus we get free validation when an attribute is
310# set.
311Applicant = attrs_to_fields(Applicant)
312
313class ApplicantsCatalog(grok.Indexes):
314    """A catalog indexing :class:`Applicant` instances in the ZODB.
315    """
316    grok.site(IUniversity)
317    grok.name('applicants_catalog')
318    grok.context(IApplicant)
319
320    fullname = index.Text(attribute='display_fullname')
321    applicant_id = index.Field(attribute='applicant_id')
322    reg_number = index.Field(attribute='reg_number')
323    email = index.Field(attribute='email')
324    state = index.Field(attribute='state')
325    container_code = index.Field(attribute='container_code')
326
327class ApplicantFactory(grok.GlobalUtility):
328    """A factory for applicants.
329    """
330    grok.implements(IFactory)
331    grok.name(u'waeup.Applicant')
332    title = u"Create a new applicant.",
333    description = u"This factory instantiates new applicant instances."
334
335    def __call__(self, *args, **kw):
336        return Applicant()
337
338    def getInterfaces(self):
339        return implementedBy(Applicant)
340
341
342#: The file id marker for applicant passport images
343APPLICANT_IMAGE_STORE_NAME = 'img-applicant'
344
345class ApplicantImageNameChooser(grok.Adapter):
346    """A file id chooser for :class:`Applicant` objects.
347
348    `context` is an :class:`Applicant` instance.
349
350    The :class:`ApplicantImageNameChooser` can build/check file ids
351    for :class:`Applicant` objects suitable for use with
352    :class:`ExtFileStore` instances. The delivered file_id contains
353    the file id marker for :class:`Applicant` object and the
354    registration number or access code of the context applicant. Also
355    the name of the connected applicant container will be part of the
356    generated file id.
357
358    This chooser is registered as an adapter providing
359    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
360
361    File store name choosers like this one are only convenience
362    components to ease the task of creating file ids for applicant
363    objects. You are nevertheless encouraged to use them instead of
364    manually setting up filenames for applicants.
365
366    .. seealso:: :mod:`waeup.kofa.imagestorage`
367
368    """
369    grok.context(IApplicant)
370    grok.implements(IFileStoreNameChooser)
371
372    def checkName(self, name=None, attr=None):
373        """Check whether the given name is a valid file id for the context.
374
375        Returns ``True`` only if `name` equals the result of
376        :meth:`chooseName`.
377
378        The `attr` parameter is not taken into account for
379        :class:`Applicant` context as the single passport image is the
380        only file we store for applicants.
381        """
382        return name == self.chooseName()
383
384    def chooseName(self, name=None, attr=None):
385        """Get a valid file id for applicant context.
386
387        *Example:*
388
389        For an applicant with applicant_id. ``'app2001_1234'``
390        and stored in an applicants container called
391        ``'mycontainer'``, this chooser would create:
392
393          ``'__img-applicant__mycontainer/app2001_1234.jpg'``
394
395        meaning that the passport image of this applicant would be
396        stored in the site-wide file storage in path:
397
398          ``mycontainer/app2001_1234.jpg``
399
400        If the context applicant has no parent, ``'_default'`` is used
401        as parent name.
402
403        In the beginning the `attr` parameter was not taken into account for
404        :class:`Applicant` context as the single passport image was the
405        only file we store for applicants. Meanwhile many universities require
406        uploads of other documents too. Now we store passport image
407        files without attribute but all other documents with.
408
409        """
410        parent_name = getattr(
411            getattr(self.context, '__parent__', None),
412            '__name__', '_default')
413        if attr is None or attr == 'passport.jpg':
414            marked_filename = '__%s__%s/%s.jpg' % (
415                APPLICANT_IMAGE_STORE_NAME,
416                parent_name, self.context.applicant_id)
417        else:
418            basename, ext = os.path.splitext(attr)
419            marked_filename = '__%s__%s/%s_%s%s' % (
420                APPLICANT_IMAGE_STORE_NAME,
421                parent_name, basename, self.context.applicant_id, ext)
422        return marked_filename
423
424
425class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
426    """Applicant specific image handling.
427
428    This handler knows in which path in a filestore to store applicant
429    images and how to turn this kind of data into some (browsable)
430    file object.
431
432    It is called from the global file storage, when it wants to
433    get/store a file with a file id starting with
434    ``__img-applicant__`` (the marker string for applicant images).
435
436    Like each other file store handler it does not handle the files
437    really (this is done by the global file store) but only computes
438    paths and things like this.
439    """
440    grok.implements(IFileStoreHandler)
441    grok.name(APPLICANT_IMAGE_STORE_NAME)
442
443    def pathFromFileID(self, store, root, file_id):
444        """All applicants images are filed in directory ``applicants``.
445        """
446        marker, filename, basename, ext = store.extractMarker(file_id)
447        sub_root = os.path.join(root, 'applicants')
448        return super(ApplicantImageStoreHandler, self).pathFromFileID(
449            store, sub_root, basename)
450
451    def createFile(self, store, root, filename, file_id, file):
452        """Create a browsable file-like object.
453        """
454        # call super method to ensure that any old files with
455        # different filename extension are deleted.
456        file, path, file_obj =  super(
457            ApplicantImageStoreHandler, self).createFile(
458            store, root,  filename, file_id, file)
459        return file, path, KofaImageFile(
460            file_obj.filename, file_obj.data)
461
462@grok.subscribe(IApplicant, grok.IObjectAddedEvent)
463def handle_applicant_added(applicant, event):
464    """If an applicant is added local and site roles are assigned.
465    """
466    role_manager = IPrincipalRoleManager(applicant)
467    role_manager.assignRoleToPrincipal(
468        'waeup.local.ApplicationOwner', applicant.applicant_id)
469    # Assign current principal the global Applicant role
470    role_manager = IPrincipalRoleManager(grok.getSite())
471    role_manager.assignRoleToPrincipal(
472        'waeup.Applicant', applicant.applicant_id)
473    if applicant.state is None:
474        IWorkflowInfo(applicant).fireTransition('init')
475
476    # Assign global applicant role for new applicant (alternative way)
477    #account = IUserAccount(applicant)
478    #account.roles = ['waeup.Applicant']
479
480    return
481
482@grok.subscribe(IApplicant, grok.IObjectRemovedEvent)
483def handle_applicant_removed(applicant, event):
484    """If an applicant is removed a message is logged, passport images are
485    deleted and the global role is unset.
486    """
487    comment = 'Application record removed'
488    target = applicant.applicant_id
489    try:
490        grok.getSite()['applicants'].logger.info('%s - %s' % (
491            target, comment))
492    except KeyError:
493        # If we delete an entire university instance there won't be
494        # an applicants subcontainer
495        return
496    # Remove any passport image.
497    file_store = getUtility(IExtFileStore)
498    file_store.deleteFileByContext(applicant)
499    # Remove all other files too
500    filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
501    for filename in filenames:
502        file_store.deleteFileByContext(applicant, attr=filename[1])
503    # Remove global role
504    role_manager = IPrincipalRoleManager(grok.getSite())
505    role_manager.unsetRoleForPrincipal(
506        'waeup.Applicant', applicant.applicant_id)
507    return
Note: See TracBrowser for help on using the repository browser.