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

Last change on this file since 10844 was 10831, checked in by Henrik Bettermann, 11 years ago

Implement a special application procedure. This application is meant for supplementary payments by alumni and other persons who are not students of the portal.

Attention: All custom packages have to be adjusted.

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