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

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

Application slip must be created again to correspond to the final slip version.

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