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

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

Simplify logging. Adjust to students.

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