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

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

Copy applicant_id into reg_number field during student record creation if applicant's reg_number is not set.

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