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

Last change on this file since 11824 was 11615, checked in by Henrik Bettermann, 11 years ago

List instead of tuple.

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