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

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

Prepare renderPDF for merging with another pdf document.

  • Property svn:keywords set to Id
File size: 53.1 KB
Line 
1## $Id: utils.py 16253 2020-09-29 08:26:52Z 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 _mergeFiles(self, mergefiles, watermark, pdf_steam):
775        merger = PdfFileMerger()
776        merger.append(StringIO(pdf_stream))
777        for file in mergefiles:
778            if watermark:
779                # Pass through all pages of each file
780                # and merge with watermark page. Paint
781                # watermark first to make it transparent.
782                marked_file = PdfFileWriter()
783                orig_file = PdfFileReader(file)
784                num_pages = orig_file.getNumPages()
785                for num in range(num_pages):
786                    watermark_file = PdfFileReader(watermark)
787                    page = watermark_file.getPage(0)
788                    page.mergePage(orig_file.getPage(num))
789                    marked_file.addPage(page)
790                # Save into a file-like object
791                tmp1 = StringIO()
792                marked_file.write(tmp1)
793                # Append the file-like object
794                merger.append(tmp1)
795            else:
796                # Just append the file object
797                merger.append(file)
798        # Save into a file-like object
799        tmp2 = StringIO()
800        merger.write(tmp2)
801        return tmp2.getvalue()
802
803    def renderPDF(self, view, filename='slip.pdf', student=None,
804                  studentview=None,
805                  tableheader=[], tabledata=[],
806                  note=None, signatures=None, sigs_in_footer=(),
807                  show_scans=True, topMargin=1.5,
808                  omit_fields=(),
809                  mergefiles=None, watermark=None):
810        """Render pdf slips for various pages (also some pages
811        in the applicants module).
812        """
813        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
814        # XXX: tell what the different parameters mean
815        style = getSampleStyleSheet()
816        creator = self.getPDFCreator(student)
817        data = []
818        doc_title = view.label
819        author = '%s (%s)' % (view.request.principal.title,
820                              view.request.principal.id)
821        footer_text = view.label.split('\n')
822        if len(footer_text) > 2:
823            # We can add a department in first line, second line is used
824            footer_text = footer_text[1]
825        else:
826            # Only the first line is used for the footer
827            footer_text = footer_text[0]
828        if getattr(student, 'student_id', None) is not None:
829            footer_text = "%s - %s - " % (student.student_id, footer_text)
830
831        # Insert student data table
832        if student is not None:
833            if view.form_fields:
834                bd_translation = trans(_('Base Data'), portal_language)
835                data.append(Paragraph(bd_translation, HEADING_STYLE))
836            data.append(render_student_data(
837                studentview, view.context, omit_fields, lang=portal_language,
838                slipname=filename))
839
840        # Insert widgets
841        if view.form_fields:
842            data.append(Paragraph(view.title, HEADING_STYLE))
843            separators = getattr(self, 'SEPARATORS_DICT', {})
844            table = creator.getWidgetsTable(
845                view.form_fields, view.context, None, lang=portal_language,
846                separators=separators)
847            data.append(table)
848
849        # Insert scanned docs
850        if show_scans:
851            data.extend(docs_as_flowables(view, portal_language))
852
853        # Insert history
854        if filename == 'clearance_slip.pdf':
855            hist_translation = trans(_('Workflow History'), portal_language)
856            data.append(Paragraph(hist_translation, HEADING_STYLE))
857            data.extend(creator.fromStringList(student.history.messages))
858
859        # Insert content tables (optionally on second page)
860        if hasattr(view, 'tabletitle'):
861            for i in range(len(view.tabletitle)):
862                if tabledata[i] and tableheader[i]:
863                    tabletitle = view.tabletitle[i]
864                    if tabletitle.startswith('_PB_'):
865                        data.append(PageBreak())
866                        tabletitle = view.tabletitle[i].strip('_PB_')
867                    #data.append(Spacer(1, 20))
868                    data.append(Paragraph(tabletitle, HEADING_STYLE))
869                    data.append(Spacer(1, 8))
870                    contenttable = render_table_data(tableheader[i],tabledata[i])
871                    data.append(contenttable)
872
873        # Insert signatures
874        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
875        # do not have a test for the following lines.
876        if signatures and not sigs_in_footer:
877            data.append(Spacer(1, 20))
878            # Render one signature table per signature to
879            # get date and signature in line.
880            for signature in signatures:
881                signaturetables = get_signature_tables(signature)
882                data.append(signaturetables[0])
883
884        view.response.setHeader(
885            'Content-Type', 'application/pdf')
886        try:
887            pdf_stream = creator.create_pdf(
888                data, None, doc_title, author=author, footer=footer_text,
889                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
890        except IOError:
891            view.flash('Error in image file.')
892            return view.redirect(view.url(view.context))
893        except LayoutError, err:
894            view.flash(
895                'PDF file could not be created. Reportlab error message: %s'
896                % escape(err.message),
897                type="danger")
898            return view.redirect(view.url(view.context))
899        if mergefiles:
900            return self._mergeFiles(mergefiles, watermark, pdf_steam)
901        return pdf_stream
902
903    def _admissionText(self, student, portal_language):
904        inst_name = grok.getSite()['configuration'].name
905        text = trans(_(
906            'This is to inform you that you have been provisionally'
907            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
908            portal_language)
909        return text
910
911    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
912                                 pre_text=None, post_text=None,
913                                 topMargin = 1.5,
914                                 letterhead_path=None,
915                                 mergefiles=None, watermark=None):
916        """Render pdf admission letter.
917        """
918        if student is None:
919            return
920        style = getSampleStyleSheet()
921        if letterhead_path:
922            creator = getUtility(IPDFCreator, name='letter')
923        else:
924            creator = self.getPDFCreator(student)
925        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
926        data = []
927        doc_title = view.label
928        author = '%s (%s)' % (view.request.principal.title,
929                              view.request.principal.id)
930        footer_text = view.label.split('\n')
931        if len(footer_text) > 2:
932            # We can add a department in first line
933            footer_text = footer_text[1]
934        else:
935            # Only the first line is used for the footer
936            footer_text = footer_text[0]
937        if getattr(student, 'student_id', None) is not None:
938            footer_text = "%s - %s - " % (student.student_id, footer_text)
939
940        # Text before student data
941        if pre_text is None:
942            html = format_html(self._admissionText(student, portal_language))
943        else:
944            html = format_html(pre_text)
945        if html:
946            data.append(Paragraph(html, NOTE_STYLE))
947            data.append(Spacer(1, 20))
948
949        # Student data
950        data.append(render_student_data(view, student,
951                    omit_fields, lang=portal_language,
952                    slipname='admission_slip.pdf'))
953
954        # Text after student data
955        data.append(Spacer(1, 20))
956        if post_text is None:
957            datelist = student.history.messages[0].split()[0].split('-')
958            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
959            post_text = trans(_(
960                'Your Kofa student record was created on ${a}.',
961                mapping = {'a': creation_date}),
962                portal_language)
963        #html = format_html(post_text)
964        #data.append(Paragraph(html, NOTE_STYLE))
965
966        # Create pdf stream
967        view.response.setHeader(
968            'Content-Type', 'application/pdf')
969        pdf_stream = creator.create_pdf(
970            data, None, doc_title, author=author, footer=footer_text,
971            note=post_text, topMargin=topMargin,
972            letterhead_path=letterhead_path)
973        if mergefiles:
974            return self._mergeFiles(mergefiles, watermark, pdf_steam)
975        return pdf_stream
976
977    def renderPDFTranscript(self, view, filename='transcript.pdf',
978                  student=None,
979                  studentview=None,
980                  note=None,
981                  signatures=(),
982                  sigs_in_footer=(),
983                  digital_sigs=(),
984                  show_scans=True, topMargin=1.5,
985                  omit_fields=(),
986                  tableheader=None,
987                  no_passport=False,
988                  save_file=False):
989        """Render pdf slip of a transcripts.
990        """
991        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
992        # XXX: tell what the different parameters mean
993        style = getSampleStyleSheet()
994        creator = self.getPDFCreator(student)
995        data = []
996        doc_title = view.label
997        author = '%s (%s)' % (view.request.principal.title,
998                              view.request.principal.id)
999        footer_text = view.label.split('\n')
1000        if len(footer_text) > 2:
1001            # We can add a department in first line
1002            footer_text = footer_text[1]
1003        else:
1004            # Only the first line is used for the footer
1005            footer_text = footer_text[0]
1006        if getattr(student, 'student_id', None) is not None:
1007            footer_text = "%s - %s - " % (student.student_id, footer_text)
1008
1009        # Insert student data table
1010        if student is not None:
1011            #bd_translation = trans(_('Base Data'), portal_language)
1012            #data.append(Paragraph(bd_translation, HEADING_STYLE))
1013            data.append(render_student_data(
1014                studentview, view.context,
1015                omit_fields, lang=portal_language,
1016                slipname=filename,
1017                no_passport=no_passport))
1018
1019        transcript_data = view.context.getTranscriptData()
1020        levels_data = transcript_data[0]
1021
1022        contextdata = []
1023        f_label = trans(_('Course of Study:'), portal_language)
1024        f_label = Paragraph(f_label, ENTRY1_STYLE)
1025        f_text = formatted_text(view.context.certificate.longtitle)
1026        f_text = Paragraph(f_text, ENTRY1_STYLE)
1027        contextdata.append([f_label,f_text])
1028
1029        f_label = trans(_('Faculty:'), portal_language)
1030        f_label = Paragraph(f_label, ENTRY1_STYLE)
1031        f_text = formatted_text(
1032            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
1033        f_text = Paragraph(f_text, ENTRY1_STYLE)
1034        contextdata.append([f_label,f_text])
1035
1036        f_label = trans(_('Department:'), portal_language)
1037        f_label = Paragraph(f_label, ENTRY1_STYLE)
1038        f_text = formatted_text(
1039            view.context.certificate.__parent__.__parent__.longtitle)
1040        f_text = Paragraph(f_text, ENTRY1_STYLE)
1041        contextdata.append([f_label,f_text])
1042
1043        f_label = trans(_('Entry Session:'), portal_language)
1044        f_label = Paragraph(f_label, ENTRY1_STYLE)
1045        f_text = formatted_text(
1046            view.session_dict.get(view.context.entry_session))
1047        f_text = Paragraph(f_text, ENTRY1_STYLE)
1048        contextdata.append([f_label,f_text])
1049
1050        f_label = trans(_('Entry Mode:'), portal_language)
1051        f_label = Paragraph(f_label, ENTRY1_STYLE)
1052        f_text = formatted_text(view.studymode_dict.get(
1053            view.context.entry_mode))
1054        f_text = Paragraph(f_text, ENTRY1_STYLE)
1055        contextdata.append([f_label,f_text])
1056
1057        f_label = trans(_('Cumulative GPA:'), portal_language)
1058        f_label = Paragraph(f_label, ENTRY1_STYLE)
1059        format_float = getUtility(IKofaUtils).format_float
1060        cgpa = format_float(transcript_data[1], 3)
1061        if student.state == GRADUATED:
1062            f_text = formatted_text('%s (%s)' % (
1063                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
1064        else:
1065            f_text = formatted_text('%s' % cgpa)
1066        f_text = Paragraph(f_text, ENTRY1_STYLE)
1067        contextdata.append([f_label,f_text])
1068
1069        contexttable = Table(contextdata,style=SLIP_STYLE)
1070        data.append(contexttable)
1071
1072        transcripttables = render_transcript_data(
1073            view, tableheader, levels_data, lang=portal_language)
1074        data.extend(transcripttables)
1075
1076        # Insert signatures
1077        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
1078        # do not have a test for the following lines.
1079        if signatures and not sigs_in_footer:
1080            data.append(Spacer(1, 20))
1081            # Render one signature table per signature to
1082            # get date and signature in line.
1083            for signature in signatures:
1084                signaturetables = get_signature_tables(signature)
1085                data.append(signaturetables[0])
1086
1087        # Insert digital signatures
1088        if digital_sigs:
1089            data.append(Spacer(1, 20))
1090            sigs = digital_sigs.split('\n')
1091            for sig in sigs:
1092                data.append(Paragraph(sig, NOTE_STYLE))
1093
1094        view.response.setHeader(
1095            'Content-Type', 'application/pdf')
1096        try:
1097            pdf_stream = creator.create_pdf(
1098                data, None, doc_title, author=author, footer=footer_text,
1099                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
1100        except IOError:
1101            view.flash(_('Error in image file.'))
1102            return view.redirect(view.url(view.context))
1103        if save_file:
1104            self._saveTranscriptPDF(student, pdf_stream)
1105            return
1106        return pdf_stream
1107
1108    def renderPDFCourseticketsOverview(
1109            self, view, name, session, data, lecturers, orientation,
1110            title_length, note):
1111        """Render pdf slip of course tickets for a lecturer.
1112        """
1113        filename = '%s_%s_%s_%s.pdf' % (
1114            name, view.context.code, session, view.request.principal.id)
1115        try:
1116            session = academic_sessions_vocab.getTerm(session).title
1117        except LookupError:
1118            session = _('void')
1119        creator = getUtility(IPDFCreator, name=orientation)
1120        style = getSampleStyleSheet()
1121        pdf_data = []
1122        pdf_data += [Paragraph(
1123            translate(_('<b>Lecturer(s): ${a}</b>',
1124                      mapping = {'a':lecturers})), style["Normal"]),]
1125        pdf_data += [Paragraph(
1126            translate(_('<b>Credits: ${a}</b>',
1127                      mapping = {'a':view.context.credits})), style["Normal"]),]
1128        # Not used in base package.
1129        if data[1]:
1130            pdf_data += [Paragraph(
1131                translate(_('<b>${a}</b>',
1132                    mapping = {'a':data[1][0]})), style["Normal"]),]
1133            pdf_data += [Paragraph(
1134                translate(_('<b>${a}</b>',
1135                    mapping = {'a':data[1][1]})), style["Normal"]),]
1136            pdf_data += [Paragraph(
1137                translate(_('<b>Total Students: ${a}</b>',
1138                    mapping = {'a':data[1][2]})), style["Normal"]),]
1139            pdf_data += [Paragraph(
1140                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
1141                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
1142            pdf_data += [Paragraph(
1143                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
1144                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
1145            grade_stats = []
1146            for item in sorted(data[1][7].items()):
1147                grade_stats.append(('%s=%s' % (item[0], item[1])))
1148            grade_stats_string = ', '.join(grade_stats)
1149            pdf_data += [Paragraph(
1150                translate(_('<b>${a}</b>',
1151                mapping = {'a':grade_stats_string})), style["Normal"]),]
1152        pdf_data.append(Spacer(1, 20))
1153        colWidths = [None] * len(data[0][0])
1154        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE,
1155                           repeatRows=1)]
1156        # Process title if too long
1157        title = " ".join(view.context.title.split())
1158        ct = textwrap.fill(title, title_length)
1159        ft = "" # title
1160        #if len(textwrap.wrap(title, title_length)) > 1:
1161        #    ft = textwrap.wrap(title, title_length)[0] + ' ...'
1162        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1163            mapping = {'a':ct,
1164                       'b':view.context.code,
1165                       'd':session}))
1166        if name == 'attendance':
1167            doc_title += '\n' + translate(_('Attendance Sheet'))
1168        if name == 'coursetickets':
1169            doc_title += '\n' + translate(_('Course Tickets Overview'))
1170        #footer_title = translate(_('${a} (${b}) - ${d}',
1171        #    mapping = {'a':ft,
1172        #               'b':view.context.code,
1173        #               'd':session}))
1174        footer_title = translate(_('${b} - ${d}',
1175            mapping = {'b':view.context.code,
1176                       'd':session}))
1177        author = '%s (%s)' % (view.request.principal.title,
1178                              view.request.principal.id)
1179        view.response.setHeader(
1180            'Content-Type', 'application/pdf')
1181        view.response.setHeader(
1182            'Content-Disposition:', 'attachment; filename="%s' % filename)
1183        pdf_stream = creator.create_pdf(
1184            pdf_data, None, doc_title, author, footer_title + ' -', note
1185            )
1186        return pdf_stream
1187
1188    def updateCourseTickets(self, course):
1189        """Udate course tickets if course attributes were changed.
1190        """
1191        current_academic_session = grok.getSite()[
1192            'configuration'].current_academic_session
1193        if not current_academic_session:
1194            return
1195        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1196        coursetickets = cat.searchResults(
1197            code=(course.code, course.code),
1198            session=(current_academic_session,current_academic_session))
1199        number = 0
1200        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1201        for ticket in coursetickets:
1202            if ticket.credits == course.credits:
1203                continue
1204            if ticket.student.current_session != current_academic_session:
1205                continue
1206            if ticket.student.state not in (PAID,):
1207                continue
1208            number += 1
1209            ticket.student.__parent__.logger.info(
1210                '%s - %s %s/%s credits updated (%s->%s)' % (
1211                    ob_class, ticket.student.student_id,
1212                    ticket.level, ticket.code, course.credits,
1213                    ticket.credits))
1214            ticket.credits = course.credits
1215        return number
1216
1217    #: A dictionary which maps widget names to headlines. The headline
1218    #: is rendered in forms and on pdf slips above the respective
1219    #: display or input widget. There are no separating headlines
1220    #: in the base package.
1221    SEPARATORS_DICT = {}
1222
1223    #: A tuple containing names of file upload viewlets which are not shown
1224    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1225    #: in the base package. This attribute makes only sense, if intermediate
1226    #: custom packages are being used, like we do for all Nigerian portals.
1227    SKIP_UPLOAD_VIEWLETS = ()
1228
1229    #: A tuple containing the names of registration states in which changing of
1230    #: passport pictures is allowed.
1231    PORTRAIT_CHANGE_STATES = (ADMITTED,)
1232
1233    #: A tuple containing all exporter names referring to students or
1234    #: subobjects thereof.
1235    STUDENT_EXPORTER_NAMES = (
1236            'students',
1237            'studentstudycourses',
1238            'studentstudylevels',
1239            'coursetickets',
1240            'studentpayments',
1241            'bedtickets',
1242            'trimmed',
1243            'outstandingcourses',
1244            'unpaidpayments',
1245            'sfpaymentsoverview',
1246            'sessionpaymentsoverview',
1247            'studylevelsoverview',
1248            'combocard',
1249            'bursary',
1250            'accommodationpayments',
1251            'transcriptdata',
1252            'trimmedpayments',
1253            )
1254
1255    #: A tuple containing all exporter names needed for backing
1256    #: up student data
1257    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
1258            'studentstudylevels', 'coursetickets',
1259            'studentpayments', 'bedtickets')
1260
1261    # Maximum size of upload files in kB
1262    MAX_KB = 250
1263
1264    #: A prefix used when generating new student ids. Each student id will
1265    #: start with this string. The default is 'K' for Kofa.
1266    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.