source: main/waeup.kofa/trunk/src/waeup/kofa/students/student.py @ 8515

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

Clean up code.

  • Property svn:keywords set to Id
File size: 14.8 KB
Line 
1## $Id: student.py 8481 2012-05-21 09:28: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##
18"""
19Container for the various objects owned by students.
20"""
21import os
22import re
23import shutil
24import grok
25from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
26from zope.component import getUtility, createObject
27from zope.component.interfaces import IFactory
28from zope.interface import implementedBy
29from zope.securitypolicy.interfaces import IPrincipalRoleManager
30
31from waeup.kofa.image import KofaImageFile
32from waeup.kofa.imagestorage import DefaultFileStoreHandler
33from waeup.kofa.interfaces import (
34    IObjectHistory, IUserAccount, IFileStoreNameChooser, IFileStoreHandler,
35    IKofaUtils, CLEARANCE, registration_states_vocab, IExtFileStore,)
36from waeup.kofa.students.accommodation import StudentAccommodation
37from waeup.kofa.students.export import EXPORTER_NAMES
38from waeup.kofa.students.interfaces import (
39    IStudent, IStudentNavigation, ICSVStudentExporter)
40from waeup.kofa.students.payments import StudentPaymentsContainer
41from waeup.kofa.students.utils import generate_student_id
42from waeup.kofa.utils.helpers import attrs_to_fields, now, copy_filesystem_tree
43
44RE_STUDID_NON_NUM = re.compile('[^\d]+')
45
46class Student(grok.Container):
47    """This is a student container for the various objects
48    owned by students.
49    """
50    grok.implements(IStudent, IStudentNavigation)
51    grok.provides(IStudent)
52
53    def __init__(self):
54        super(Student, self).__init__()
55        # The site doesn't exist in unit tests
56        try:
57            self.student_id = generate_student_id()
58        except TypeError:
59            self.student_id = u'Z654321'
60        self.password = None
61        return
62
63    def loggerInfo(self, ob_class, comment=None):
64        target = self.__name__
65        return grok.getSite()['students'].logger_info(ob_class,target,comment)
66
67    @property
68    def display_fullname(self):
69        middlename = getattr(self, 'middlename', None)
70        kofa_utils = getUtility(IKofaUtils)
71        return kofa_utils.fullname(self.firstname, self.lastname, middlename)
72
73    @property
74    def fullname(self):
75        middlename = getattr(self, 'middlename', None)
76        if middlename:
77            return '%s-%s-%s' % (self.firstname.lower(),
78                middlename.lower(), self.lastname.lower())
79        else:
80            return '%s-%s' % (self.firstname.lower(), self.lastname.lower())
81
82    @property
83    def state(self):
84        state = IWorkflowState(self).getState()
85        return state
86
87    @property
88    def translated_state(self):
89        state = registration_states_vocab.getTermByToken(
90            self.state).title
91        return state
92
93    @property
94    def history(self):
95        history = IObjectHistory(self)
96        return history
97
98    def getStudent(self):
99        return self
100
101    @property
102    def certcode(self):
103        cert = getattr(self.get('studycourse', None), 'certificate', None)
104        if cert is not None:
105            return cert.code
106        return
107
108    @property
109    def faccode(self):
110        cert = getattr(self.get('studycourse', None), 'certificate', None)
111        if cert is not None:
112            return cert.__parent__.__parent__.__parent__.code
113        return
114
115    @property
116    def depcode(self):
117        cert = getattr(self.get('studycourse', None), 'certificate', None)
118        if cert is not None:
119            return cert.__parent__.__parent__.code
120        return
121
122    @property
123    def current_session(self):
124        session = getattr(
125            self.get('studycourse', None), 'current_session', None)
126        return session
127
128    @property
129    def current_mode(self):
130        certificate = getattr(
131            self.get('studycourse', None), 'certificate', None)
132        if certificate is not None:
133            return certificate.study_mode
134        return None
135
136    @property
137    def is_postgrad(self):
138        is_postgrad = getattr(
139            self.get('studycourse', None), 'is_postgrad', False)
140        return is_postgrad
141
142# Set all attributes of Student required in IStudent as field
143# properties. Doing this, we do not have to set initial attributes
144# ourselves and as a bonus we get free validation when an attribute is
145# set.
146Student = attrs_to_fields(Student)
147
148class StudentFactory(grok.GlobalUtility):
149    """A factory for students.
150    """
151    grok.implements(IFactory)
152    grok.name(u'waeup.Student')
153    title = u"Create a new student.",
154    description = u"This factory instantiates new student instances."
155
156    def __call__(self, *args, **kw):
157        return Student()
158
159    def getInterfaces(self):
160        return implementedBy(Student)
161
162@grok.subscribe(IStudent, grok.IObjectAddedEvent)
163def handle_student_added(student, event):
164    """If a student is added all subcontainers are automatically added
165    and the transition create is fired. The latter produces a logging
166    message.
167    """
168    if student.state == CLEARANCE:
169        student.clearance_locked = False
170    else:
171        student.clearance_locked = True
172    studycourse = createObject(u'waeup.StudentStudyCourse')
173    student['studycourse'] = studycourse
174    payments = StudentPaymentsContainer()
175    student['payments'] = payments
176    accommodation = StudentAccommodation()
177    student['accommodation'] = accommodation
178    # Assign global student role for new student
179    account = IUserAccount(student)
180    account.roles = ['waeup.Student']
181    # Assign local StudentRecordOwner role
182    role_manager = IPrincipalRoleManager(student)
183    role_manager.assignRoleToPrincipal(
184        'waeup.local.StudentRecordOwner', student.student_id)
185    if student.state is None:
186        IWorkflowInfo(student).fireTransition('create')
187    return
188
189def path_from_studid(student_id):
190    """Convert a student_id into a predictable relative folder path.
191
192    Used for storing files.
193
194    Returns the name of folder in which files for a particular student
195    should be stored. This is a relative path, relative to any general
196    students folder with 5 zero-padded digits (except when student_id
197    is overlong).
198
199    We normally map 1,000 different student ids into one single
200    path. For instance ``K1000000`` will give ``01000/K1000000``,
201    ``K1234567`` will give ``0123/K1234567`` and ``K12345678`` will
202    result in ``1234/K12345678``.
203
204    For lower numbers < 10**6 we return the same path for up to 10,000
205    student_ids. So for instance ``KM123456`` will result in
206    ``00120/KM123456`` (there will be no path starting with
207    ``00123``).
208
209    Works also with overlong number: here the leading zeros will be
210    missing but ``K123456789`` will give reliably
211    ``12345/K123456789`` as expected.
212    """
213    # remove all non numeric characters and turn this into an int.
214    num = int(RE_STUDID_NON_NUM.sub('', student_id))
215    if num < 10**6:
216        # store max. of 10000 studs per folder and correct num for 5 digits
217        num = num / 10000 * 10
218    else:
219        # store max. of 1000 studs per folder
220        num = num / 1000
221    # format folder name to have 5 zero-padded digits
222    folder_name = u'%05d' % num
223    folder_name = os.path.join(folder_name, student_id)
224    return folder_name
225
226def move_student_files(student, del_dir):
227    """Move files belonging to `student` to `del_dir`.
228
229    `del_dir` is expected to be the path to the site-wide directory
230    for storing backup data.
231
232    The current files of the student are removed after backup.
233
234    If the student has no associated files stored, nothing is done.
235    """
236    stud_id = student.student_id
237
238    src = getUtility(IExtFileStore).root
239    src = os.path.join(src, 'students', path_from_studid(stud_id))
240
241    dst = os.path.join(
242        del_dir, 'media', 'students', path_from_studid(stud_id))
243
244    if not os.path.isdir(src):
245        # Do not copy if no files were stored.
246        return
247    if not os.path.exists(dst):
248        os.makedirs(dst, 0755)
249    copy_filesystem_tree(src, dst)
250    shutil.rmtree(src)
251    return
252
253def update_student_deletion_csvs(student, del_dir):
254    """Update deletion CSV files with data from student.
255
256    `del_dir` is expected to be the path to the site-wide directory
257    for storing backup data.
258
259    Each exporter available for students (and their many subobjects)
260    is called in order to export CSV data of the given student to csv
261    files in the site-wide backup directory for object data (see
262    DataCenter).
263
264    Each exported row is appended a column giving the deletion date
265    (column `del_date`) as a UTC timestamp.
266    """
267    for name in EXPORTER_NAMES:
268        exporter = getUtility(ICSVStudentExporter, name=name)
269        csv_data = exporter.export_student(student)
270        csv_data = csv_data.split('\r\n')
271
272        # append a deletion timestamp on each data row
273        timestamp = str(now().replace(microsecond=0)) # store UTC timestamp
274        for num, row in enumerate(csv_data[1:-1]):
275            csv_data[num+1] = csv_data[num+1] + ',' + timestamp
276        csv_path = os.path.join(del_dir, '%s.csv' % name)
277
278        # write data to CSV file
279        if not os.path.exists(csv_path):
280            # create new CSV file (including header line)
281            csv_data[0] = csv_data[0] + ',del_date'
282            open(csv_path, 'wb').write('\r\n'.join(csv_data))
283        else:
284            # append existing CSV file (omitting headerline)
285            open(csv_path, 'a').write('\r\n'.join(csv_data[1:]))
286    return
287
288@grok.subscribe(IStudent, grok.IObjectRemovedEvent)
289def handle_student_removed(student, event):
290    """If a student is removed a message is logged and data is put
291       into a backup location.
292
293    The data of the removed student is appended to CSV files in local
294    datacenter and any existing external files (passport images, etc.)
295    are copied over to this location as well.
296
297    Documents in the file storage refering to the given student are
298    removed afterwards (if they exist). Please make no assumptions
299    about how the deletion takes place. Files might be deleted
300    individually (leaving the students file directory intact) or the
301    whole student directory might be deleted completely.
302
303    All CSV rows created/appended contain a timestamp with the
304    datetime of removal in an additional `del_date` column.
305
306    XXX: blocking of used student_ids yet not implemented.
307    """
308    comment = 'Student record removed'
309    target = student.student_id
310    try:
311        site = grok.getSite()
312        site['students'].logger.info('%s - %s' % (
313            target, comment))
314    except KeyError:
315        # If we delete an entire university instance there won't be
316        # a students subcontainer
317        return
318
319    del_dir = site['datacenter'].deleted_path
320
321    # save files of the student
322    move_student_files(student, del_dir)
323
324    # update CSV files
325    update_student_deletion_csvs(student, del_dir)
326    return
327
328#: The file id marker for student files
329STUDENT_FILE_STORE_NAME = 'file-student'
330
331class StudentFileNameChooser(grok.Adapter):
332    """A file id chooser for :class:`Student` objects.
333
334    `context` is an :class:`Student` instance.
335
336    The :class:`StudentImageNameChooser` can build/check file ids for
337    :class:`Student` objects suitable for use with
338    :class:`ExtFileStore` instances. The delivered file_id contains
339    the file id marker for :class:`Student` object and the student id
340    of the context student.
341
342    This chooser is registered as an adapter providing
343    :class:`waeup.kofa.interfaces.IFileStoreNameChooser`.
344
345    File store name choosers like this one are only convenience
346    components to ease the task of creating file ids for student
347    objects. You are nevertheless encouraged to use them instead of
348    manually setting up filenames for students.
349
350    .. seealso:: :mod:`waeup.kofa.imagestorage`
351
352    """
353    grok.context(IStudent)
354    grok.implements(IFileStoreNameChooser)
355
356    def checkName(self, name=None, attr=None):
357        """Check whether the given name is a valid file id for the context.
358
359        Returns ``True`` only if `name` equals the result of
360        :meth:`chooseName`.
361
362        """
363        return name == self.chooseName()
364
365    def chooseName(self, attr, name=None):
366        """Get a valid file id for student context.
367
368        *Example:*
369
370        For a student with student id ``'A123456'`` and
371        with attr ``'nice_image.jpeg'`` stored in
372        the students container this chooser would create:
373
374          ``'__file-student__students/A/A123456/nice_image_A123456.jpeg'``
375
376        meaning that the nice image of this applicant would be
377        stored in the site-wide file storage in path:
378
379          ``students/A/A123456/nice_image_A123456.jpeg``
380
381        """
382        basename, ext = os.path.splitext(attr)
383        stud_id = self.context.student_id
384        marked_filename = '__%s__%s/%s_%s%s' % (
385            STUDENT_FILE_STORE_NAME, path_from_studid(stud_id), basename,
386            stud_id, ext)
387        return marked_filename
388
389
390class StudentFileStoreHandler(DefaultFileStoreHandler, grok.GlobalUtility):
391    """Student specific file handling.
392
393    This handler knows in which path in a filestore to store student
394    files and how to turn this kind of data into some (browsable)
395    file object.
396
397    It is called from the global file storage, when it wants to
398    get/store a file with a file id starting with
399    ``__file-student__`` (the marker string for student files).
400
401    Like each other file store handler it does not handle the files
402    really (this is done by the global file store) but only computes
403    paths and things like this.
404    """
405    grok.implements(IFileStoreHandler)
406    grok.name(STUDENT_FILE_STORE_NAME)
407
408    def pathFromFileID(self, store, root, file_id):
409        """All student files are put in directory ``students``.
410        """
411        marker, filename, basename, ext = store.extractMarker(file_id)
412        sub_root = os.path.join(root, 'students')
413        return super(StudentFileStoreHandler, self).pathFromFileID(
414            store, sub_root, basename)
415
416    def createFile(self, store, root, filename, file_id, file):
417        """Create a browsable file-like object.
418        """
419        # call super method to ensure that any old files with
420        # different filename extension are deleted.
421        file, path, file_obj =  super(
422            StudentFileStoreHandler, self).createFile(
423            store, root,  filename, file_id, file)
424        return file, path, KofaImageFile(
425            file_obj.filename, file_obj.data)
Note: See TracBrowser for help on using the repository browser.