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

Last change on this file since 16275 was 16228, checked in by Henrik Bettermann, 4 years ago

Use Applicant.createStudent method also to create graduated
students from transcript application data (not used in base package).

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