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

Last change on this file since 12500 was 12395, checked in by Henrik Bettermann, 10 years ago

Catch traceback when creating an application slip with a corrupted image file. This error caused a lot of trouble on KwaraPoly?.

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