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

Last change on this file since 16609 was 16609, checked in by Henrik Bettermann, 3 years ago

Enable students and officers to upload scanned signature in the same
way as passport pictures are handled. Some tabs and titles had to be
renamed. The placeholder file, which is still the portrait
placeholder, are no longer shown on base pages.

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