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

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

Prepare renderPDFAdmissionLetter for merging with another pdf document.

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