source: main/waeup.kofa/trunk/src/waeup/kofa/students/utils.py @ 16250

Last change on this file since 16250 was 16159, checked in by Henrik Bettermann, 4 years ago

Paint watermark first to make it transparent.

  • Property svn:keywords set to Id
File size: 53.0 KB
Line 
1## $Id: utils.py 16159 2020-07-10 06:11:44Z 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"""General helper functions and utilities for the students section.
19"""
20import grok
21import textwrap
22from copy import deepcopy
23from cgi import escape
24from time import time
25from cStringIO import StringIO
26from PyPDF2 import PdfFileMerger, PdfFileReader, PdfFileWriter
27from reportlab.lib import colors
28from reportlab.lib.units import cm
29from reportlab.lib.pagesizes import A4
30from reportlab.lib.styles import getSampleStyleSheet
31from reportlab.platypus import Paragraph, Image, Table, Spacer
32from reportlab.platypus.doctemplate import LayoutError
33from reportlab.platypus.flowables import PageBreak
34from zope.event import notify
35from zope.schema.interfaces import ConstraintNotSatisfied
36from zope.component import getUtility, createObject, queryUtility
37from zope.catalog.interfaces import ICatalog
38from zope.formlib.form import setUpEditWidgets
39from zope.i18n import translate
40from waeup.kofa.interfaces import (
41    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED, GRADUATED,
42    academic_sessions_vocab, IFileStoreNameChooser)
43from waeup.kofa.interfaces import MessageFactory as _
44from waeup.kofa.students.interfaces import IStudentsUtils
45from waeup.kofa.students.workflow import ADMITTED
46from waeup.kofa.students.vocabularies import StudyLevelSource, MatNumNotInSource
47from waeup.kofa.browser.pdf import (
48    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
49    get_signature_tables, get_qrcode)
50from waeup.kofa.browser.interfaces import IPDFCreator
51from waeup.kofa.utils.helpers import to_timezone
52
53SLIP_STYLE = [
54    ('VALIGN',(0,0),(-1,-1),'TOP'),
55    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
56    ]
57
58CONTENT_STYLE = [
59    ('VALIGN',(0,0),(-1,-1),'TOP'),
60    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
61    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
62    #('BACKGROUND',(0,0),(-1,0),colors.black),
63    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
64    ('BOX', (0,0), (-1,-1), 1, colors.black),
65    ]
66
67FONT_SIZE = 10
68FONT_COLOR = 'black'
69
70def trans(text, lang):
71    # shortcut
72    return translate(text, 'waeup.kofa', target_language=lang)
73
74def formatted_text(text, color=FONT_COLOR, lang='en'):
75    """Turn `text`, `color` and `size` into an HTML snippet.
76
77    The snippet is suitable for use with reportlab and generating PDFs.
78    Wraps the `text` into a ``<font>`` tag with passed attributes.
79
80    Also non-strings are converted. Raw strings are expected to be
81    utf-8 encoded (usually the case for widgets etc.).
82
83    Finally, a br tag is added if widgets contain div tags
84    which are not supported by reportlab.
85
86    The returned snippet is unicode type.
87    """
88    if not isinstance(text, unicode):
89        if isinstance(text, basestring):
90            text = text.decode('utf-8')
91        else:
92            text = unicode(text)
93    if text == 'None':
94        text = ''
95    # Very long matriculation numbers need to be wrapped
96    if text.find(' ') == -1 and len(text.split('/')) > 6:
97        text = '/'.join(text.split('/')[:5]) + \
98            '/ ' + '/'.join(text.split('/')[5:])
99    # Mainly for boolean values we need our customized
100    # localisation of the zope domain
101    text = translate(text, 'zope', target_language=lang)
102    text = text.replace('</div>', '<br /></div>')
103    tag1 = u'<font color="%s">' % (color)
104    return tag1 + u'%s</font>' % text
105
106def generate_student_id():
107    students = grok.getSite()['students']
108    new_id = students.unique_student_id
109    return new_id
110
111def set_up_widgets(view, ignore_request=False):
112    view.adapters = {}
113    view.widgets = setUpEditWidgets(
114        view.form_fields, view.prefix, view.context, view.request,
115        adapters=view.adapters, for_display=True,
116        ignore_request=ignore_request
117        )
118
119def render_student_data(studentview, context, omit_fields=(),
120                        lang='en', slipname=None, no_passport=False):
121    """Render student table for an existing frame.
122    """
123    width, height = A4
124    set_up_widgets(studentview, ignore_request=True)
125    data_left = []
126    data_middle = []
127    style = getSampleStyleSheet()
128    img = getUtility(IExtFileStore).getFileByContext(
129        studentview.context, attr='passport.jpg')
130    if img is None:
131        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
132        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
133    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
134    data_left.append([doc_img])
135    #data.append([Spacer(1, 12)])
136
137    f_label = trans(_('Name:'), lang)
138    f_label = Paragraph(f_label, ENTRY1_STYLE)
139    f_text = formatted_text(studentview.context.display_fullname)
140    f_text = Paragraph(f_text, ENTRY1_STYLE)
141    data_middle.append([f_label,f_text])
142
143    for widget in studentview.widgets:
144        if 'name' in widget.name:
145            continue
146        f_label = translate(
147            widget.label.strip(), 'waeup.kofa',
148            target_language=lang)
149        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
150        f_text = formatted_text(widget(), lang=lang)
151        f_text = Paragraph(f_text, ENTRY1_STYLE)
152        data_middle.append([f_label,f_text])
153    if not 'date_of_birth' in omit_fields:
154        f_label = trans(_('Date of Birth:'), lang)
155        f_label = Paragraph(f_label, ENTRY1_STYLE)
156        date_of_birth = studentview.context.date_of_birth
157        tz = getUtility(IKofaUtils).tzinfo
158        date_of_birth = to_timezone(date_of_birth, tz)
159        if date_of_birth is not None:
160            date_of_birth = date_of_birth.strftime("%d/%m/%Y")
161        f_text = formatted_text(date_of_birth)
162        f_text = Paragraph(f_text, ENTRY1_STYLE)
163        data_middle.append([f_label,f_text])
164    if getattr(studentview.context, 'certcode', None):
165        if not 'certificate' in omit_fields:
166            f_label = trans(_('Study Course:'), lang)
167            f_label = Paragraph(f_label, ENTRY1_STYLE)
168            f_text = formatted_text(
169                studentview.context['studycourse'].certificate.longtitle)
170            f_text = Paragraph(f_text, ENTRY1_STYLE)
171            data_middle.append([f_label,f_text])
172        if not 'department' in omit_fields:
173            f_label = trans(_('Department:'), lang)
174            f_label = Paragraph(f_label, ENTRY1_STYLE)
175            f_text = formatted_text(
176                studentview.context[
177                'studycourse'].certificate.__parent__.__parent__.longtitle,
178                )
179            f_text = Paragraph(f_text, ENTRY1_STYLE)
180            data_middle.append([f_label,f_text])
181        if not 'faculty' in omit_fields:
182            f_label = trans(_('Faculty:'), lang)
183            f_label = Paragraph(f_label, ENTRY1_STYLE)
184            f_text = formatted_text(
185                studentview.context[
186                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
187                )
188            f_text = Paragraph(f_text, ENTRY1_STYLE)
189            data_middle.append([f_label,f_text])
190        if not 'current_mode' in omit_fields:
191            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
192            sm = studymodes_dict[studentview.context.current_mode]
193            f_label = trans(_('Study Mode:'), lang)
194            f_label = Paragraph(f_label, ENTRY1_STYLE)
195            f_text = formatted_text(sm)
196            f_text = Paragraph(f_text, ENTRY1_STYLE)
197            data_middle.append([f_label,f_text])
198        if not 'entry_session' in omit_fields:
199            f_label = trans(_('Entry Session:'), lang)
200            f_label = Paragraph(f_label, ENTRY1_STYLE)
201            entry_session = studentview.context.entry_session
202            try:
203                entry_session = academic_sessions_vocab.getTerm(
204                    entry_session).title
205            except LookupError:
206                entry_session = _('void')
207            f_text = formatted_text(entry_session)
208            f_text = Paragraph(f_text, ENTRY1_STYLE)
209            data_middle.append([f_label,f_text])
210        # Requested by Uniben, does not really make sense
211        if not 'current_level' in omit_fields:
212            f_label = trans(_('Current Level:'), lang)
213            f_label = Paragraph(f_label, ENTRY1_STYLE)
214            current_level = studentview.context['studycourse'].current_level
215            studylevelsource = StudyLevelSource().factory
216            current_level = studylevelsource.getTitle(
217                studentview.context, current_level)
218            f_text = formatted_text(current_level)
219            f_text = Paragraph(f_text, ENTRY1_STYLE)
220            data_middle.append([f_label,f_text])
221
222    if no_passport:
223        table = Table(data_middle,style=SLIP_STYLE)
224        table.hAlign = 'LEFT'
225        return table
226
227    # append QR code to the right
228    if slipname:
229        url = studentview.url(context, slipname)
230        data_right = [[get_qrcode(url, width=70.0)]]
231        table_right = Table(data_right,style=SLIP_STYLE)
232    else:
233        table_right = None
234
235    table_left = Table(data_left,style=SLIP_STYLE)
236    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
237    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
238    return table
239
240def render_table_data(tableheader, tabledata, lang='en'):
241    """Render children table for an existing frame.
242    """
243    data = []
244    #data.append([Spacer(1, 12)])
245    line = []
246    style = getSampleStyleSheet()
247    for element in tableheader:
248        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
249        field = Paragraph(field, style["Normal"])
250        line.append(field)
251    data.append(line)
252    for ticket in tabledata:
253        line = []
254        for element in tableheader:
255              field = formatted_text(getattr(ticket,element[1],u' '))
256              field = Paragraph(field, style["Normal"])
257              line.append(field)
258        data.append(line)
259    table = Table(data,colWidths=[
260        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
261    return table
262
263def render_transcript_data(view, tableheader, levels_data, lang='en'):
264    """Render children table for an existing frame.
265    """
266    data = []
267    style = getSampleStyleSheet()
268    format_float = getUtility(IKofaUtils).format_float
269    for level in levels_data:
270        level_obj = level['level']
271        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
272        headerline = []
273        tabledata = []
274        if 'evel' in view.level_dict.get('ticket.level', str(level_obj.level)):
275            subheader = '%s %s, %s' % (
276                trans(_('Session'), lang),
277                view.session_dict[level_obj.level_session],
278                view.level_dict.get('ticket.level', str(level_obj.level)))
279        else:
280            subheader = '%s %s, %s %s' % (
281                trans(_('Session'), lang),
282                view.session_dict[level_obj.level_session],
283                trans(_('Level'), lang),
284                view.level_dict.get(level_obj.level, str(level_obj.level)))
285        data.append(Paragraph(subheader, HEADING_STYLE))
286        for element in tableheader:
287            field = '<strong>%s</strong>' % formatted_text(element[0])
288            field = Paragraph(field, style["Normal"])
289            headerline.append(field)
290        tabledata.append(headerline)
291        for ticket in tickets:
292            ticketline = []
293            for element in tableheader:
294                  field = formatted_text(getattr(ticket,element[1],u' '))
295                  field = Paragraph(field, style["Normal"])
296                  ticketline.append(field)
297            tabledata.append(ticketline)
298        table = Table(tabledata,colWidths=[
299            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
300        data.append(table)
301        sgpa = format_float(level['sgpa'], 2)
302        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), sgpa)
303        #sgpa = '%s: %.2f' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
304        data.append(Paragraph(sgpa, style["Normal"]))
305        if getattr(level_obj, 'transcript_remark', None):
306            remark = '%s: %s' % (
307                trans('Transcript Remark', lang),
308                getattr(level_obj, 'transcript_remark'))
309            data.append(Paragraph(remark, style["Normal"]))
310    return data
311
312def docs_as_flowables(view, lang='en'):
313    """Create reportlab flowables out of scanned docs.
314    """
315    # XXX: fix circular import problem
316    from waeup.kofa.browser.fileviewlets import FileManager
317    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
318    style = getSampleStyleSheet()
319    data = []
320
321    # Collect viewlets
322    fm = FileManager(view.context, view.request, view)
323    fm.update()
324    if fm.viewlets:
325        sc_translation = trans(_('Scanned Documents'), lang)
326        data.append(Paragraph(sc_translation, HEADING_STYLE))
327        # Insert list of scanned documents
328        table_data = []
329        for viewlet in fm.viewlets:
330            if viewlet.file_exists:
331                # Show viewlet only if file exists
332                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
333                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
334                    view.context, attr=viewlet.download_name), 'name', None)
335                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
336                if img_path is None:
337                    pass
338                elif not img_path[-4:] in ('.jpg', '.JPG'):
339                    # reportlab requires jpg images, I think.
340                    f_text = Paragraph('%s (not displayable)' % (
341                        viewlet.title,), ENTRY1_STYLE)
342                else:
343                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
344                table_data.append([f_label, f_text])
345        if table_data:
346            # safety belt; empty tables lead to problems.
347            data.append(Table(table_data, style=SLIP_STYLE))
348    return data
349
350class StudentsUtils(grok.GlobalUtility):
351    """A collection of methods subject to customization.
352    """
353    grok.implements(IStudentsUtils)
354
355    def getReturningData(self, student):
356        """ Define what happens after school fee payment
357        depending on the student's senate verdict.
358        In the base configuration current level is always increased
359        by 100 no matter which verdict has been assigned.
360        """
361        new_level = student['studycourse'].current_level + 100
362        new_session = student['studycourse'].current_session + 1
363        return new_session, new_level
364
365    def setReturningData(self, student):
366        """ Define what happens after school fee payment
367        depending on the student's senate verdict.
368        This method folllows the same algorithm as `getReturningData` but
369        it also sets the new values.
370        """
371        new_session, new_level = self.getReturningData(student)
372        try:
373            student['studycourse'].current_level = new_level
374        except ConstraintNotSatisfied:
375            # Do not change level if level exceeds the
376            # certificate's end_level.
377            pass
378        student['studycourse'].current_session = new_session
379        verdict = student['studycourse'].current_verdict
380        student['studycourse'].current_verdict = '0'
381        student['studycourse'].previous_verdict = verdict
382        return
383
384    def _getSessionConfiguration(self, session):
385        try:
386            return grok.getSite()['configuration'][str(session)]
387        except KeyError:
388            return None
389
390    def _isPaymentDisabled(self, p_session, category, student):
391        academic_session = self._getSessionConfiguration(p_session)
392        if category == 'schoolfee' and \
393            'sf_all' in academic_session.payment_disabled:
394            return True
395        return False
396
397    def samePaymentMade(self, student, category, p_item, p_session):
398        for key in student['payments'].keys():
399            ticket = student['payments'][key]
400            if ticket.p_state == 'paid' and\
401               ticket.p_category == category and \
402               ticket.p_item != 'Balance' and \
403               ticket.p_item == p_item and \
404               ticket.p_session == p_session:
405                  return True
406        return False
407
408    def setPaymentDetails(self, category, student,
409            previous_session=None, previous_level=None, combi=[]):
410        """Create a payment ticket and set the payment data of a
411        student for the payment category specified.
412        """
413        p_item = u''
414        amount = 0.0
415        if previous_session:
416            if previous_session < student['studycourse'].entry_session:
417                return _('The previous session must not fall below '
418                         'your entry session.'), None
419            if category == 'schoolfee':
420                # School fee is always paid for the following session
421                if previous_session > student['studycourse'].current_session:
422                    return _('This is not a previous session.'), None
423            else:
424                if previous_session > student['studycourse'].current_session - 1:
425                    return _('This is not a previous session.'), None
426            p_session = previous_session
427            p_level = previous_level
428            p_current = False
429        else:
430            p_session = student['studycourse'].current_session
431            p_level = student['studycourse'].current_level
432            p_current = True
433        academic_session = self._getSessionConfiguration(p_session)
434        if academic_session == None:
435            return _(u'Session configuration object is not available.'), None
436        # Determine fee.
437        if category == 'schoolfee':
438            try:
439                certificate = student['studycourse'].certificate
440                p_item = certificate.code
441            except (AttributeError, TypeError):
442                return _('Study course data are incomplete.'), None
443            if previous_session:
444                # Students can pay for previous sessions in all
445                # workflow states.  Fresh students are excluded by the
446                # update method of the PreviousPaymentAddFormPage.
447                if previous_level == 100:
448                    amount = getattr(certificate, 'school_fee_1', 0.0)
449                else:
450                    amount = getattr(certificate, 'school_fee_2', 0.0)
451            else:
452                if student.state == CLEARED:
453                    amount = getattr(certificate, 'school_fee_1', 0.0)
454                elif student.state == RETURNING:
455                    # In case of returning school fee payment the
456                    # payment session and level contain the values of
457                    # the session the student has paid for. Payment
458                    # session is always next session.
459                    p_session, p_level = self.getReturningData(student)
460                    academic_session = self._getSessionConfiguration(p_session)
461                    if academic_session == None:
462                        return _(
463                            u'Session configuration object is not available.'
464                            ), None
465                    amount = getattr(certificate, 'school_fee_2', 0.0)
466                elif student.is_postgrad and student.state == PAID:
467                    # Returning postgraduate students also pay for the
468                    # next session but their level always remains the
469                    # same.
470                    p_session += 1
471                    academic_session = self._getSessionConfiguration(p_session)
472                    if academic_session == None:
473                        return _(
474                            u'Session configuration object is not available.'
475                            ), None
476                    amount = getattr(certificate, 'school_fee_2', 0.0)
477        elif category == 'clearance':
478            try:
479                p_item = student['studycourse'].certificate.code
480            except (AttributeError, TypeError):
481                return _('Study course data are incomplete.'), None
482            amount = academic_session.clearance_fee
483        elif category == 'bed_allocation':
484            p_item = self.getAccommodationDetails(student)['bt']
485            amount = academic_session.booking_fee
486        elif category == 'hostel_maintenance':
487            amount = 0.0
488            bedticket = student['accommodation'].get(
489                str(student.current_session), None)
490            if bedticket is not None and bedticket.bed is not None:
491                p_item = bedticket.bed_coordinates
492                if bedticket.bed.__parent__.maint_fee > 0:
493                    amount = bedticket.bed.__parent__.maint_fee
494                else:
495                    # fallback
496                    amount = academic_session.maint_fee
497            else:
498                return _(u'No bed allocated.'), None
499        elif category == 'combi' and combi:
500            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
501            for cat in combi:
502                fee_name = cat + '_fee'
503                cat_amount = getattr(academic_session, fee_name, 0.0)
504                if not cat_amount:
505                    return _('%s undefined.' % categories[cat]), None
506                amount += cat_amount
507                p_item += u'%s + ' % categories[cat]
508            p_item = p_item.strip(' + ')
509        else:
510            fee_name = category + '_fee'
511            amount = getattr(academic_session, fee_name, 0.0)
512        if amount in (0.0, None):
513            return _('Amount could not be determined.'), None
514        if self.samePaymentMade(student, category, p_item, p_session):
515            return _('This type of payment has already been made.'), None
516        if self._isPaymentDisabled(p_session, category, student):
517            return _('This category of payments has been disabled.'), None
518        payment = createObject(u'waeup.StudentOnlinePayment')
519        timestamp = ("%d" % int(time()*10000))[1:]
520        payment.p_id = "p%s" % timestamp
521        payment.p_category = category
522        payment.p_item = p_item
523        payment.p_session = p_session
524        payment.p_level = p_level
525        payment.p_current = p_current
526        payment.amount_auth = amount
527        payment.p_combi = combi
528        return None, payment
529
530    def setBalanceDetails(self, category, student,
531            balance_session, balance_level, balance_amount):
532        """Create a balance payment ticket and set the payment data
533        as selected by the student.
534        """
535        p_item = u'Balance'
536        p_session = balance_session
537        p_level = balance_level
538        p_current = False
539        amount = balance_amount
540        academic_session = self._getSessionConfiguration(p_session)
541        if academic_session == None:
542            return _(u'Session configuration object is not available.'), None
543        if amount in (0.0, None) or amount < 0:
544            return _('Amount must be greater than 0.'), None
545        payment = createObject(u'waeup.StudentOnlinePayment')
546        timestamp = ("%d" % int(time()*10000))[1:]
547        payment.p_id = "p%s" % timestamp
548        payment.p_category = category
549        payment.p_item = p_item
550        payment.p_session = p_session
551        payment.p_level = p_level
552        payment.p_current = p_current
553        payment.amount_auth = amount
554        return None, payment
555
556    def increaseMatricInteger(self, student):
557        """Increase counter for matric numbers.
558        This counter can be a centrally stored attribute or an attribute of
559        faculties, departments or certificates. In the base package the counter
560        is as an attribute of the site configuration container.
561        """
562        grok.getSite()['configuration'].next_matric_integer += 1
563        return
564
565    def constructMatricNumber(self, student):
566        """Fetch the matric number counter which fits the student and
567        construct the new matric number of the student.
568        In the base package the counter is returned which is as an attribute
569        of the site configuration container.
570        """
571        next_integer = grok.getSite()['configuration'].next_matric_integer
572        if next_integer == 0:
573            return _('Matriculation number cannot be set.'), None
574        return None, unicode(next_integer)
575
576    def setMatricNumber(self, student):
577        """Set matriculation number of student. If the student's matric number
578        is unset a new matric number is
579        constructed according to the matriculation number construction rules
580        defined in the `constructMatricNumber` method. The new matric number is
581        set, the students catalog updated. The corresponding matric number
582        counter is increased by one.
583
584        This method is tested but not used in the base package. It can
585        be used in custom packages by adding respective views
586        and by customizing `increaseMatricInteger` and `constructMatricNumber`
587        according to the university's matriculation number construction rules.
588
589        The method can be disabled by setting the counter to zero.
590        """
591        if student.matric_number is not None:
592            return _('Matriculation number already set.'), None
593        if student.certcode is None:
594            return _('No certificate assigned.'), None
595        error, matric_number = self.constructMatricNumber(student)
596        if error:
597            return error, None
598        try:
599            student.matric_number = matric_number
600        except MatNumNotInSource:
601            return _('Matriculation number %s exists.' % matric_number), None
602        notify(grok.ObjectModifiedEvent(student))
603        self.increaseMatricInteger(student)
604        return None, matric_number
605
606    def getAccommodationDetails(self, student):
607        """Determine the accommodation data of a student.
608        """
609        d = {}
610        d['error'] = u''
611        hostels = grok.getSite()['hostels']
612        d['booking_session'] = hostels.accommodation_session
613        d['allowed_states'] = hostels.accommodation_states
614        d['startdate'] = hostels.startdate
615        d['enddate'] = hostels.enddate
616        d['expired'] = hostels.expired
617        # Determine bed type
618        studycourse = student['studycourse']
619        certificate = getattr(studycourse,'certificate',None)
620        entry_session = studycourse.entry_session
621        current_level = studycourse.current_level
622        if None in (entry_session, current_level, certificate):
623            return d
624        end_level = certificate.end_level
625        if current_level == 10:
626            bt = 'pr'
627        elif entry_session == grok.getSite()['hostels'].accommodation_session:
628            bt = 'fr'
629        elif current_level >= end_level:
630            bt = 'fi'
631        else:
632            bt = 're'
633        if student.sex == 'f':
634            sex = 'female'
635        else:
636            sex = 'male'
637        special_handling = 'regular'
638        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
639        return d
640
641    def checkAccommodationRequirements(self, student, acc_details):
642        if acc_details.get('expired', False):
643            startdate = acc_details.get('startdate')
644            enddate = acc_details.get('enddate')
645            if startdate and enddate:
646                tz = getUtility(IKofaUtils).tzinfo
647                startdate = to_timezone(
648                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
649                enddate = to_timezone(
650                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
651                return _("Outside booking period: ${a} - ${b}",
652                         mapping = {'a': startdate, 'b': enddate})
653            else:
654                return _("Outside booking period.")
655        if not acc_details.get('bt'):
656            return _("Your data are incomplete.")
657        if not student.state in acc_details['allowed_states']:
658            return _("You are in the wrong registration state.")
659        if student['studycourse'].current_session != acc_details[
660            'booking_session']:
661            return _('Your current session does not '
662                     'match accommodation session.')
663        bsession = str(acc_details['booking_session'])
664        if bsession in student['accommodation'].keys() \
665            and not 'booking expired' in \
666            student['accommodation'][bsession].bed_coordinates:
667            return _('You already booked a bed space in '
668                     'current accommodation session.')
669        return
670
671    def selectBed(self, available_beds):
672        """Select a bed from a filtered list of available beds.
673        In the base configuration beds are sorted by the sort id
674        of the hostel and the bed number. The first bed found in
675        this sorted list is taken.
676        """
677        sorted_beds = sorted(available_beds,
678                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
679        return sorted_beds[0]
680
681    def GPABoundaries(self, faccode=None, depcode=None, certcode=None):
682        return ((1, 'Fail'),
683               (1.5, 'Pass'),
684               (2.4, '3rd Class'),
685               (3.5, '2nd Class Lower'),
686               (4.5, '2nd Class Upper'),
687               (5, '1st Class'))
688
689    def getClassFromCGPA(self, gpa, student):
690        """Determine the class of degree. In some custom packages
691        this class depends on e.g. the entry session of the student. In the
692        base package, it does not.
693        """
694        if gpa < self.GPABoundaries()[0][0]:
695            return 0, self.GPABoundaries()[0][1]
696        if gpa < self.GPABoundaries()[1][0]:
697            return 1, self.GPABoundaries()[1][1]
698        if gpa < self.GPABoundaries()[2][0]:
699            return 2, self.GPABoundaries()[2][1]
700        if gpa < self.GPABoundaries()[3][0]:
701            return 3, self.GPABoundaries()[3][1]
702        if gpa < self.GPABoundaries()[4][0]:
703            return 4, self.GPABoundaries()[4][1]
704        if gpa <= self.GPABoundaries()[5][0]:
705            return 5, self.GPABoundaries()[5][1]
706        return
707
708    def getDegreeClassNumber(self, level_obj):
709        """Get degree class number (used for SessionResultsPresentation
710        reports).
711        """
712        if level_obj.gpa_params[1] == 0:
713            # No credits weighted
714            return 6
715        return self.getClassFromCGPA(
716            level_obj.cumulative_params[0], level_obj.student)[0]
717
718    def _saveTranscriptPDF(self, student, transcript):
719        """Create a transcript PDF file and store it in student folder.
720        """
721        file_store = getUtility(IExtFileStore)
722        file_id = IFileStoreNameChooser(student).chooseName(
723            attr="final_transcript.pdf")
724        file_store.createFile(file_id, StringIO(transcript))
725        return
726
727    def warnCreditsOOR(self, studylevel, course=None):
728        """Return message if credits are out of range. In the base
729        package only maximum credits is set.
730        """
731        if course and studylevel.total_credits + course.credits > 50:
732            return _('Maximum credits exceeded.')
733        elif studylevel.total_credits > 50:
734            return _('Maximum credits exceeded.')
735        return
736
737    def warnCourseAlreadyPassed(self, studylevel, course):
738        """Return message if course has already been passed at
739        previous levels.
740        """
741        for slevel in studylevel.__parent__.values():
742            for cticket in slevel.values():
743                if cticket.code == course.code \
744                    and cticket.total_score >= cticket.passmark:
745                    return _('Course has already been passed at previous level.')
746        return False
747
748    def getBedCoordinates(self, bedticket):
749        """Return descriptive bed coordinates.
750        This method can be used to customize the `display_coordinates`
751        property method in order to  display a
752        customary description of the bed space.
753        """
754        return bedticket.bed_coordinates
755
756    def clearance_disabled_message(self, student):
757        """Render message if clearance is disabled.
758        """
759        try:
760            session_config = grok.getSite()[
761                'configuration'][str(student.current_session)]
762        except KeyError:
763            return _('Session configuration object is not available.')
764        if not session_config.clearance_enabled:
765            return _('Clearance is disabled for this session.')
766        return None
767
768    def getPDFCreator(self, context):
769        """Get a pdf creator suitable for `context`.
770        The default implementation always returns the default creator.
771        """
772        return getUtility(IPDFCreator)
773
774    def renderPDF(self, view, filename='slip.pdf', student=None,
775                  studentview=None,
776                  tableheader=[], tabledata=[],
777                  note=None, signatures=None, sigs_in_footer=(),
778                  show_scans=True, topMargin=1.5,
779                  omit_fields=()):
780        """Render pdf slips for various pages (also some pages
781        in the applicants module).
782        """
783        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
784        # XXX: tell what the different parameters mean
785        style = getSampleStyleSheet()
786        creator = self.getPDFCreator(student)
787        data = []
788        doc_title = view.label
789        author = '%s (%s)' % (view.request.principal.title,
790                              view.request.principal.id)
791        footer_text = view.label.split('\n')
792        if len(footer_text) > 2:
793            # We can add a department in first line, second line is used
794            footer_text = footer_text[1]
795        else:
796            # Only the first line is used for the footer
797            footer_text = footer_text[0]
798        if getattr(student, 'student_id', None) is not None:
799            footer_text = "%s - %s - " % (student.student_id, footer_text)
800
801        # Insert student data table
802        if student is not None:
803            if view.form_fields:
804                bd_translation = trans(_('Base Data'), portal_language)
805                data.append(Paragraph(bd_translation, HEADING_STYLE))
806            data.append(render_student_data(
807                studentview, view.context, omit_fields, lang=portal_language,
808                slipname=filename))
809
810        # Insert widgets
811        if view.form_fields:
812            data.append(Paragraph(view.title, HEADING_STYLE))
813            separators = getattr(self, 'SEPARATORS_DICT', {})
814            table = creator.getWidgetsTable(
815                view.form_fields, view.context, None, lang=portal_language,
816                separators=separators)
817            data.append(table)
818
819        # Insert scanned docs
820        if show_scans:
821            data.extend(docs_as_flowables(view, portal_language))
822
823        # Insert history
824        if filename == 'clearance_slip.pdf':
825            hist_translation = trans(_('Workflow History'), portal_language)
826            data.append(Paragraph(hist_translation, HEADING_STYLE))
827            data.extend(creator.fromStringList(student.history.messages))
828
829        # Insert content tables (optionally on second page)
830        if hasattr(view, 'tabletitle'):
831            for i in range(len(view.tabletitle)):
832                if tabledata[i] and tableheader[i]:
833                    tabletitle = view.tabletitle[i]
834                    if tabletitle.startswith('_PB_'):
835                        data.append(PageBreak())
836                        tabletitle = view.tabletitle[i].strip('_PB_')
837                    #data.append(Spacer(1, 20))
838                    data.append(Paragraph(tabletitle, HEADING_STYLE))
839                    data.append(Spacer(1, 8))
840                    contenttable = render_table_data(tableheader[i],tabledata[i])
841                    data.append(contenttable)
842
843        # Insert signatures
844        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
845        # do not have a test for the following lines.
846        if signatures and not sigs_in_footer:
847            data.append(Spacer(1, 20))
848            # Render one signature table per signature to
849            # get date and signature in line.
850            for signature in signatures:
851                signaturetables = get_signature_tables(signature)
852                data.append(signaturetables[0])
853
854        view.response.setHeader(
855            'Content-Type', 'application/pdf')
856        try:
857            pdf_stream = creator.create_pdf(
858                data, None, doc_title, author=author, footer=footer_text,
859                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
860        except IOError:
861            view.flash('Error in image file.')
862            return view.redirect(view.url(view.context))
863        except LayoutError, err:
864            view.flash(
865                'PDF file could not be created. Reportlab error message: %s'
866                % escape(err.message),
867                type="danger")
868            return view.redirect(view.url(view.context))
869        return pdf_stream
870
871    def _admissionText(self, student, portal_language):
872        inst_name = grok.getSite()['configuration'].name
873        text = trans(_(
874            'This is to inform you that you have been provisionally'
875            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
876            portal_language)
877        return text
878
879    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
880                                 pre_text=None, post_text=None,
881                                 topMargin = 1.5,
882                                 letterhead_path=None,
883                                 mergefiles=None, watermark=None):
884        """Render pdf admission letter.
885        """
886        if student is None:
887            return
888        style = getSampleStyleSheet()
889        if letterhead_path:
890            creator = getUtility(IPDFCreator, name='letter')
891        else:
892            creator = self.getPDFCreator(student)
893        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
894        data = []
895        doc_title = view.label
896        author = '%s (%s)' % (view.request.principal.title,
897                              view.request.principal.id)
898        footer_text = view.label.split('\n')
899        if len(footer_text) > 2:
900            # We can add a department in first line
901            footer_text = footer_text[1]
902        else:
903            # Only the first line is used for the footer
904            footer_text = footer_text[0]
905        if getattr(student, 'student_id', None) is not None:
906            footer_text = "%s - %s - " % (student.student_id, footer_text)
907
908        # Text before student data
909        if pre_text is None:
910            html = format_html(self._admissionText(student, portal_language))
911        else:
912            html = format_html(pre_text)
913        if html:
914            data.append(Paragraph(html, NOTE_STYLE))
915            data.append(Spacer(1, 20))
916
917        # Student data
918        data.append(render_student_data(view, student,
919                    omit_fields, lang=portal_language,
920                    slipname='admission_slip.pdf'))
921
922        # Text after student data
923        data.append(Spacer(1, 20))
924        if post_text is None:
925            datelist = student.history.messages[0].split()[0].split('-')
926            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
927            post_text = trans(_(
928                'Your Kofa student record was created on ${a}.',
929                mapping = {'a': creation_date}),
930                portal_language)
931        #html = format_html(post_text)
932        #data.append(Paragraph(html, NOTE_STYLE))
933
934        # Create pdf stream
935        view.response.setHeader(
936            'Content-Type', 'application/pdf')
937        pdf_stream = creator.create_pdf(
938            data, None, doc_title, author=author, footer=footer_text,
939            note=post_text, topMargin=topMargin,
940            letterhead_path=letterhead_path)
941        if mergefiles:
942            merger = PdfFileMerger()
943            merger.append(StringIO(pdf_stream))
944            for file in mergefiles:
945                if watermark:
946                    # Pass through all pages of each file
947                    # and merge with watermark page. Paint
948                    # watermark first to make it transparent.
949                    marked_file = PdfFileWriter()
950                    orig_file = PdfFileReader(file)
951                    num_pages = orig_file.getNumPages()
952                    for num in range(num_pages):
953                        watermark_file = PdfFileReader(watermark)
954                        page = watermark_file.getPage(0)
955                        page.mergePage(orig_file.getPage(num))
956                        marked_file.addPage(page)
957                    # Save into a file-like object
958                    tmp1 = StringIO()
959                    marked_file.write(tmp1)
960                    # Append the file-like object
961                    merger.append(tmp1)
962                else:
963                    # Just append the file object
964                    merger.append(file)
965            # Save into a file-like object
966            tmp2 = StringIO()
967            merger.write(tmp2)
968            return tmp2.getvalue()
969        return pdf_stream
970
971    def renderPDFTranscript(self, view, filename='transcript.pdf',
972                  student=None,
973                  studentview=None,
974                  note=None,
975                  signatures=(),
976                  sigs_in_footer=(),
977                  digital_sigs=(),
978                  show_scans=True, topMargin=1.5,
979                  omit_fields=(),
980                  tableheader=None,
981                  no_passport=False,
982                  save_file=False):
983        """Render pdf slip of a transcripts.
984        """
985        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
986        # XXX: tell what the different parameters mean
987        style = getSampleStyleSheet()
988        creator = self.getPDFCreator(student)
989        data = []
990        doc_title = view.label
991        author = '%s (%s)' % (view.request.principal.title,
992                              view.request.principal.id)
993        footer_text = view.label.split('\n')
994        if len(footer_text) > 2:
995            # We can add a department in first line
996            footer_text = footer_text[1]
997        else:
998            # Only the first line is used for the footer
999            footer_text = footer_text[0]
1000        if getattr(student, 'student_id', None) is not None:
1001            footer_text = "%s - %s - " % (student.student_id, footer_text)
1002
1003        # Insert student data table
1004        if student is not None:
1005            #bd_translation = trans(_('Base Data'), portal_language)
1006            #data.append(Paragraph(bd_translation, HEADING_STYLE))
1007            data.append(render_student_data(
1008                studentview, view.context,
1009                omit_fields, lang=portal_language,
1010                slipname=filename,
1011                no_passport=no_passport))
1012
1013        transcript_data = view.context.getTranscriptData()
1014        levels_data = transcript_data[0]
1015
1016        contextdata = []
1017        f_label = trans(_('Course of Study:'), portal_language)
1018        f_label = Paragraph(f_label, ENTRY1_STYLE)
1019        f_text = formatted_text(view.context.certificate.longtitle)
1020        f_text = Paragraph(f_text, ENTRY1_STYLE)
1021        contextdata.append([f_label,f_text])
1022
1023        f_label = trans(_('Faculty:'), portal_language)
1024        f_label = Paragraph(f_label, ENTRY1_STYLE)
1025        f_text = formatted_text(
1026            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
1027        f_text = Paragraph(f_text, ENTRY1_STYLE)
1028        contextdata.append([f_label,f_text])
1029
1030        f_label = trans(_('Department:'), portal_language)
1031        f_label = Paragraph(f_label, ENTRY1_STYLE)
1032        f_text = formatted_text(
1033            view.context.certificate.__parent__.__parent__.longtitle)
1034        f_text = Paragraph(f_text, ENTRY1_STYLE)
1035        contextdata.append([f_label,f_text])
1036
1037        f_label = trans(_('Entry Session:'), portal_language)
1038        f_label = Paragraph(f_label, ENTRY1_STYLE)
1039        f_text = formatted_text(
1040            view.session_dict.get(view.context.entry_session))
1041        f_text = Paragraph(f_text, ENTRY1_STYLE)
1042        contextdata.append([f_label,f_text])
1043
1044        f_label = trans(_('Entry Mode:'), portal_language)
1045        f_label = Paragraph(f_label, ENTRY1_STYLE)
1046        f_text = formatted_text(view.studymode_dict.get(
1047            view.context.entry_mode))
1048        f_text = Paragraph(f_text, ENTRY1_STYLE)
1049        contextdata.append([f_label,f_text])
1050
1051        f_label = trans(_('Cumulative GPA:'), portal_language)
1052        f_label = Paragraph(f_label, ENTRY1_STYLE)
1053        format_float = getUtility(IKofaUtils).format_float
1054        cgpa = format_float(transcript_data[1], 3)
1055        if student.state == GRADUATED:
1056            f_text = formatted_text('%s (%s)' % (
1057                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
1058        else:
1059            f_text = formatted_text('%s' % cgpa)
1060        f_text = Paragraph(f_text, ENTRY1_STYLE)
1061        contextdata.append([f_label,f_text])
1062
1063        contexttable = Table(contextdata,style=SLIP_STYLE)
1064        data.append(contexttable)
1065
1066        transcripttables = render_transcript_data(
1067            view, tableheader, levels_data, lang=portal_language)
1068        data.extend(transcripttables)
1069
1070        # Insert signatures
1071        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
1072        # do not have a test for the following lines.
1073        if signatures and not sigs_in_footer:
1074            data.append(Spacer(1, 20))
1075            # Render one signature table per signature to
1076            # get date and signature in line.
1077            for signature in signatures:
1078                signaturetables = get_signature_tables(signature)
1079                data.append(signaturetables[0])
1080
1081        # Insert digital signatures
1082        if digital_sigs:
1083            data.append(Spacer(1, 20))
1084            sigs = digital_sigs.split('\n')
1085            for sig in sigs:
1086                data.append(Paragraph(sig, NOTE_STYLE))
1087
1088        view.response.setHeader(
1089            'Content-Type', 'application/pdf')
1090        try:
1091            pdf_stream = creator.create_pdf(
1092                data, None, doc_title, author=author, footer=footer_text,
1093                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
1094        except IOError:
1095            view.flash(_('Error in image file.'))
1096            return view.redirect(view.url(view.context))
1097        if save_file:
1098            self._saveTranscriptPDF(student, pdf_stream)
1099            return
1100        return pdf_stream
1101
1102    def renderPDFCourseticketsOverview(
1103            self, view, name, session, data, lecturers, orientation,
1104            title_length, note):
1105        """Render pdf slip of course tickets for a lecturer.
1106        """
1107        filename = '%s_%s_%s_%s.pdf' % (
1108            name, view.context.code, session, view.request.principal.id)
1109        try:
1110            session = academic_sessions_vocab.getTerm(session).title
1111        except LookupError:
1112            session = _('void')
1113        creator = getUtility(IPDFCreator, name=orientation)
1114        style = getSampleStyleSheet()
1115        pdf_data = []
1116        pdf_data += [Paragraph(
1117            translate(_('<b>Lecturer(s): ${a}</b>',
1118                      mapping = {'a':lecturers})), style["Normal"]),]
1119        pdf_data += [Paragraph(
1120            translate(_('<b>Credits: ${a}</b>',
1121                      mapping = {'a':view.context.credits})), style["Normal"]),]
1122        # Not used in base package.
1123        if data[1]:
1124            pdf_data += [Paragraph(
1125                translate(_('<b>${a}</b>',
1126                    mapping = {'a':data[1][0]})), style["Normal"]),]
1127            pdf_data += [Paragraph(
1128                translate(_('<b>${a}</b>',
1129                    mapping = {'a':data[1][1]})), style["Normal"]),]
1130            pdf_data += [Paragraph(
1131                translate(_('<b>Total Students: ${a}</b>',
1132                    mapping = {'a':data[1][2]})), style["Normal"]),]
1133            pdf_data += [Paragraph(
1134                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
1135                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
1136            pdf_data += [Paragraph(
1137                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
1138                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
1139            grade_stats = []
1140            for item in sorted(data[1][7].items()):
1141                grade_stats.append(('%s=%s' % (item[0], item[1])))
1142            grade_stats_string = ', '.join(grade_stats)
1143            pdf_data += [Paragraph(
1144                translate(_('<b>${a}</b>',
1145                mapping = {'a':grade_stats_string})), style["Normal"]),]
1146        pdf_data.append(Spacer(1, 20))
1147        colWidths = [None] * len(data[0][0])
1148        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE,
1149                           repeatRows=1)]
1150        # Process title if too long
1151        title = " ".join(view.context.title.split())
1152        ct = textwrap.fill(title, title_length)
1153        ft = "" # title
1154        #if len(textwrap.wrap(title, title_length)) > 1:
1155        #    ft = textwrap.wrap(title, title_length)[0] + ' ...'
1156        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1157            mapping = {'a':ct,
1158                       'b':view.context.code,
1159                       'd':session}))
1160        if name == 'attendance':
1161            doc_title += '\n' + translate(_('Attendance Sheet'))
1162        if name == 'coursetickets':
1163            doc_title += '\n' + translate(_('Course Tickets Overview'))
1164        #footer_title = translate(_('${a} (${b}) - ${d}',
1165        #    mapping = {'a':ft,
1166        #               'b':view.context.code,
1167        #               'd':session}))
1168        footer_title = translate(_('${b} - ${d}',
1169            mapping = {'b':view.context.code,
1170                       'd':session}))
1171        author = '%s (%s)' % (view.request.principal.title,
1172                              view.request.principal.id)
1173        view.response.setHeader(
1174            'Content-Type', 'application/pdf')
1175        view.response.setHeader(
1176            'Content-Disposition:', 'attachment; filename="%s' % filename)
1177        pdf_stream = creator.create_pdf(
1178            pdf_data, None, doc_title, author, footer_title + ' -', note
1179            )
1180        return pdf_stream
1181
1182    def updateCourseTickets(self, course):
1183        """Udate course tickets if course attributes were changed.
1184        """
1185        current_academic_session = grok.getSite()[
1186            'configuration'].current_academic_session
1187        if not current_academic_session:
1188            return
1189        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1190        coursetickets = cat.searchResults(
1191            code=(course.code, course.code),
1192            session=(current_academic_session,current_academic_session))
1193        number = 0
1194        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1195        for ticket in coursetickets:
1196            if ticket.credits == course.credits:
1197                continue
1198            if ticket.student.current_session != current_academic_session:
1199                continue
1200            if ticket.student.state not in (PAID,):
1201                continue
1202            number += 1
1203            ticket.student.__parent__.logger.info(
1204                '%s - %s %s/%s credits updated (%s->%s)' % (
1205                    ob_class, ticket.student.student_id,
1206                    ticket.level, ticket.code, course.credits,
1207                    ticket.credits))
1208            ticket.credits = course.credits
1209        return number
1210
1211    #: A dictionary which maps widget names to headlines. The headline
1212    #: is rendered in forms and on pdf slips above the respective
1213    #: display or input widget. There are no separating headlines
1214    #: in the base package.
1215    SEPARATORS_DICT = {}
1216
1217    #: A tuple containing names of file upload viewlets which are not shown
1218    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1219    #: in the base package. This attribute makes only sense, if intermediate
1220    #: custom packages are being used, like we do for all Nigerian portals.
1221    SKIP_UPLOAD_VIEWLETS = ()
1222
1223    #: A tuple containing the names of registration states in which changing of
1224    #: passport pictures is allowed.
1225    PORTRAIT_CHANGE_STATES = (ADMITTED,)
1226
1227    #: A tuple containing all exporter names referring to students or
1228    #: subobjects thereof.
1229    STUDENT_EXPORTER_NAMES = (
1230            'students',
1231            'studentstudycourses',
1232            'studentstudylevels',
1233            'coursetickets',
1234            'studentpayments',
1235            'bedtickets',
1236            'trimmed',
1237            'outstandingcourses',
1238            'unpaidpayments',
1239            'sfpaymentsoverview',
1240            'sessionpaymentsoverview',
1241            'studylevelsoverview',
1242            'combocard',
1243            'bursary',
1244            'accommodationpayments',
1245            'transcriptdata',
1246            'trimmedpayments',
1247            )
1248
1249    #: A tuple containing all exporter names needed for backing
1250    #: up student data
1251    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
1252            'studentstudylevels', 'coursetickets',
1253            'studentpayments', 'bedtickets')
1254
1255    # Maximum size of upload files in kB
1256    MAX_KB = 250
1257
1258    #: A prefix used when generating new student ids. Each student id will
1259    #: start with this string. The default is 'K' for Kofa.
1260    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.