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

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

Fix typo.

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