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

Last change on this file since 17182 was 17182, checked in by Henrik Bettermann, 22 months ago

p_session of bed allocation and maintenance fees is always the portal’s accommodation session and not the student’s current session.

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