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

Last change on this file since 9139 was 9121, checked in by Henrik Bettermann, 12 years ago

Save student_id before firing transition.

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