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

Last change on this file since 16939 was 16598, checked in by Henrik Bettermann, 3 years ago

Catch UnicodeEncodeError?.

  • Property svn:keywords set to Id
File size: 19.9 KB
Line 
1## $Id: applicant.py 16598 2021-09-02 08:49:14Z 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        # Does registration number exist?
191        student = createObject(u'waeup.Student')
192        try:
193            student.reg_number = self.reg_number
194        except RegNumNotInSource:
195            # Reset _curr_stud_id
196            site['students']._curr_stud_id -= 1
197            return False, _('Registration Number exists.')
198        # Has the course_admitted/course_studied field been properly filled?
199        if certificate is None:
200            # Reset _curr_stud_id
201            site['students']._curr_stud_id -= 1
202            return False, _('No study course provided.')
203        # Set student attributes
204        try:
205            for item in mapping:
206                if item[0] == 'email':
207                    # The field type in the application section might be
208                    # TextLineChoice to guarantee uniqueness of email addresses.
209                    setattr(student, item[1], str(getattr(self, item[0], None)))
210                else:
211                    setattr(student, item[1], getattr(self, item[0], None))
212        except RequiredMissing, err:
213            site['students']._curr_stud_id -= 1
214            return False, 'RequiredMissing: %s' % err
215        except WrongType, err:
216            site['students']._curr_stud_id -= 1
217            return False, 'WrongType: %s' % err
218        except UnicodeEncodeError, err:
219            return False, 'UnicodeEncodeError: %s' % err
220        except:
221            site['students']._curr_stud_id -= 1
222            return False, 'Unknown Error'
223        # Prove if the certificate still exists
224        try:
225            StudentStudyCourse().certificate = certificate
226        except ConstraintNotSatisfied, err:
227            # Reset _curr_stud_id
228            site['students']._curr_stud_id -= 1
229            return False, 'ConstraintNotSatisfied: %s' % certificate.code
230        # Finally prove if an application slip can be created
231        try:
232            test_applicant_slip = getAdapter(self, IPDF, name='application_slip')(
233                view=view)
234        except IOError:
235            site['students']._curr_stud_id -= 1
236            return False, _('IOError: Application Slip could not be created.')
237        except LayoutError, err:
238            site['students']._curr_stud_id -= 1
239            return False, _('Reportlab LayoutError: %s' % err)
240        except AttributeError, err:
241            site['students']._curr_stud_id -= 1
242            return False, _('Reportlab AttributeError: ${a}', mapping = {'a':err})
243        except:
244            site['students']._curr_stud_id -= 1
245            return False, _('Unknown Reportlab error')
246        # Add student
247        site['students'].addStudent(student)
248        # Save student_id
249        self.student_id = student.student_id
250        if graduated:
251            # Set state
252            IWorkflowState(student).setState('graduated')
253            # Save the certificate and set study course attributes
254            self._setGraduatedStudyCourseAttributes(student['studycourse'])
255        else:
256            # Fire transitions
257            IWorkflowInfo(self).fireTransition('create')
258            IWorkflowInfo(student).fireTransition('admit')
259            # Save the certificate and set study course attributes
260            self._setStudyCourseAttributes(student['studycourse'])
261        # Set password
262        IUserAccount(student).setPassword(self.application_number)
263        self._copyPassportImage(student)
264        self._copyFiles(student)
265        # Update the catalog
266        notify(grok.ObjectModifiedEvent(student))
267        # Save application slip
268        applicant_slip = getAdapter(self, IPDF, name='application_slip')(
269            view=view)
270        self._saveApplicationPDF(student, applicant_slip, view=view)
271        if view and send_email:
272            kofa_utils = getUtility(IKofaUtils)
273            pw = self.application_number
274            args = {'login':student.student_id, 'password':pw}
275            login_url = view.url(grok.getSite()) + '/login?%s' % urlencode(args)
276            rpw_url = view.url(grok.getSite(), 'changepw')
277            kofa_utils.informNewStudent(
278                IUserAccount(student), pw, login_url, rpw_url)
279        return True, _('Student ${a} created', mapping = {'a':student.student_id})
280
281    def _saveApplicationPDF(self, student, applicant_slip, view=None):
282        """Create an application slip as PDF and store it in student folder.
283        """
284        file_store = getUtility(IExtFileStore)
285        file_id = IFileStoreNameChooser(student).chooseName(
286            attr="application_slip.pdf")
287        file_store.createFile(file_id, StringIO(applicant_slip))
288        return
289
290    def _copyPassportImage(self, student):
291        """Copy any passport image over to student location.
292        """
293        file_store = getUtility(IExtFileStore)
294        appl_file = file_store.getFileByContext(self)
295        if appl_file is None:
296            return
297        stud_file_id = IFileStoreNameChooser(student).chooseName(
298            attr="passport.jpg")
299        file_store.createFile(stud_file_id, appl_file)
300        return
301
302    def _copyFiles(self, student):
303        """Copy all other files over to student location. Not
304        used in base package but tested with fake file.
305        """
306        file_store = getUtility(IExtFileStore)
307        filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
308        for filename in filenames:
309            appl_file = file_store.getFileByContext(self, attr=filename[1])
310            if appl_file is None:
311                continue
312            ext = os.path.splitext(appl_file.name)[1]
313            stud_file_id = IFileStoreNameChooser(student).chooseName(
314                attr=filename[1] + ext)
315            file_store.createFile(stud_file_id, appl_file)
316        return
317
318# Set all attributes of Applicant required in IApplicant as field
319# properties. Doing this, we do not have to set initial attributes
320# ourselves and as a bonus we get free validation when an attribute is
321# set.
322Applicant = attrs_to_fields(Applicant)
323
324class ApplicantsCatalog(grok.Indexes):
325    """A catalog indexing :class:`Applicant` instances in the ZODB.
326    """
327    grok.site(IUniversity)
328    grok.name('applicants_catalog')
329    grok.context(IApplicant)
330
331    fullname = index.Text(attribute='display_fullname')
332    applicant_id = index.Field(attribute='applicant_id')
333    reg_number = index.Field(attribute='reg_number')
334    email = index.Field(attribute='email')
335    state = index.Field(attribute='state')
336    container_code = index.Field(attribute='container_code')
337
338class ApplicantFactory(grok.GlobalUtility):
339    """A factory for applicants.
340    """
341    grok.implements(IFactory)
342    grok.name(u'waeup.Applicant')
343    title = u"Create a new applicant.",
344    description = u"This factory instantiates new applicant instances."
345
346    def __call__(self, *args, **kw):
347        return Applicant()
348
349    def getInterfaces(self):
350        return implementedBy(Applicant)
351
352
353#: The file id marker for applicant passport images
354APPLICANT_IMAGE_STORE_NAME = 'img-applicant'
355
356class ApplicantImageNameChooser(grok.Adapter):
357    """A file id chooser for :class:`Applicant` objects.
358
359    `context` is an :class:`Applicant` instance.
360
361    The :class:`ApplicantImageNameChooser` can build/check file ids
362    for :class:`Applicant` objects suitable for use with
363    :class:`ExtFileStore` instances. The delivered file_id contains
364    the file id marker for :class:`Applicant` object and the
365    registration number or access code of the context applicant. Also
366    the name of the connected applicant container will be part of the
367    generated file id.
368
369    This chooser is registered as an adapter providing
370    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
371
372    File store name choosers like this one are only convenience
373    components to ease the task of creating file ids for applicant
374    objects. You are nevertheless encouraged to use them instead of
375    manually setting up filenames for applicants.
376
377    .. seealso:: :mod:`waeup.kofa.imagestorage`
378
379    """
380    grok.context(IApplicant)
381    grok.implements(IFileStoreNameChooser)
382
383    def checkName(self, name=None, attr=None):
384        """Check whether the given name is a valid file id for the context.
385
386        Returns ``True`` only if `name` equals the result of
387        :meth:`chooseName`.
388
389        The `attr` parameter is not taken into account for
390        :class:`Applicant` context as the single passport image is the
391        only file we store for applicants.
392        """
393        return name == self.chooseName()
394
395    def chooseName(self, name=None, attr=None):
396        """Get a valid file id for applicant context.
397
398        *Example:*
399
400        For an applicant with applicant_id. ``'app2001_1234'``
401        and stored in an applicants container called
402        ``'mycontainer'``, this chooser would create:
403
404          ``'__img-applicant__mycontainer/app2001_1234.jpg'``
405
406        meaning that the passport image of this applicant would be
407        stored in the site-wide file storage in path:
408
409          ``mycontainer/app2001_1234.jpg``
410
411        If the context applicant has no parent, ``'_default'`` is used
412        as parent name.
413
414        In the beginning the `attr` parameter was not taken into account for
415        :class:`Applicant` context as the single passport image was the
416        only file we store for applicants. Meanwhile many universities require
417        uploads of other documents too. Now we store passport image
418        files without attribute but all other documents with.
419
420        """
421        parent_name = getattr(
422            getattr(self.context, '__parent__', None),
423            '__name__', '_default')
424        if attr is None or attr == 'passport.jpg':
425            marked_filename = '__%s__%s/%s.jpg' % (
426                APPLICANT_IMAGE_STORE_NAME,
427                parent_name, self.context.applicant_id)
428        else:
429            basename, ext = os.path.splitext(attr)
430            marked_filename = '__%s__%s/%s_%s%s' % (
431                APPLICANT_IMAGE_STORE_NAME,
432                parent_name, basename, self.context.applicant_id, ext)
433        return marked_filename
434
435
436class ApplicantImageStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
437    """Applicant specific image handling.
438
439    This handler knows in which path in a filestore to store applicant
440    images and how to turn this kind of data into some (browsable)
441    file object.
442
443    It is called from the global file storage, when it wants to
444    get/store a file with a file id starting with
445    ``__img-applicant__`` (the marker string for applicant images).
446
447    Like each other file store handler it does not handle the files
448    really (this is done by the global file store) but only computes
449    paths and things like this.
450    """
451    grok.implements(IFileStoreHandler)
452    grok.name(APPLICANT_IMAGE_STORE_NAME)
453
454    def pathFromFileID(self, store, root, file_id):
455        """All applicants images are filed in directory ``applicants``.
456        """
457        marker, filename, basename, ext = store.extractMarker(file_id)
458        sub_root = os.path.join(root, 'applicants')
459        return super(ApplicantImageStoreHandler, self).pathFromFileID(
460            store, sub_root, basename)
461
462    def createFile(self, store, root, filename, file_id, file):
463        """Create a browsable file-like object.
464        """
465        # call super method to ensure that any old files with
466        # different filename extension are deleted.
467        file, path, file_obj =  super(
468            ApplicantImageStoreHandler, self).createFile(
469            store, root,  filename, file_id, file)
470        return file, path, KofaImageFile(
471            file_obj.filename, file_obj.data)
472
473@grok.subscribe(IApplicant, grok.IObjectAddedEvent)
474def handle_applicant_added(applicant, event):
475    """If an applicant is added local and site roles are assigned.
476    """
477    role_manager = IPrincipalRoleManager(applicant)
478    role_manager.assignRoleToPrincipal(
479        'waeup.local.ApplicationOwner', applicant.applicant_id)
480    # Assign current principal the global Applicant role
481    role_manager = IPrincipalRoleManager(grok.getSite())
482    role_manager.assignRoleToPrincipal(
483        'waeup.Applicant', applicant.applicant_id)
484    if applicant.state is None:
485        IWorkflowInfo(applicant).fireTransition('init')
486
487    # Assign global applicant role for new applicant (alternative way)
488    #account = IUserAccount(applicant)
489    #account.roles = ['waeup.Applicant']
490
491    return
492
493@grok.subscribe(IApplicant, grok.IObjectRemovedEvent)
494def handle_applicant_removed(applicant, event):
495    """If an applicant is removed a message is logged, passport images are
496    deleted and the global role is unset.
497    """
498    comment = 'Application record removed'
499    target = applicant.applicant_id
500    try:
501        grok.getSite()['applicants'].logger.info('%s - %s' % (
502            target, comment))
503    except KeyError:
504        # If we delete an entire university instance there won't be
505        # an applicants subcontainer
506        return
507    # Remove any passport image.
508    file_store = getUtility(IExtFileStore)
509    file_store.deleteFileByContext(applicant)
510    # Remove all other files too
511    filenames = getUtility(IApplicantsUtils).ADDITIONAL_FILES
512    for filename in filenames:
513        file_store.deleteFileByContext(applicant, attr=filename[1])
514    # Remove global role
515    role_manager = IPrincipalRoleManager(grok.getSite())
516    role_manager.unsetRoleForPrincipal(
517        'waeup.Applicant', applicant.applicant_id)
518    return
Note: See TracBrowser for help on using the repository browser.