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

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

Copy also entry_mode. Extend tests to check if nothing has been forgotten.

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