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

Last change on this file since 13972 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
RevLine 
[7192]1## $Id: applicant.py 13968 2016-06-22 04:39:50Z henrik $
[5272]2##
[7192]3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
[5272]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.
[7192]8##
[5272]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.
[7192]13##
[5272]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##
[7063]18import os
[5272]19import grok
[7391]20from cStringIO import StringIO
[6115]21from grok import index
[8404]22from hurry.query import Eq, Text
23from hurry.query.query import Query
[7438]24from zope.component import getUtility, createObject, getAdapter
[5318]25from zope.component.interfaces import IFactory
[7421]26from zope.event import notify
[7240]27from zope.securitypolicy.interfaces import IPrincipalRoleManager
[5479]28from zope.interface import implementedBy
[8843]29from zope.schema.interfaces import RequiredMissing, ConstraintNotSatisfied
[6296]30from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
[7819]31from waeup.kofa.image import KofaImageFile
[7811]32from waeup.kofa.imagestorage import DefaultFileStoreHandler
33from waeup.kofa.interfaces import (
[7819]34    IObjectHistory, IFileStoreHandler, IFileStoreNameChooser, IKofaUtils,
[9217]35    IExtFileStore, IPDF, IUserAccount, IUniversity)
[8311]36from waeup.kofa.interfaces import MessageFactory as _
[7811]37from waeup.kofa.students.vocabularies import RegNumNotInSource
[8843]38from waeup.kofa.students.studycourse import StudentStudyCourse
[7811]39from waeup.kofa.utils.helpers import attrs_to_fields
[10845]40from waeup.kofa.applicants.interfaces import (
41    IApplicant, IApplicantEdit, ISpecialApplicant)
[7811]42from waeup.kofa.applicants.workflow import application_states_dict
[13968]43from waeup.kofa.applicants.payment import ApplicantOnlinePayment
[5272]44
[8404]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
[7250]54class Applicant(grok.Container):
[10845]55    grok.implements(IApplicant, IApplicantEdit, ISpecialApplicant)
[5483]56    grok.provides(IApplicant)
[5272]57
[11615]58    applicant_student_mapping = [
[11614]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'),
[11615]66        ]
[8667]67
[7260]68    def __init__(self):
[6296]69        super(Applicant, self).__init__()
[7240]70        self.password = None
[6476]71        self.application_date = None
[7260]72        self.applicant_id = None
[6331]73        return
[6296]74
[13968]75    @property
76    def payments(self):
77        payments = [value for value in self.values()
78            if isinstance(value, ApplicantOnlinePayment)]
79        return payments
80
[8742]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
[6475]86
[6324]87    @property
[8286]88    def state(self):
[7686]89        return IWorkflowState(self).getState()
[6324]90
[6339]91    @property
[8377]92    def container_code(self):
[13080]93        try:
94            code = self.__parent__.code
95        except AttributeError:  # in unit tests
96            return
[13216]97        if (self.password,
98            self.firstname,
99            self.lastname,
100            self.email) == (None, None, None, None):
101            return code + '-'
102        return code + '+'
[8377]103
104    @property
[7686]105    def translated_state(self):
[13080]106        try:
107            state = application_states_dict[self.state]
108        except LookupError:  # in unit tests
109            return
110        return state
[7686]111
112    @property
[6339]113    def history(self):
114        history = IObjectHistory(self)
115        return history
116
[7240]117    @property
[10831]118    def special(self):
[13080]119        try:
120            special = self.__parent__.prefix.startswith('special')
121        except AttributeError:  # in unit tests
122            return
123        return special
[10831]124
125    @property
[7240]126    def application_number(self):
[7260]127        try:
128            return self.applicant_id.split('_')[1]
129        except AttributeError:
130            return None
[7240]131
132    @property
[7364]133    def display_fullname(self):
[7356]134        middlename = getattr(self, 'middlename', None)
[7819]135        kofa_utils = getUtility(IKofaUtils)
[7811]136        return kofa_utils.fullname(self.firstname, self.lastname, middlename)
[7240]137
[9480]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
[7391]146    def createStudent(self, view=None):
[8311]147        """Create a student, fill with base data, create an application slip
148        and copy applicant data.
[7338]149        """
[7344]150        # Is applicant in the correct state?
[8286]151        if self.state != 'admitted':
[8311]152            return False, _('Applicant has not yet been admitted.')
[7344]153        # Does registration number exist?
[7338]154        student = createObject(u'waeup.Student')
[7341]155        try:
156            student.reg_number = self.reg_number
157        except RegNumNotInSource:
[8311]158            return False, _('Registration Number exists.')
[7344]159        # Has the course_admitted field been properly filled?
[7347]160        if self.course_admitted is None:
[8311]161            return False, _('No course admitted provided.')
[8843]162        # Set student attributes
[8635]163        try:
[11614]164            for item in self.applicant_student_mapping:
165                setattr(student, item[1], getattr(self, item[0], None))
[8635]166        except RequiredMissing, err:
[8855]167            return False, 'RequiredMissing: %s' % err
[8843]168        except:
[9391]169            return False, 'Unknown Error'
[12395]170        # Prove if the certificate still exists
[8843]171        try:
172            StudentStudyCourse().certificate = self.course_admitted
173        except ConstraintNotSatisfied, err:
[8855]174            return False, 'ConstraintNotSatisfied: %s' % self.course_admitted.code
[12395]175        # Finally prove if an application slip can be created
176        try:
[12565]177            test_applicant_slip = getAdapter(self, IPDF, name='application_slip')(
[12395]178                view=view)
179        except IOError:
180            return False, _('IOError: Application Slip could not be created.')
[8843]181        # Add student
[7406]182        site = grok.getSite()
183        site['students'].addStudent(student)
[9121]184        # Save student_id
185        self.student_id = student.student_id
186        # Fire transitions
[7406]187        IWorkflowInfo(self).fireTransition('create')
[8487]188        IWorkflowInfo(student).fireTransition('admit')
[7406]189        # Set password
[7409]190        IUserAccount(student).setPassword(self.application_number)
[9480]191        # Save the certificate and set study course attributes
192        self._setStudyCourseAttributes(student['studycourse'])
[7388]193        self._copyPassportImage(student)
[7421]194        # Update the catalog
195        notify(grok.ObjectModifiedEvent(student))
[7391]196        # Save application slip
[12565]197        applicant_slip = getAdapter(self, IPDF, name='application_slip')(
198            view=view)
[12395]199        self._saveApplicationPDF(student, applicant_slip, view=view)
200        return True, _('Student ${a} created', mapping = {'a':student.student_id})
[7355]201
[12395]202    def _saveApplicationPDF(self, student, applicant_slip, view=None):
[7391]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
[7388]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)
[7417]216        if appl_file is None:
217            return
[7388]218        stud_file_id = IFileStoreNameChooser(student).chooseName(
219            attr="passport.jpg")
[7417]220        file_store.createFile(stud_file_id, appl_file)
[7388]221        return
222
[5983]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.
[6115]227Applicant = attrs_to_fields(Applicant)
[5272]228
[8404]229class ApplicantsCatalog(grok.Indexes):
[6115]230    """A catalog indexing :class:`Applicant` instances in the ZODB.
231    """
[9217]232    grok.site(IUniversity)
[6115]233    grok.name('applicants_catalog')
234    grok.context(IApplicant)
235
[8404]236    fullname = index.Text(attribute='display_fullname')
[7240]237    applicant_id = index.Field(attribute='applicant_id')
[7270]238    reg_number = index.Field(attribute='reg_number')
[8039]239    email = index.Field(attribute='email')
[8635]240    state = index.Field(attribute='state')
[13216]241    container_code = index.Field(attribute='container_code')
[6115]242
[5318]243class ApplicantFactory(grok.GlobalUtility):
[6620]244    """A factory for applicants.
[5318]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):
[7260]252        return Applicant()
[5318]253
254    def getInterfaces(self):
[5479]255        return implementedBy(Applicant)
[7063]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
[7811]275    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
[7063]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
[7811]282    .. seealso:: :mod:`waeup.kofa.imagestorage`
[7063]283
284    """
285    grok.context(IApplicant)
286    grok.implements(IFileStoreNameChooser)
287
[7067]288    def checkName(self, name=None, attr=None):
[7063]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`.
[7067]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.
[7063]297        """
298        return name == self.chooseName()
299
[7105]300    def chooseName(self, name=None, attr=None):
[7063]301        """Get a valid file id for applicant context.
302
303        *Example:*
304
[7240]305        For an applicant with applicant_id. ``'app2001_1234'``
[7063]306        and stored in an applicants container called
307        ``'mycontainer'``, this chooser would create:
308
[7240]309          ``'__img-applicant__mycontainer/app2001_1234.jpg'``
[7063]310
311        meaning that the passport image of this applicant would be
312        stored in the site-wide file storage in path:
313
[7240]314          ``mycontainer/app2001_1234.jpg``
[7063]315
316        If the context applicant has no parent, ``'_default'`` is used
317        as parent name.
[7067]318
[10089]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.
[7067]324
[7063]325        """
326        parent_name = getattr(
327            getattr(self.context, '__parent__', None),
328            '__name__', '_default')
[10089]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,
[10092]337                parent_name, basename, self.context.applicant_id, ext)
[7063]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)
[7121]363        sub_root = os.path.join(root, 'applicants')
364        return super(ApplicantImageStoreHandler, self).pathFromFileID(
365            store, sub_root, basename)
[7063]366
367    def createFile(self, store, root, filename, file_id, file):
368        """Create a browsable file-like object.
369        """
[7121]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)
[7819]375        return file, path, KofaImageFile(
[7121]376            file_obj.filename, file_obj.data)
[7240]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)
[8375]389    if applicant.state is None:
390        IWorkflowInfo(applicant).fireTransition('init')
[7240]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):
[13224]400    """If an applicant is removed a message is logged, passport images are
401    deleted and the global role is unset.
[7240]402    """
[13073]403    comment = 'Application record removed'
[7240]404    target = applicant.applicant_id
405    try:
[7652]406        grok.getSite()['applicants'].logger.info('%s - %s' % (
407            target, comment))
[7240]408    except KeyError:
409        # If we delete an entire university instance there won't be
410        # an applicants subcontainer
411        return
[13224]412    # Remove any passport image.
[8285]413    file_store = getUtility(IExtFileStore)
414    file_store.deleteFileByContext(applicant)
[13224]415    # Remove global role
416    role_manager = IPrincipalRoleManager(grok.getSite())
417    role_manager.unsetRoleForPrincipal(
418        'waeup.Applicant', applicant.applicant_id)
[7240]419    return
Note: See TracBrowser for help on using the repository browser.