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

Last change on this file since 9373 was 9217, checked in by uli, 12 years ago

Merge changes from uli-async-update back into trunk.

  • Property svn:keywords set to Id
File size: 13.6 KB
Line 
1## $Id: applicant.py 9217 2012-09-21 11:21:05Z uli $
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 IApplicant, IApplicantEdit
41from waeup.kofa.applicants.workflow import application_states_dict
42
43def search(query=None, searchtype=None, view=None):
44    if searchtype in ('fullname',):
45        results = Query().searchResults(
46            Text(('applicants_catalog', searchtype), query))
47    else:
48        results = Query().searchResults(
49            Eq(('applicants_catalog', searchtype), query))
50    return results
51
52class Applicant(grok.Container):
53    grok.implements(IApplicant,IApplicantEdit)
54    grok.provides(IApplicant)
55
56    create_names = [
57        'firstname', 'lastname',
58        'sex', 'date_of_birth',
59        'email', 'phone'
60        ]
61
62    def __init__(self):
63        super(Applicant, self).__init__()
64        self.password = None
65        self.application_date = None
66        self.applicant_id = None
67        return
68
69    def writeLogMessage(self, view, message):
70        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
71        self.__parent__.__parent__.logger.info(
72            '%s - %s - %s' % (ob_class, self.applicant_id, message))
73        return
74
75    @property
76    def state(self):
77        return IWorkflowState(self).getState()
78
79    @property
80    def container_code(self):
81        return self.__parent__.code
82
83    @property
84    def translated_state(self):
85        return application_states_dict[self.state]
86
87    @property
88    def history(self):
89        history = IObjectHistory(self)
90        return history
91
92    @property
93    def application_number(self):
94        try:
95            return self.applicant_id.split('_')[1]
96        except AttributeError:
97            return None
98
99    @property
100    def display_fullname(self):
101        middlename = getattr(self, 'middlename', None)
102        kofa_utils = getUtility(IKofaUtils)
103        return kofa_utils.fullname(self.firstname, self.lastname, middlename)
104
105    def createStudent(self, view=None):
106        """Create a student, fill with base data, create an application slip
107        and copy applicant data.
108        """
109        # Is applicant in the correct state?
110        if self.state != 'admitted':
111            return False, _('Applicant has not yet been admitted.')
112        # Does registration number exist?
113        student = createObject(u'waeup.Student')
114        try:
115            student.reg_number = self.reg_number
116        except RegNumNotInSource:
117            return False, _('Registration Number exists.')
118        # Has the course_admitted field been properly filled?
119        if self.course_admitted is None:
120            return False, _('No course admitted provided.')
121        # Set student attributes
122        try:
123            for name in self.create_names:
124                setattr(student, name, getattr(self, name, None))
125        except RequiredMissing, err:
126            return False, 'RequiredMissing: %s' % err
127        except:
128            return False, 'Error: %s' % err
129        # Finally prove if the certificate still exists
130        try:
131            StudentStudyCourse().certificate = self.course_admitted
132        except ConstraintNotSatisfied, err:
133            return False, 'ConstraintNotSatisfied: %s' % self.course_admitted.code
134        # Add student
135        site = grok.getSite()
136        site['students'].addStudent(student)
137        # Save student_id
138        self.student_id = student.student_id
139        # Fire transitions
140        IWorkflowInfo(self).fireTransition('create')
141        IWorkflowInfo(student).fireTransition('admit')
142        # Set password
143        IUserAccount(student).setPassword(self.application_number)
144        # Save the certificate and set session attributes
145        student['studycourse'].certificate = self.course_admitted
146        student['studycourse'].entry_mode = self.course_admitted.study_mode
147        student['studycourse'].entry_session = self.__parent__.year
148        student['studycourse'].current_session = self.__parent__.year
149        student['studycourse'].current_level = self.course_admitted.start_level
150        self._copyPassportImage(student)
151        # Update the catalog
152        notify(grok.ObjectModifiedEvent(student))
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(IUniversity)
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.