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

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

Simplify logging. Adjust to students.

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