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

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

Basename must come first.

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