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

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

Be more verbose.

  • Property svn:keywords set to Id
File size: 19.3 KB
Line 
1## $Id: applicant.py 16282 2020-10-12 17:52:04Z 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            stud_file_id = IFileStoreNameChooser(student).chooseName(
302                attr=filename[1])
303            file_store.createFile(stud_file_id, appl_file)
304        return
305
306# Set all attributes of Applicant required in IApplicant as field
307# properties. Doing this, we do not have to set initial attributes
308# ourselves and as a bonus we get free validation when an attribute is
309# set.
310Applicant = attrs_to_fields(Applicant)
311
312class ApplicantsCatalog(grok.Indexes):
313    """A catalog indexing :class:`Applicant` instances in the ZODB.
314    """
315    grok.site(IUniversity)
316    grok.name('applicants_catalog')
317    grok.context(IApplicant)
318
319    fullname = index.Text(attribute='display_fullname')
320    applicant_id = index.Field(attribute='applicant_id')
321    reg_number = index.Field(attribute='reg_number')
322    email = index.Field(attribute='email')
323    state = index.Field(attribute='state')
324    container_code = index.Field(attribute='container_code')
325
326class ApplicantFactory(grok.GlobalUtility):
327    """A factory for applicants.
328    """
329    grok.implements(IFactory)
330    grok.name(u'waeup.Applicant')
331    title = u"Create a new applicant.",
332    description = u"This factory instantiates new applicant instances."
333
334    def __call__(self, *args, **kw):
335        return Applicant()
336
337    def getInterfaces(self):
338        return implementedBy(Applicant)
339
340
341#: The file id marker for applicant passport images
342APPLICANT_IMAGE_STORE_NAME = 'img-applicant'
343
344class ApplicantImageNameChooser(grok.Adapter):
345    """A file id chooser for :class:`Applicant` objects.
346
347    `context` is an :class:`Applicant` instance.
348
349    The :class:`ApplicantImageNameChooser` can build/check file ids
350    for :class:`Applicant` objects suitable for use with
351    :class:`ExtFileStore` instances. The delivered file_id contains
352    the file id marker for :class:`Applicant` object and the
353    registration number or access code of the context applicant. Also
354    the name of the connected applicant container will be part of the
355    generated file id.
356
357    This chooser is registered as an adapter providing
358    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
359
360    File store name choosers like this one are only convenience
361    components to ease the task of creating file ids for applicant
362    objects. You are nevertheless encouraged to use them instead of
363    manually setting up filenames for applicants.
364
365    .. seealso:: :mod:`waeup.kofa.imagestorage`
366
367    """
368    grok.context(IApplicant)
369    grok.implements(IFileStoreNameChooser)
370
371    def checkName(self, name=None, attr=None):
372        """Check whether the given name is a valid file id for the context.
373
374        Returns ``True`` only if `name` equals the result of
375        :meth:`chooseName`.
376
377        The `attr` parameter is not taken into account for
378        :class:`Applicant` context as the single passport image is the
379        only file we store for applicants.
380        """
381        return name == self.chooseName()
382
383    def chooseName(self, name=None, attr=None):
384        """Get a valid file id for applicant context.
385
386        *Example:*
387
388        For an applicant with applicant_id. ``'app2001_1234'``
389        and stored in an applicants container called
390        ``'mycontainer'``, this chooser would create:
391
392          ``'__img-applicant__mycontainer/app2001_1234.jpg'``
393
394        meaning that the passport image of this applicant would be
395        stored in the site-wide file storage in path:
396
397          ``mycontainer/app2001_1234.jpg``
398
399        If the context applicant has no parent, ``'_default'`` is used
400        as parent name.
401
402        In the beginning the `attr` parameter was not taken into account for
403        :class:`Applicant` context as the single passport image was the
404        only file we store for applicants. Meanwhile many universities require
405        uploads of other documents too. Now we store passport image
406        files without attribute but all other documents with.
407
408        """
409        parent_name = getattr(
410            getattr(self.context, '__parent__', None),
411            '__name__', '_default')
412        if attr is None or attr == 'passport.jpg':
413            marked_filename = '__%s__%s/%s.jpg' % (
414                APPLICANT_IMAGE_STORE_NAME,
415                parent_name, self.context.applicant_id)
416        else:
417            basename, ext = os.path.splitext(attr)
418            marked_filename = '__%s__%s/%s_%s%s' % (
419                APPLICANT_IMAGE_STORE_NAME,
420                parent_name, basename, self.context.applicant_id, ext)
421        return marked_filename
422
423
424class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
425    """Applicant specific image handling.
426
427    This handler knows in which path in a filestore to store applicant
428    images and how to turn this kind of data into some (browsable)
429    file object.
430
431    It is called from the global file storage, when it wants to
432    get/store a file with a file id starting with
433    ``__img-applicant__`` (the marker string for applicant images).
434
435    Like each other file store handler it does not handle the files
436    really (this is done by the global file store) but only computes
437    paths and things like this.
438    """
439    grok.implements(IFileStoreHandler)
440    grok.name(APPLICANT_IMAGE_STORE_NAME)
441
442    def pathFromFileID(self, store, root, file_id):
443        """All applicants images are filed in directory ``applicants``.
444        """
445        marker, filename, basename, ext = store.extractMarker(file_id)
446        sub_root = os.path.join(root, 'applicants')
447        return super(ApplicantImageStoreHandler, self).pathFromFileID(
448            store, sub_root, basename)
449
450    def createFile(self, store, root, filename, file_id, file):
451        """Create a browsable file-like object.
452        """
453        # call super method to ensure that any old files with
454        # different filename extension are deleted.
455        file, path, file_obj =  super(
456            ApplicantImageStoreHandler, self).createFile(
457            store, root,  filename, file_id, file)
458        return file, path, KofaImageFile(
459            file_obj.filename, file_obj.data)
460
461@grok.subscribe(IApplicant, grok.IObjectAddedEvent)
462def handle_applicant_added(applicant, event):
463    """If an applicant is added local and site roles are assigned.
464    """
465    role_manager = IPrincipalRoleManager(applicant)
466    role_manager.assignRoleToPrincipal(
467        'waeup.local.ApplicationOwner', applicant.applicant_id)
468    # Assign current principal the global Applicant role
469    role_manager = IPrincipalRoleManager(grok.getSite())
470    role_manager.assignRoleToPrincipal(
471        'waeup.Applicant', applicant.applicant_id)
472    if applicant.state is None:
473        IWorkflowInfo(applicant).fireTransition('init')
474
475    # Assign global applicant role for new applicant (alternative way)
476    #account = IUserAccount(applicant)
477    #account.roles = ['waeup.Applicant']
478
479    return
480
481@grok.subscribe(IApplicant, grok.IObjectRemovedEvent)
482def handle_applicant_removed(applicant, event):
483    """If an applicant is removed a message is logged, passport images are
484    deleted and the global role is unset.
485    """
486    comment = 'Application record removed'
487    target = applicant.applicant_id
488    try:
489        grok.getSite()['applicants'].logger.info('%s - %s' % (
490            target, comment))
491    except KeyError:
492        # If we delete an entire university instance there won't be
493        # an applicants subcontainer
494        return
495    # Remove any passport image.
496    file_store = getUtility(IExtFileStore)
497    file_store.deleteFileByContext(applicant)
498    # Remove all other files too
499    filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
500    for filename in filenames:
501        file_store.deleteFileByContext(applicant, attr=filename[1])
502    # Remove global role
503    role_manager = IPrincipalRoleManager(grok.getSite())
504    role_manager.unsetRoleForPrincipal(
505        'waeup.Applicant', applicant.applicant_id)
506    return
Note: See TracBrowser for help on using the repository browser.