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

Last change on this file since 13698 was 13224, checked in by Henrik Bettermann, 9 years ago

Remove global roles after deletion of applicants and students.

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