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

Last change on this file since 14256 was 14256, checked in by Henrik Bettermann, 8 years ago

Catch Reportlab LayoutError.

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