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

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

Be more verbose.

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