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

Last change on this file since 13214 was 13213, checked in by Henrik Bettermann, 9 years ago

Add property to mark 'unused' records. Add FieldIndex? to applicants catalog.

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