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

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

Replace getStudent method by a 'student' attribute.

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