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

Last change on this file since 15940 was 15932, checked in by Henrik Bettermann, 6 years ago

Reset _curr_stud_id if student could not be created.

  • Property svn:keywords set to Id
File size: 16.7 KB
RevLine 
[7192]1## $Id: applicant.py 15932 2020-01-16 11:04:43Z 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 (
42    IApplicant, IApplicantEdit, ISpecialApplicant)
[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):
[8311]155        """Create a student, fill with base data, create an application slip
156        and copy applicant data.
[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)
[7421]220        # Update the catalog
221        notify(grok.ObjectModifiedEvent(student))
[7391]222        # Save application slip
[12565]223        applicant_slip = getAdapter(self, IPDF, name='application_slip')(
224            view=view)
[12395]225        self._saveApplicationPDF(student, applicant_slip, view=view)
226        return True, _('Student ${a} created', mapping = {'a':student.student_id})
[7355]227
[12395]228    def _saveApplicationPDF(self, student, applicant_slip, view=None):
[7391]229        """Create an application slip as PDF and store it in student folder.
230        """
231        file_store = getUtility(IExtFileStore)
232        file_id = IFileStoreNameChooser(student).chooseName(
233            attr="application_slip.pdf")
234        file_store.createFile(file_id, StringIO(applicant_slip))
235        return
236
[7388]237    def _copyPassportImage(self, student):
238        """Copy any passport image over to student location.
239        """
240        file_store = getUtility(IExtFileStore)
241        appl_file = file_store.getFileByContext(self)
[7417]242        if appl_file is None:
243            return
[7388]244        stud_file_id = IFileStoreNameChooser(student).chooseName(
245            attr="passport.jpg")
[7417]246        file_store.createFile(stud_file_id, appl_file)
[7388]247        return
248
[5983]249# Set all attributes of Applicant required in IApplicant as field
250# properties. Doing this, we do not have to set initial attributes
251# ourselves and as a bonus we get free validation when an attribute is
252# set.
[6115]253Applicant = attrs_to_fields(Applicant)
[5272]254
[8404]255class ApplicantsCatalog(grok.Indexes):
[6115]256    """A catalog indexing :class:`Applicant` instances in the ZODB.
257    """
[9217]258    grok.site(IUniversity)
[6115]259    grok.name('applicants_catalog')
260    grok.context(IApplicant)
261
[8404]262    fullname = index.Text(attribute='display_fullname')
[7240]263    applicant_id = index.Field(attribute='applicant_id')
[7270]264    reg_number = index.Field(attribute='reg_number')
[8039]265    email = index.Field(attribute='email')
[8635]266    state = index.Field(attribute='state')
[13216]267    container_code = index.Field(attribute='container_code')
[6115]268
[5318]269class ApplicantFactory(grok.GlobalUtility):
[6620]270    """A factory for applicants.
[5318]271    """
272    grok.implements(IFactory)
273    grok.name(u'waeup.Applicant')
274    title = u"Create a new applicant.",
275    description = u"This factory instantiates new applicant instances."
276
277    def __call__(self, *args, **kw):
[7260]278        return Applicant()
[5318]279
280    def getInterfaces(self):
[5479]281        return implementedBy(Applicant)
[7063]282
283
284#: The file id marker for applicant passport images
285APPLICANT_IMAGE_STORE_NAME = 'img-applicant'
286
287class ApplicantImageNameChooser(grok.Adapter):
288    """A file id chooser for :class:`Applicant` objects.
289
290    `context` is an :class:`Applicant` instance.
291
292    The :class:`ApplicantImageNameChooser` can build/check file ids
293    for :class:`Applicant` objects suitable for use with
294    :class:`ExtFileStore` instances. The delivered file_id contains
295    the file id marker for :class:`Applicant` object and the
296    registration number or access code of the context applicant. Also
297    the name of the connected applicant container will be part of the
298    generated file id.
299
300    This chooser is registered as an adapter providing
[7811]301    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
[7063]302
303    File store name choosers like this one are only convenience
304    components to ease the task of creating file ids for applicant
305    objects. You are nevertheless encouraged to use them instead of
306    manually setting up filenames for applicants.
307
[7811]308    .. seealso:: :mod:`waeup.kofa.imagestorage`
[7063]309
310    """
311    grok.context(IApplicant)
312    grok.implements(IFileStoreNameChooser)
313
[7067]314    def checkName(self, name=None, attr=None):
[7063]315        """Check whether the given name is a valid file id for the context.
316
317        Returns ``True`` only if `name` equals the result of
318        :meth:`chooseName`.
[7067]319
320        The `attr` parameter is not taken into account for
321        :class:`Applicant` context as the single passport image is the
322        only file we store for applicants.
[7063]323        """
324        return name == self.chooseName()
325
[7105]326    def chooseName(self, name=None, attr=None):
[7063]327        """Get a valid file id for applicant context.
328
329        *Example:*
330
[7240]331        For an applicant with applicant_id. ``'app2001_1234'``
[7063]332        and stored in an applicants container called
333        ``'mycontainer'``, this chooser would create:
334
[7240]335          ``'__img-applicant__mycontainer/app2001_1234.jpg'``
[7063]336
337        meaning that the passport image of this applicant would be
338        stored in the site-wide file storage in path:
339
[7240]340          ``mycontainer/app2001_1234.jpg``
[7063]341
342        If the context applicant has no parent, ``'_default'`` is used
343        as parent name.
[7067]344
[10089]345        In the beginning the `attr` parameter was not taken into account for
346        :class:`Applicant` context as the single passport image was the
347        only file we store for applicants. Meanwhile FUTMinna requires
348        uploads of other documents too. Now we store passport image
349        files without attribute but all other documents with.
[7067]350
[7063]351        """
352        parent_name = getattr(
353            getattr(self.context, '__parent__', None),
354            '__name__', '_default')
[10089]355        if attr is None or attr == 'passport.jpg':
356            marked_filename = '__%s__%s/%s.jpg' % (
357                APPLICANT_IMAGE_STORE_NAME,
358                parent_name, self.context.applicant_id)
359        else:
360            basename, ext = os.path.splitext(attr)
361            marked_filename = '__%s__%s/%s_%s%s' % (
362                APPLICANT_IMAGE_STORE_NAME,
[10092]363                parent_name, basename, self.context.applicant_id, ext)
[7063]364        return marked_filename
365
366
367class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
368    """Applicant specific image handling.
369
370    This handler knows in which path in a filestore to store applicant
371    images and how to turn this kind of data into some (browsable)
372    file object.
373
374    It is called from the global file storage, when it wants to
375    get/store a file with a file id starting with
376    ``__img-applicant__`` (the marker string for applicant images).
377
378    Like each other file store handler it does not handle the files
379    really (this is done by the global file store) but only computes
380    paths and things like this.
381    """
382    grok.implements(IFileStoreHandler)
383    grok.name(APPLICANT_IMAGE_STORE_NAME)
384
385    def pathFromFileID(self, store, root, file_id):
386        """All applicants images are filed in directory ``applicants``.
387        """
388        marker, filename, basename, ext = store.extractMarker(file_id)
[7121]389        sub_root = os.path.join(root, 'applicants')
390        return super(ApplicantImageStoreHandler, self).pathFromFileID(
391            store, sub_root, basename)
[7063]392
393    def createFile(self, store, root, filename, file_id, file):
394        """Create a browsable file-like object.
395        """
[7121]396        # call super method to ensure that any old files with
397        # different filename extension are deleted.
398        file, path, file_obj =  super(
399            ApplicantImageStoreHandler, self).createFile(
400            store, root,  filename, file_id, file)
[7819]401        return file, path, KofaImageFile(
[7121]402            file_obj.filename, file_obj.data)
[7240]403
404@grok.subscribe(IApplicant, grok.IObjectAddedEvent)
405def handle_applicant_added(applicant, event):
406    """If an applicant is added local and site roles are assigned.
407    """
408    role_manager = IPrincipalRoleManager(applicant)
409    role_manager.assignRoleToPrincipal(
410        'waeup.local.ApplicationOwner', applicant.applicant_id)
411    # Assign current principal the global Applicant role
412    role_manager = IPrincipalRoleManager(grok.getSite())
413    role_manager.assignRoleToPrincipal(
414        'waeup.Applicant', applicant.applicant_id)
[8375]415    if applicant.state is None:
416        IWorkflowInfo(applicant).fireTransition('init')
[7240]417
418    # Assign global applicant role for new applicant (alternative way)
419    #account = IUserAccount(applicant)
420    #account.roles = ['waeup.Applicant']
421
422    return
423
424@grok.subscribe(IApplicant, grok.IObjectRemovedEvent)
425def handle_applicant_removed(applicant, event):
[13224]426    """If an applicant is removed a message is logged, passport images are
427    deleted and the global role is unset.
[7240]428    """
[13073]429    comment = 'Application record removed'
[7240]430    target = applicant.applicant_id
431    try:
[7652]432        grok.getSite()['applicants'].logger.info('%s - %s' % (
433            target, comment))
[7240]434    except KeyError:
435        # If we delete an entire university instance there won't be
436        # an applicants subcontainer
437        return
[13224]438    # Remove any passport image.
[8285]439    file_store = getUtility(IExtFileStore)
440    file_store.deleteFileByContext(applicant)
[13224]441    # Remove global role
442    role_manager = IPrincipalRoleManager(grok.getSite())
443    role_manager.unsetRoleForPrincipal(
444        'waeup.Applicant', applicant.applicant_id)
[7240]445    return
Note: See TracBrowser for help on using the repository browser.