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

Last change on this file since 16069 was 16016, checked in by Henrik Bettermann, 5 years ago

Do not return but continue.

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