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

Last change on this file since 17054 was 17054, checked in by Henrik Bettermann, 2 years ago

Catch error.

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