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

Last change on this file since 8405 was 8404, checked in by Henrik Bettermann, 13 years ago

Implement search page for applicants. Add fullname to applicants_catalog.
Plugins must be updated and /reindex?ctlg=applicants must be performed.

Tests will follow.

Rename ApplicantCatalog? to ApplicantsCatalog?. This does not affect persistent data.

Rename StudentIndexes? to StudentsCatalog?.

Add more localization.

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