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

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

Catch all errors.

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