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

Last change on this file since 8403 was 8403, checked in by uli, 13 years ago

Backup student data when a student is deleted (blocking of student_id yet missing).

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