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

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

Ease customization of portrait upload conditions.

  • Property svn:keywords set to Id
File size: 54.0 KB
Line 
1## $Id: utils.py 16671 2021-10-07 11:06:40Z 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 allowPortraitChange(self, student):
613        """Check if student is allowed to change the portrait file.
614        """
615        if student.state not in self.PORTRAIT_CHANGE_STATES:
616            return False
617        return True
618
619    def getAccommodationDetails(self, student):
620        """Determine the accommodation data of a student.
621        """
622        d = {}
623        d['error'] = u''
624        hostels = grok.getSite()['hostels']
625        d['booking_session'] = hostels.accommodation_session
626        d['allowed_states'] = hostels.accommodation_states
627        d['startdate'] = hostels.startdate
628        d['enddate'] = hostels.enddate
629        d['expired'] = hostels.expired
630        # Determine bed type
631        studycourse = student['studycourse']
632        certificate = getattr(studycourse,'certificate',None)
633        entry_session = studycourse.entry_session
634        current_level = studycourse.current_level
635        if None in (entry_session, current_level, certificate):
636            return d
637        end_level = certificate.end_level
638        if current_level == 10:
639            bt = 'pr'
640        elif entry_session == grok.getSite()['hostels'].accommodation_session:
641            bt = 'fr'
642        elif current_level >= end_level:
643            bt = 'fi'
644        else:
645            bt = 're'
646        if student.sex == 'f':
647            sex = 'female'
648        else:
649            sex = 'male'
650        special_handling = 'regular'
651        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
652        return d
653
654    def checkAccommodationRequirements(self, student, acc_details):
655        if acc_details.get('expired', False):
656            startdate = acc_details.get('startdate')
657            enddate = acc_details.get('enddate')
658            if startdate and enddate:
659                tz = getUtility(IKofaUtils).tzinfo
660                startdate = to_timezone(
661                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
662                enddate = to_timezone(
663                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
664                return _("Outside booking period: ${a} - ${b}",
665                         mapping = {'a': startdate, 'b': enddate})
666            else:
667                return _("Outside booking period.")
668        if not acc_details.get('bt'):
669            return _("Your data are incomplete.")
670        if not student.state in acc_details['allowed_states']:
671            return _("You are in the wrong registration state.")
672        if student['studycourse'].current_session != acc_details[
673            'booking_session']:
674            return _('Your current session does not '
675                     'match accommodation session.')
676        bsession = str(acc_details['booking_session'])
677        if bsession in student['accommodation'].keys() \
678            and not 'booking expired' in \
679            student['accommodation'][bsession].bed_coordinates:
680            return _('You already booked a bed space in '
681                     'current accommodation session.')
682        return
683
684    def selectBed(self, available_beds):
685        """Select a bed from a filtered list of available beds.
686        In the base configuration beds are sorted by the sort id
687        of the hostel and the bed number. The first bed found in
688        this sorted list is taken.
689        """
690        sorted_beds = sorted(available_beds,
691                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
692        return sorted_beds[0]
693
694    def GPABoundaries(self, faccode=None, depcode=None, certcode=None):
695        return ((1, 'Fail'),
696               (1.5, 'Pass'),
697               (2.4, '3rd Class'),
698               (3.5, '2nd Class Lower'),
699               (4.5, '2nd Class Upper'),
700               (5, '1st Class'))
701
702    def getClassFromCGPA(self, gpa, student):
703        """Determine the class of degree. In some custom packages
704        this class depends on e.g. the entry session of the student. In the
705        base package, it does not.
706        """
707        if gpa < self.GPABoundaries()[0][0]:
708            return 0, self.GPABoundaries()[0][1]
709        if gpa < self.GPABoundaries()[1][0]:
710            return 1, self.GPABoundaries()[1][1]
711        if gpa < self.GPABoundaries()[2][0]:
712            return 2, self.GPABoundaries()[2][1]
713        if gpa < self.GPABoundaries()[3][0]:
714            return 3, self.GPABoundaries()[3][1]
715        if gpa < self.GPABoundaries()[4][0]:
716            return 4, self.GPABoundaries()[4][1]
717        if gpa <= self.GPABoundaries()[5][0]:
718            return 5, self.GPABoundaries()[5][1]
719        return
720
721    def getDegreeClassNumber(self, level_obj):
722        """Get degree class number (used for SessionResultsPresentation
723        reports).
724        """
725        if level_obj.gpa_params[1] == 0:
726            # No credits weighted
727            return 6
728        return self.getClassFromCGPA(
729            level_obj.cumulative_params[0], level_obj.student)[0]
730
731    def _saveTranscriptPDF(self, student, transcript):
732        """Create a transcript PDF file and store it in student folder.
733        """
734        file_store = getUtility(IExtFileStore)
735        file_id = IFileStoreNameChooser(student).chooseName(
736            attr="final_transcript.pdf")
737        file_store.createFile(file_id, StringIO(transcript))
738        return
739
740    def warnCreditsOOR(self, studylevel, course=None):
741        """Return message if credits are out of range. In the base
742        package only maximum credits is set.
743        """
744        if course and studylevel.total_credits + course.credits > 50:
745            return _('Maximum credits exceeded.')
746        elif studylevel.total_credits > 50:
747            return _('Maximum credits exceeded.')
748        return
749
750    def warnCourseAlreadyPassed(self, studylevel, course):
751        """Return message if course has already been passed at
752        previous levels.
753        """
754        for slevel in studylevel.__parent__.values():
755            for cticket in slevel.values():
756                if cticket.code == course.code \
757                    and cticket.total_score >= cticket.passmark:
758                    return _('Course has already been passed at previous level.')
759        return False
760
761    def getBedCoordinates(self, bedticket):
762        """Return descriptive bed coordinates.
763        This method can be used to customize the `display_coordinates`
764        property method in order to  display a
765        customary description of the bed space.
766        """
767        return bedticket.bed_coordinates
768
769    def clearance_disabled_message(self, student):
770        """Render message if clearance is disabled.
771        """
772        try:
773            session_config = grok.getSite()[
774                'configuration'][str(student.current_session)]
775        except KeyError:
776            return _('Session configuration object is not available.')
777        if not session_config.clearance_enabled:
778            return _('Clearance is disabled for this session.')
779        return None
780
781    def getPDFCreator(self, context):
782        """Get a pdf creator suitable for `context`.
783        The default implementation always returns the default creator.
784        """
785        return getUtility(IPDFCreator)
786
787    def _mergeFiles(self, mergefiles, watermark, pdf_stream):
788        merger = PdfFileMerger()
789        merger.append(StringIO(pdf_stream))
790        for file in mergefiles:
791            if watermark:
792                # Pass through all pages of each file
793                # and merge with watermark page. Paint
794                # watermark first to make it transparent.
795                marked_file = PdfFileWriter()
796                orig_file = PdfFileReader(file)
797                num_pages = orig_file.getNumPages()
798                for num in range(num_pages):
799                    watermark_file = PdfFileReader(watermark)
800                    page = watermark_file.getPage(0)
801                    page.mergePage(orig_file.getPage(num))
802                    marked_file.addPage(page)
803                # Save into a file-like object
804                tmp1 = StringIO()
805                marked_file.write(tmp1)
806                # Append the file-like object
807                merger.append(tmp1)
808            else:
809                # Just append the file object
810                merger.append(file)
811        # Save into a file-like object
812        tmp2 = StringIO()
813        merger.write(tmp2)
814        return tmp2.getvalue()
815
816    def renderPDF(self, view, filename='slip.pdf', student=None,
817                  studentview=None,
818                  tableheader=[], tabledata=[],
819                  note=None, signatures=None, sigs_in_footer=(),
820                  show_scans=True, topMargin=1.5,
821                  omit_fields=(),
822                  mergefiles=None, watermark=None,
823                  pagebreak=False):
824        """Render pdf slips for various pages (also some pages
825        in the applicants module).
826        """
827        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
828        # XXX: tell what the different parameters mean
829        style = getSampleStyleSheet()
830        creator = self.getPDFCreator(student)
831        data = []
832        doc_title = view.label
833        author = '%s (%s)' % (view.request.principal.title,
834                              view.request.principal.id)
835        footer_text = view.label.split('\n')
836        if len(footer_text) > 2:
837            # We can add a department in first line, second line is used
838            footer_text = footer_text[1]
839        else:
840            # Only the first line is used for the footer
841            footer_text = footer_text[0]
842        if getattr(student, 'student_id', None) is not None:
843            footer_text = "%s - %s - " % (student.student_id, footer_text)
844
845        # Insert student data table
846        if student is not None:
847            if view.form_fields:
848                bd_translation = trans(_('Base Data'), portal_language)
849                data.append(Paragraph(bd_translation, HEADING_STYLE))
850            data.append(render_student_data(
851                studentview, view.context, omit_fields, lang=portal_language,
852                slipname=filename))
853
854        # Insert widgets
855        if view.form_fields:
856            data.append(Paragraph(view.title, HEADING_STYLE))
857            separators = getattr(self, 'SEPARATORS_DICT', {})
858            table = creator.getWidgetsTable(
859                view.form_fields, view.context, None, lang=portal_language,
860                separators=separators)
861            data.append(table)
862
863        # Insert scanned docs
864        if show_scans:
865            data.extend(docs_as_flowables(view, portal_language))
866
867        # Insert history
868        if filename == 'clearance_slip.pdf':
869            hist_translation = trans(_('Workflow History'), portal_language)
870            data.append(Paragraph(hist_translation, HEADING_STYLE))
871            data.extend(creator.fromStringList(student.history.messages))
872
873        # Insert content tables (optionally on second page)
874        if hasattr(view, 'tabletitle'):
875            for i in range(len(view.tabletitle)):
876                if tabledata[i] and tableheader[i]:
877                    tabletitle = view.tabletitle[i]
878                    if tabletitle.startswith('_PB_'):
879                        data.append(PageBreak())
880                        tabletitle = view.tabletitle[i].strip('_PB_')
881                    #data.append(Spacer(1, 20))
882                    data.append(Paragraph(tabletitle, HEADING_STYLE))
883                    data.append(Spacer(1, 8))
884                    contenttable = render_table_data(tableheader[i],tabledata[i])
885                    data.append(contenttable)
886
887        # Insert signatures
888        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
889        # do not have a test for the following lines.
890        if signatures and not sigs_in_footer:
891            # Insert page break if necessary, else some space
892            if pagebreak:
893                data.append(PageBreak())
894            else:
895                data.append(Spacer(1, 20))
896            # Render one signature table per signature to
897            # get date and signature in line.
898            for signature in signatures:
899                signaturetables = get_signature_tables(signature)
900                data.append(signaturetables[0])
901
902        view.response.setHeader(
903            'Content-Type', 'application/pdf')
904        try:
905            pdf_stream = creator.create_pdf(
906                data, None, doc_title, author=author, footer=footer_text,
907                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
908        except IOError:
909            view.flash('Error in image file.')
910            return view.redirect(view.url(view.context))
911        except LayoutError, err:
912            view.flash(
913                'PDF file could not be created. Reportlab error message: %s'
914                % escape(err.message),
915                type="danger")
916            return view.redirect(view.url(view.context))
917        if mergefiles:
918            return self._mergeFiles(mergefiles, watermark, pdf_stream)
919        return pdf_stream
920
921    def _admissionText(self, student, portal_language):
922        inst_name = grok.getSite()['configuration'].name
923        text = trans(_(
924            'This is to inform you that you have been provisionally'
925            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
926            portal_language)
927        return text
928
929    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
930                                 pre_text=None, post_text=None,
931                                 topMargin = 1.5,
932                                 letterhead_path=None,
933                                 mergefiles=None, watermark=None):
934        """Render pdf admission letter.
935        """
936        if student is None:
937            return
938        style = getSampleStyleSheet()
939        if letterhead_path:
940            creator = getUtility(IPDFCreator, name='letter')
941        else:
942            creator = self.getPDFCreator(student)
943        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
944        data = []
945        doc_title = view.label
946        author = '%s (%s)' % (view.request.principal.title,
947                              view.request.principal.id)
948        footer_text = view.label.split('\n')
949        if len(footer_text) > 2:
950            # We can add a department in first line
951            footer_text = footer_text[1]
952        else:
953            # Only the first line is used for the footer
954            footer_text = footer_text[0]
955        if getattr(student, 'student_id', None) is not None:
956            footer_text = "%s - %s - " % (student.student_id, footer_text)
957
958        # Text before student data
959        if pre_text is None:
960            html = format_html(self._admissionText(student, portal_language))
961        else:
962            html = format_html(pre_text)
963        if html:
964            data.append(Paragraph(html, NOTE_STYLE))
965            data.append(Spacer(1, 20))
966
967        # Student data
968        data.append(render_student_data(view, student,
969                    omit_fields, lang=portal_language,
970                    slipname='admission_slip.pdf'))
971
972        # Text after student data
973        data.append(Spacer(1, 20))
974        if post_text is None:
975            datelist = student.history.messages[0].split()[0].split('-')
976            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
977            post_text = trans(_(
978                'Your Kofa student record was created on ${a}.',
979                mapping = {'a': creation_date}),
980                portal_language)
981        #html = format_html(post_text)
982        #data.append(Paragraph(html, NOTE_STYLE))
983
984        # Create pdf stream
985        view.response.setHeader(
986            'Content-Type', 'application/pdf')
987        pdf_stream = creator.create_pdf(
988            data, None, doc_title, author=author, footer=footer_text,
989            note=post_text, topMargin=topMargin,
990            letterhead_path=letterhead_path)
991        if mergefiles:
992            return self._mergeFiles(mergefiles, watermark, pdf_stream)
993        return pdf_stream
994
995    def renderPDFTranscript(self, view, filename='transcript.pdf',
996                  student=None,
997                  studentview=None,
998                  note=None,
999                  signatures=(),
1000                  sigs_in_footer=(),
1001                  digital_sigs=(),
1002                  show_scans=True, topMargin=1.5,
1003                  omit_fields=(),
1004                  tableheader=None,
1005                  no_passport=False,
1006                  save_file=False):
1007        """Render pdf slip of a transcripts.
1008        """
1009        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1010        # XXX: tell what the different parameters mean
1011        style = getSampleStyleSheet()
1012        creator = self.getPDFCreator(student)
1013        data = []
1014        doc_title = view.label
1015        author = '%s (%s)' % (view.request.principal.title,
1016                              view.request.principal.id)
1017        footer_text = view.label.split('\n')
1018        if len(footer_text) > 2:
1019            # We can add a department in first line
1020            footer_text = footer_text[1]
1021        else:
1022            # Only the first line is used for the footer
1023            footer_text = footer_text[0]
1024        if getattr(student, 'student_id', None) is not None:
1025            footer_text = "%s - %s - " % (student.student_id, footer_text)
1026
1027        # Insert student data table
1028        if student is not None:
1029            #bd_translation = trans(_('Base Data'), portal_language)
1030            #data.append(Paragraph(bd_translation, HEADING_STYLE))
1031            data.append(render_student_data(
1032                studentview, view.context,
1033                omit_fields, lang=portal_language,
1034                slipname=filename,
1035                no_passport=no_passport))
1036
1037        transcript_data = view.context.getTranscriptData()
1038        levels_data = transcript_data[0]
1039
1040        contextdata = []
1041        f_label = trans(_('Course of Study:'), portal_language)
1042        f_label = Paragraph(f_label, ENTRY1_STYLE)
1043        f_text = formatted_text(view.context.certificate.longtitle)
1044        f_text = Paragraph(f_text, ENTRY1_STYLE)
1045        contextdata.append([f_label,f_text])
1046
1047        f_label = trans(_('Faculty:'), portal_language)
1048        f_label = Paragraph(f_label, ENTRY1_STYLE)
1049        f_text = formatted_text(
1050            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
1051        f_text = Paragraph(f_text, ENTRY1_STYLE)
1052        contextdata.append([f_label,f_text])
1053
1054        f_label = trans(_('Department:'), portal_language)
1055        f_label = Paragraph(f_label, ENTRY1_STYLE)
1056        f_text = formatted_text(
1057            view.context.certificate.__parent__.__parent__.longtitle)
1058        f_text = Paragraph(f_text, ENTRY1_STYLE)
1059        contextdata.append([f_label,f_text])
1060
1061        f_label = trans(_('Entry Session:'), portal_language)
1062        f_label = Paragraph(f_label, ENTRY1_STYLE)
1063        f_text = formatted_text(
1064            view.session_dict.get(view.context.entry_session))
1065        f_text = Paragraph(f_text, ENTRY1_STYLE)
1066        contextdata.append([f_label,f_text])
1067
1068        f_label = trans(_('Entry Mode:'), portal_language)
1069        f_label = Paragraph(f_label, ENTRY1_STYLE)
1070        f_text = formatted_text(view.studymode_dict.get(
1071            view.context.entry_mode))
1072        f_text = Paragraph(f_text, ENTRY1_STYLE)
1073        contextdata.append([f_label,f_text])
1074
1075        f_label = trans(_('Cumulative GPA:'), portal_language)
1076        f_label = Paragraph(f_label, ENTRY1_STYLE)
1077        format_float = getUtility(IKofaUtils).format_float
1078        cgpa = format_float(transcript_data[1], 3)
1079        if student.state == GRADUATED:
1080            f_text = formatted_text('%s (%s)' % (
1081                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
1082        else:
1083            f_text = formatted_text('%s' % cgpa)
1084        f_text = Paragraph(f_text, ENTRY1_STYLE)
1085        contextdata.append([f_label,f_text])
1086
1087        contexttable = Table(contextdata,style=SLIP_STYLE)
1088        data.append(contexttable)
1089
1090        transcripttables = render_transcript_data(
1091            view, tableheader, levels_data, lang=portal_language)
1092        data.extend(transcripttables)
1093
1094        # Insert signatures
1095        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
1096        # do not have a test for the following lines.
1097        if signatures and not sigs_in_footer:
1098            data.append(Spacer(1, 20))
1099            # Render one signature table per signature to
1100            # get date and signature in line.
1101            for signature in signatures:
1102                signaturetables = get_signature_tables(signature)
1103                data.append(signaturetables[0])
1104
1105        # Insert digital signatures
1106        if digital_sigs:
1107            data.append(Spacer(1, 20))
1108            sigs = digital_sigs.split('\n')
1109            for sig in sigs:
1110                data.append(Paragraph(sig, NOTE_STYLE))
1111
1112        view.response.setHeader(
1113            'Content-Type', 'application/pdf')
1114        try:
1115            pdf_stream = creator.create_pdf(
1116                data, None, doc_title, author=author, footer=footer_text,
1117                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
1118        except IOError:
1119            view.flash(_('Error in image file.'))
1120            return view.redirect(view.url(view.context))
1121        if save_file:
1122            self._saveTranscriptPDF(student, pdf_stream)
1123            return
1124        return pdf_stream
1125
1126    def renderPDFCourseticketsOverview(
1127            self, view, name, session, data, lecturers, orientation,
1128            title_length, note):
1129        """Render pdf slip of course tickets for a lecturer.
1130        """
1131        filename = '%s_%s_%s_%s.pdf' % (
1132            name, view.context.code, session, view.request.principal.id)
1133        try:
1134            session = academic_sessions_vocab.getTerm(session).title
1135        except LookupError:
1136            session = _('void')
1137        creator = getUtility(IPDFCreator, name=orientation)
1138        style = getSampleStyleSheet()
1139        pdf_data = []
1140        pdf_data += [Paragraph(
1141            translate(_('<b>Lecturer(s): ${a}</b>',
1142                      mapping = {'a':lecturers})), style["Normal"]),]
1143        pdf_data += [Paragraph(
1144            translate(_('<b>Credits: ${a}</b>',
1145                      mapping = {'a':view.context.credits})), style["Normal"]),]
1146        # Not used in base package.
1147        if data[1]:
1148            pdf_data += [Paragraph(
1149                translate(_('<b>${a}</b>',
1150                    mapping = {'a':data[1][0]})), style["Normal"]),]
1151            pdf_data += [Paragraph(
1152                translate(_('<b>${a}</b>',
1153                    mapping = {'a':data[1][1]})), style["Normal"]),]
1154            pdf_data += [Paragraph(
1155                translate(_('<b>Total Students: ${a}</b>',
1156                    mapping = {'a':data[1][2]})), style["Normal"]),]
1157            pdf_data += [Paragraph(
1158                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
1159                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
1160            pdf_data += [Paragraph(
1161                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
1162                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
1163            grade_stats = []
1164            for item in sorted(data[1][7].items()):
1165                grade_stats.append(('%s=%s' % (item[0], item[1])))
1166            grade_stats_string = ', '.join(grade_stats)
1167            pdf_data += [Paragraph(
1168                translate(_('<b>${a}</b>',
1169                mapping = {'a':grade_stats_string})), style["Normal"]),]
1170        pdf_data.append(Spacer(1, 20))
1171        colWidths = [None] * len(data[0][0])
1172        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE,
1173                           repeatRows=1)]
1174        # Process title if too long
1175        title = " ".join(view.context.title.split())
1176        ct = textwrap.fill(title, title_length)
1177        ft = "" # title
1178        #if len(textwrap.wrap(title, title_length)) > 1:
1179        #    ft = textwrap.wrap(title, title_length)[0] + ' ...'
1180        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1181            mapping = {'a':ct,
1182                       'b':view.context.code,
1183                       'd':session}))
1184        if name == 'attendance':
1185            doc_title += '\n' + translate(_('Attendance Sheet'))
1186        if name == 'coursetickets':
1187            doc_title += '\n' + translate(_('Course Tickets Overview'))
1188        #footer_title = translate(_('${a} (${b}) - ${d}',
1189        #    mapping = {'a':ft,
1190        #               'b':view.context.code,
1191        #               'd':session}))
1192        footer_title = translate(_('${b} - ${d}',
1193            mapping = {'b':view.context.code,
1194                       'd':session}))
1195        author = '%s (%s)' % (view.request.principal.title,
1196                              view.request.principal.id)
1197        view.response.setHeader(
1198            'Content-Type', 'application/pdf')
1199        view.response.setHeader(
1200            'Content-Disposition:', 'attachment; filename="%s' % filename)
1201        pdf_stream = creator.create_pdf(
1202            pdf_data, None, doc_title, author, footer_title + ' -', note
1203            )
1204        return pdf_stream
1205
1206    def updateCourseTickets(self, course):
1207        """Udate course tickets if course attributes were changed.
1208        """
1209        current_academic_session = grok.getSite()[
1210            'configuration'].current_academic_session
1211        if not current_academic_session:
1212            return
1213        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1214        coursetickets = cat.searchResults(
1215            code=(course.code, course.code),
1216            session=(current_academic_session,current_academic_session))
1217        number = 0
1218        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1219        for ticket in coursetickets:
1220            if ticket.credits == course.credits:
1221                continue
1222            if ticket.student.current_session != current_academic_session:
1223                continue
1224            if ticket.student.state not in (PAID,):
1225                continue
1226            number += 1
1227            ticket.student.__parent__.logger.info(
1228                '%s - %s %s/%s credits updated (%s->%s)' % (
1229                    ob_class, ticket.student.student_id,
1230                    ticket.level, ticket.code, course.credits,
1231                    ticket.credits))
1232            ticket.credits = course.credits
1233        return number
1234
1235    #: A dictionary which maps widget names to headlines. The headline
1236    #: is rendered in forms and on pdf slips above the respective
1237    #: display or input widget. There are no separating headlines
1238    #: in the base package.
1239    SEPARATORS_DICT = {}
1240
1241    #: A tuple containing names of file upload viewlets which are not shown
1242    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1243    #: in the base package. This attribute makes only sense, if intermediate
1244    #: custom packages are being used, like we do for all Nigerian portals.
1245    SKIP_UPLOAD_VIEWLETS = ()
1246
1247    #: A tuple containing the names of registration states in which changing of
1248    #: passport pictures is allowed.
1249    PORTRAIT_CHANGE_STATES = (ADMITTED,)
1250
1251    #: A tuple containing the names of registration states in which changing of
1252    #: scanned signatures is allowed.
1253    SIGNATURE_CHANGE_STATES = ()
1254
1255    #: A tuple containing all exporter names referring to students or
1256    #: subobjects thereof.
1257    STUDENT_EXPORTER_NAMES = (
1258            'students',
1259            'studentstudycourses',
1260            'studentstudylevels',
1261            'coursetickets',
1262            'studentpayments',
1263            'bedtickets',
1264            'trimmed',
1265            'outstandingcourses',
1266            'unpaidpayments',
1267            'sfpaymentsoverview',
1268            'sessionpaymentsoverview',
1269            'studylevelsoverview',
1270            'combocard',
1271            'bursary',
1272            'accommodationpayments',
1273            'transcriptdata',
1274            'trimmedpayments',
1275            )
1276
1277    #: A tuple containing all exporter names needed for backing
1278    #: up student data
1279    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
1280            'studentstudylevels', 'coursetickets',
1281            'studentpayments', 'bedtickets')
1282
1283    # Maximum size of upload files in kB
1284    MAX_KB = 250
1285
1286    #: A prefix used when generating new student ids. Each student id will
1287    #: start with this string. The default is 'K' for Kofa.
1288    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.