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

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

Make provision against storing other objects than applicant payments in applicant containers.

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