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

Last change on this file since 9120 was 9119, checked in by Henrik Bettermann, 12 years ago

Save student_id in application record.

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