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

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

Copy also middlename.

  • Property svn:keywords set to Id
File size: 13.7 KB
RevLine 
[7192]1## $Id: applicant.py 9544 2012-11-05 18:38:47Z 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
280        The `attr` parameter is not taken into account for
281        :class:`Applicant` context as the single passport image is the
282        only file we store for applicants.
283
[7063]284        """
285        parent_name = getattr(
286            getattr(self.context, '__parent__', None),
287            '__name__', '_default')
288        marked_filename = '__%s__%s/%s.jpg' % (
289            APPLICANT_IMAGE_STORE_NAME,
[7240]290            parent_name, self.context.applicant_id)
[7063]291        return marked_filename
292
293
294class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
295    """Applicant specific image handling.
296
297    This handler knows in which path in a filestore to store applicant
298    images and how to turn this kind of data into some (browsable)
299    file object.
300
301    It is called from the global file storage, when it wants to
302    get/store a file with a file id starting with
303    ``__img-applicant__`` (the marker string for applicant images).
304
305    Like each other file store handler it does not handle the files
306    really (this is done by the global file store) but only computes
307    paths and things like this.
308    """
309    grok.implements(IFileStoreHandler)
310    grok.name(APPLICANT_IMAGE_STORE_NAME)
311
312    def pathFromFileID(self, store, root, file_id):
313        """All applicants images are filed in directory ``applicants``.
314        """
315        marker, filename, basename, ext = store.extractMarker(file_id)
[7121]316        sub_root = os.path.join(root, 'applicants')
317        return super(ApplicantImageStoreHandler, self).pathFromFileID(
318            store, sub_root, basename)
[7063]319
320    def createFile(self, store, root, filename, file_id, file):
321        """Create a browsable file-like object.
322        """
[7121]323        ext = os.path.splitext(filename)[1].lower()
324        if ext not in ['.jpg', '.png']:
325            raise ValueError('Only .jpg and .png allowed')
326        # call super method to ensure that any old files with
327        # different filename extension are deleted.
328        file, path, file_obj =  super(
329            ApplicantImageStoreHandler, self).createFile(
330            store, root,  filename, file_id, file)
[7819]331        return file, path, KofaImageFile(
[7121]332            file_obj.filename, file_obj.data)
[7240]333
334@grok.subscribe(IApplicant, grok.IObjectAddedEvent)
335def handle_applicant_added(applicant, event):
336    """If an applicant is added local and site roles are assigned.
337    """
338    role_manager = IPrincipalRoleManager(applicant)
339    role_manager.assignRoleToPrincipal(
340        'waeup.local.ApplicationOwner', applicant.applicant_id)
341    # Assign current principal the global Applicant role
342    role_manager = IPrincipalRoleManager(grok.getSite())
343    role_manager.assignRoleToPrincipal(
344        'waeup.Applicant', applicant.applicant_id)
[8375]345    if applicant.state is None:
346        IWorkflowInfo(applicant).fireTransition('init')
[7240]347
348    # Assign global applicant role for new applicant (alternative way)
349    #account = IUserAccount(applicant)
350    #account.roles = ['waeup.Applicant']
351
352    return
353
354@grok.subscribe(IApplicant, grok.IObjectRemovedEvent)
355def handle_applicant_removed(applicant, event):
356    """If an applicant is removed a message is logged.
357    """
358    comment = 'Applicant record removed'
359    target = applicant.applicant_id
360    try:
[7652]361        grok.getSite()['applicants'].logger.info('%s - %s' % (
362            target, comment))
[7240]363    except KeyError:
364        # If we delete an entire university instance there won't be
365        # an applicants subcontainer
366        return
[8285]367    # Remove also any passport image.
368    file_store = getUtility(IExtFileStore)
369    file_store.deleteFileByContext(applicant)
[7240]370    return
Note: See TracBrowser for help on using the repository browser.