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

Last change on this file since 15995 was 15943, checked in by Henrik Bettermann, 5 years ago

Provide components for file uploads in the applicants section.

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