source: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/students/student.py @ 10021

Last change on this file since 10021 was 9211, checked in by uli, 12 years ago

Rollback r9209. Looks like multiple merges from trunk confuse svn when merging back into trunk.

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