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

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

Do it right.

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