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

Last change on this file since 14194 was 13974, checked in by Henrik Bettermann, 8 years ago

Add refereereports property attribute.

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