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

Last change on this file since 13144 was 13080, checked in by Henrik Bettermann, 10 years ago

Improve interfaces for documentation.

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