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

Last change on this file since 11476 was 10845, checked in by Henrik Bettermann, 11 years ago

Jason is requesting customization of registration number field. This can't be done with the IApplicantBaseData based interfaces. It requires a new interface ISpecialApplicant.

Attention: The changes here are not yet compatible with the custom packages. Do not checkout!

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