source: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/applicants/applicant.py @ 9098

Last change on this file since 9098 was 9079, checked in by uli, 12 years ago

Use interface instead of class for site() directive. Decouples
dependencies of component classes.

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