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

Last change on this file since 15665 was 15664, checked in by Henrik Bettermann, 5 years ago

Implement combi payments (tests will follow).

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