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

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

Insert pagebreak before signature tables if necessary (not tested in base package).

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