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

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

Allow students to book accommodation also if they are in previous sessions (not activated in base package).

  • Property svn:keywords set to Id
File size: 54.6 KB
RevLine 
[7191]1## $Id: utils.py 17176 2022-11-25 11:54:59Z 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':
[8595]492            p_item = self.getAccommodationDetails(student)['bt']
493            amount = academic_session.booking_fee
[9423]494        elif category == 'hostel_maintenance':
[10681]495            amount = 0.0
[9429]496            bedticket = student['accommodation'].get(
497                str(student.current_session), None)
[13501]498            if bedticket is not None and bedticket.bed is not None:
[9429]499                p_item = bedticket.bed_coordinates
[10681]500                if bedticket.bed.__parent__.maint_fee > 0:
501                    amount = bedticket.bed.__parent__.maint_fee
502                else:
503                    # fallback
504                    amount = academic_session.maint_fee
[9429]505            else:
[13505]506                return _(u'No bed allocated.'), None
[15664]507        elif category == 'combi' and combi:
508            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
509            for cat in combi:
510                fee_name = cat + '_fee'
511                cat_amount = getattr(academic_session, fee_name, 0.0)
512                if not cat_amount:
513                    return _('%s undefined.' % categories[cat]), None
514                amount += cat_amount
515                p_item += u'%s + ' % categories[cat]
516            p_item = p_item.strip(' + ')
[15652]517        else:
518            fee_name = category + '_fee'
519            amount = getattr(academic_session, fee_name, 0.0)
[8595]520        if amount in (0.0, None):
[9517]521            return _('Amount could not be determined.'), None
[11641]522        if self.samePaymentMade(student, category, p_item, p_session):
523            return _('This type of payment has already been made.'), None
[11451]524        if self._isPaymentDisabled(p_session, category, student):
[13797]525            return _('This category of payments has been disabled.'), None
[8708]526        payment = createObject(u'waeup.StudentOnlinePayment')
[8951]527        timestamp = ("%d" % int(time()*10000))[1:]
[8595]528        payment.p_id = "p%s" % timestamp
529        payment.p_category = category
530        payment.p_item = p_item
531        payment.p_session = p_session
532        payment.p_level = p_level
[9148]533        payment.p_current = p_current
[8595]534        payment.amount_auth = amount
[15685]535        payment.p_combi = combi
[8595]536        return None, payment
[7019]537
[9868]538    def setBalanceDetails(self, category, student,
[9864]539            balance_session, balance_level, balance_amount):
[13124]540        """Create a balance payment ticket and set the payment data
541        as selected by the student.
[9864]542        """
[9868]543        p_item = u'Balance'
[9864]544        p_session = balance_session
545        p_level = balance_level
546        p_current = False
547        amount = balance_amount
548        academic_session = self._getSessionConfiguration(p_session)
549        if academic_session == None:
550            return _(u'Session configuration object is not available.'), None
[9874]551        if amount in (0.0, None) or amount < 0:
552            return _('Amount must be greater than 0.'), None
[9864]553        payment = createObject(u'waeup.StudentOnlinePayment')
554        timestamp = ("%d" % int(time()*10000))[1:]
555        payment.p_id = "p%s" % timestamp
[9868]556        payment.p_category = category
[9864]557        payment.p_item = p_item
558        payment.p_session = p_session
559        payment.p_level = p_level
560        payment.p_current = p_current
561        payment.amount_auth = amount
562        return None, payment
563
[12896]564    def increaseMatricInteger(self, student):
565        """Increase counter for matric numbers.
566        This counter can be a centrally stored attribute or an attribute of
567        faculties, departments or certificates. In the base package the counter
[13124]568        is as an attribute of the site configuration container.
[12896]569        """
570        grok.getSite()['configuration'].next_matric_integer += 1
571        return
572
[11595]573    def constructMatricNumber(self, student):
[12896]574        """Fetch the matric number counter which fits the student and
575        construct the new matric number of the student.
[12902]576        In the base package the counter is returned which is as an attribute
[13124]577        of the site configuration container.
[12896]578        """
[11595]579        next_integer = grok.getSite()['configuration'].next_matric_integer
580        if next_integer == 0:
[11619]581            return _('Matriculation number cannot be set.'), None
582        return None, unicode(next_integer)
[11589]583
584    def setMatricNumber(self, student):
[13124]585        """Set matriculation number of student. If the student's matric number
586        is unset a new matric number is
[12896]587        constructed according to the matriculation number construction rules
[13124]588        defined in the `constructMatricNumber` method. The new matric number is
[12896]589        set, the students catalog updated. The corresponding matric number
590        counter is increased by one.
[11589]591
592        This method is tested but not used in the base package. It can
593        be used in custom packages by adding respective views
[13124]594        and by customizing `increaseMatricInteger` and `constructMatricNumber`
[12896]595        according to the university's matriculation number construction rules.
[11589]596
[12896]597        The method can be disabled by setting the counter to zero.
[11589]598        """
599        if student.matric_number is not None:
600            return _('Matriculation number already set.'), None
[11590]601        if student.certcode is None:
602            return _('No certificate assigned.'), None
[11619]603        error, matric_number = self.constructMatricNumber(student)
604        if error:
605            return error, None
[11589]606        try:
[11592]607            student.matric_number = matric_number
[11589]608        except MatNumNotInSource:
[13224]609            return _('Matriculation number %s exists.' % matric_number), None
[11589]610        notify(grok.ObjectModifiedEvent(student))
[12896]611        self.increaseMatricInteger(student)
[11595]612        return None, matric_number
[11589]613
[16671]614    def allowPortraitChange(self, student):
615        """Check if student is allowed to change the portrait file.
616        """
617        if student.state not in self.PORTRAIT_CHANGE_STATES:
618            return False
619        return True
620
[7186]621    def getAccommodationDetails(self, student):
[9219]622        """Determine the accommodation data of a student.
[7841]623        """
[7150]624        d = {}
625        d['error'] = u''
[8685]626        hostels = grok.getSite()['hostels']
627        d['booking_session'] = hostels.accommodation_session
628        d['allowed_states'] = hostels.accommodation_states
[8688]629        d['startdate'] = hostels.startdate
630        d['enddate'] = hostels.enddate
631        d['expired'] = hostels.expired
[7150]632        # Determine bed type
633        studycourse = student['studycourse']
[7369]634        certificate = getattr(studycourse,'certificate',None)
[7150]635        entry_session = studycourse.entry_session
636        current_level = studycourse.current_level
[9187]637        if None in (entry_session, current_level, certificate):
638            return d
[7369]639        end_level = certificate.end_level
[9148]640        if current_level == 10:
641            bt = 'pr'
642        elif entry_session == grok.getSite()['hostels'].accommodation_session:
[7150]643            bt = 'fr'
644        elif current_level >= end_level:
645            bt = 'fi'
646        else:
647            bt = 're'
648        if student.sex == 'f':
649            sex = 'female'
650        else:
651            sex = 'male'
652        special_handling = 'regular'
653        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
654        return d
[7019]655
[17176]656    ACCOMMODATION_SPAN = 0
657
[13247]658    def checkAccommodationRequirements(self, student, acc_details):
659        if acc_details.get('expired', False):
660            startdate = acc_details.get('startdate')
661            enddate = acc_details.get('enddate')
662            if startdate and enddate:
663                tz = getUtility(IKofaUtils).tzinfo
664                startdate = to_timezone(
665                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
666                enddate = to_timezone(
667                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
668                return _("Outside booking period: ${a} - ${b}",
669                         mapping = {'a': startdate, 'b': enddate})
670            else:
671                return _("Outside booking period.")
672        if not acc_details.get('bt'):
673            return _("Your data are incomplete.")
674        if not student.state in acc_details['allowed_states']:
675            return _("You are in the wrong registration state.")
[17176]676        if  acc_details['booking_session'] - student[
677            'studycourse'].current_session > self.ACCOMMODATION_SPAN:
678            return _('Your current session does not allow ' + \
679                    'to book accommodation.')
[15306]680        bsession = str(acc_details['booking_session'])
681        if bsession in student['accommodation'].keys() \
682            and not 'booking expired' in \
683            student['accommodation'][bsession].bed_coordinates:
[13247]684            return _('You already booked a bed space in '
685                     'current accommodation session.')
686        return
687
[15705]688    def selectBed(self, available_beds):
[13457]689        """Select a bed from a filtered list of available beds.
690        In the base configuration beds are sorted by the sort id
691        of the hostel and the bed number. The first bed found in
692        this sorted list is taken.
[7841]693        """
[13457]694        sorted_beds = sorted(available_beds,
695                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
696        return sorted_beds[0]
[7150]697
[15879]698    def GPABoundaries(self, faccode=None, depcode=None, certcode=None):
699        return ((1, 'Fail'),
700               (1.5, 'Pass'),
701               (2.4, '3rd Class'),
702               (3.5, '2nd Class Lower'),
703               (4.5, '2nd Class Upper'),
704               (5, '1st Class'))
[9979]705
[15879]706    def getClassFromCGPA(self, gpa, student):
707        """Determine the class of degree. In some custom packages
708        this class depends on e.g. the entry session of the student. In the
709        base package, it does not.
[9191]710        """
[15879]711        if gpa < self.GPABoundaries()[0][0]:
712            return 0, self.GPABoundaries()[0][1]
713        if gpa < self.GPABoundaries()[1][0]:
714            return 1, self.GPABoundaries()[1][1]
715        if gpa < self.GPABoundaries()[2][0]:
716            return 2, self.GPABoundaries()[2][1]
717        if gpa < self.GPABoundaries()[3][0]:
718            return 3, self.GPABoundaries()[3][1]
719        if gpa < self.GPABoundaries()[4][0]:
720            return 4, self.GPABoundaries()[4][1]
721        if gpa <= self.GPABoundaries()[5][0]:
722            return 5, self.GPABoundaries()[5][1]
723        return
[9191]724
[15879]725    def getDegreeClassNumber(self, level_obj):
726        """Get degree class number (used for SessionResultsPresentation
727        reports).
728        """
729        if level_obj.gpa_params[1] == 0:
730            # No credits weighted
731            return 6
732        return self.getClassFromCGPA(
733            level_obj.cumulative_params[0], level_obj.student)[0]
[9191]734
[15879]735    def _saveTranscriptPDF(self, student, transcript):
736        """Create a transcript PDF file and store it in student folder.
737        """
738        file_store = getUtility(IExtFileStore)
739        file_id = IFileStoreNameChooser(student).chooseName(
740            attr="final_transcript.pdf")
741        file_store.createFile(file_id, StringIO(transcript))
742        return
[9191]743
[15879]744    def warnCreditsOOR(self, studylevel, course=None):
745        """Return message if credits are out of range. In the base
746        package only maximum credits is set.
747        """
748        if course and studylevel.total_credits + course.credits > 50:
749            return _('Maximum credits exceeded.')
750        elif studylevel.total_credits > 50:
751            return _('Maximum credits exceeded.')
752        return
[9191]753
[15986]754    def warnCourseAlreadyPassed(self, studylevel, course):
755        """Return message if course has already been passed at
756        previous levels.
757        """
758        for slevel in studylevel.__parent__.values():
759            for cticket in slevel.values():
760                if cticket.code == course.code \
761                    and cticket.total_score >= cticket.passmark:
762                    return _('Course has already been passed at previous level.')
763        return False
764
[15879]765    def getBedCoordinates(self, bedticket):
766        """Return descriptive bed coordinates.
767        This method can be used to customize the `display_coordinates`
768        property method in order to  display a
769        customary description of the bed space.
770        """
771        return bedticket.bed_coordinates
[9191]772
[15879]773    def clearance_disabled_message(self, student):
774        """Render message if clearance is disabled.
775        """
776        try:
777            session_config = grok.getSite()[
778                'configuration'][str(student.current_session)]
779        except KeyError:
780            return _('Session configuration object is not available.')
781        if not session_config.clearance_enabled:
782            return _('Clearance is disabled for this session.')
783        return None
784
[9949]785    def getPDFCreator(self, context):
786        """Get a pdf creator suitable for `context`.
787        The default implementation always returns the default creator.
788        """
789        return getUtility(IPDFCreator)
790
[16254]791    def _mergeFiles(self, mergefiles, watermark, pdf_stream):
[16253]792        merger = PdfFileMerger()
793        merger.append(StringIO(pdf_stream))
794        for file in mergefiles:
795            if watermark:
796                # Pass through all pages of each file
797                # and merge with watermark page. Paint
798                # watermark first to make it transparent.
799                marked_file = PdfFileWriter()
800                orig_file = PdfFileReader(file)
801                num_pages = orig_file.getNumPages()
802                for num in range(num_pages):
803                    watermark_file = PdfFileReader(watermark)
804                    page = watermark_file.getPage(0)
805                    page.mergePage(orig_file.getPage(num))
806                    marked_file.addPage(page)
807                # Save into a file-like object
808                tmp1 = StringIO()
809                marked_file.write(tmp1)
810                # Append the file-like object
811                merger.append(tmp1)
812            else:
813                # Just append the file object
814                merger.append(file)
815        # Save into a file-like object
816        tmp2 = StringIO()
817        merger.write(tmp2)
818        return tmp2.getvalue()
819
[8257]820    def renderPDF(self, view, filename='slip.pdf', student=None,
[9906]821                  studentview=None,
[10439]822                  tableheader=[], tabledata=[],
[9555]823                  note=None, signatures=None, sigs_in_footer=(),
[10250]824                  show_scans=True, topMargin=1.5,
[16253]825                  omit_fields=(),
[16291]826                  mergefiles=None, watermark=None,
827                  pagebreak=False):
[14151]828        """Render pdf slips for various pages (also some pages
829        in the applicants module).
[7841]830        """
[10261]831        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9916]832        # XXX: tell what the different parameters mean
[8112]833        style = getSampleStyleSheet()
[9949]834        creator = self.getPDFCreator(student)
[8112]835        data = []
836        doc_title = view.label
837        author = '%s (%s)' % (view.request.principal.title,
838                              view.request.principal.id)
[9913]839        footer_text = view.label.split('\n')
[16126]840        if len(footer_text) > 2:
[13304]841            # We can add a department in first line, second line is used
[9913]842            footer_text = footer_text[1]
843        else:
[9917]844            # Only the first line is used for the footer
[9913]845            footer_text = footer_text[0]
[7714]846        if getattr(student, 'student_id', None) is not None:
[7310]847            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]848
[7318]849        # Insert student data table
[7310]850        if student is not None:
[15988]851            if view.form_fields:
852                bd_translation = trans(_('Base Data'), portal_language)
853                data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]854            data.append(render_student_data(
[11550]855                studentview, view.context, omit_fields, lang=portal_language,
856                slipname=filename))
[7304]857
[7318]858        # Insert widgets
[9191]859        if view.form_fields:
[9910]860            data.append(Paragraph(view.title, HEADING_STYLE))
[9191]861            separators = getattr(self, 'SEPARATORS_DICT', {})
862            table = creator.getWidgetsTable(
863                view.form_fields, view.context, None, lang=portal_language,
864                separators=separators)
865            data.append(table)
[7318]866
[8112]867        # Insert scanned docs
[9550]868        if show_scans:
869            data.extend(docs_as_flowables(view, portal_language))
[7318]870
[9452]871        # Insert history
[15337]872        if filename == 'clearance_slip.pdf':
[9452]873            hist_translation = trans(_('Workflow History'), portal_language)
[9910]874            data.append(Paragraph(hist_translation, HEADING_STYLE))
[9452]875            data.extend(creator.fromStringList(student.history.messages))
876
[10438]877        # Insert content tables (optionally on second page)
[10439]878        if hasattr(view, 'tabletitle'):
879            for i in range(len(view.tabletitle)):
880                if tabledata[i] and tableheader[i]:
[15984]881                    tabletitle = view.tabletitle[i]
882                    if tabletitle.startswith('_PB_'):
883                        data.append(PageBreak())
884                        tabletitle = view.tabletitle[i].strip('_PB_')
[10439]885                    #data.append(Spacer(1, 20))
[15984]886                    data.append(Paragraph(tabletitle, HEADING_STYLE))
[10439]887                    data.append(Spacer(1, 8))
888                    contenttable = render_table_data(tableheader[i],tabledata[i])
889                    data.append(contenttable)
[7318]890
[9010]891        # Insert signatures
[9965]892        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
893        # do not have a test for the following lines.
[9555]894        if signatures and not sigs_in_footer:
[16291]895            # Insert page break if necessary, else some space
896            if pagebreak:
897                data.append(PageBreak())
898            else:
899                data.append(Spacer(1, 20))
[9966]900            # Render one signature table per signature to
901            # get date and signature in line.
902            for signature in signatures:
903                signaturetables = get_signature_tables(signature)
904                data.append(signaturetables[0])
[9010]905
[7150]906        view.response.setHeader(
907            'Content-Type', 'application/pdf')
[8112]908        try:
909            pdf_stream = creator.create_pdf(
[8257]910                data, None, doc_title, author=author, footer=footer_text,
[9948]911                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
[8112]912        except IOError:
913            view.flash('Error in image file.')
914            return view.redirect(view.url(view.context))
[14256]915        except LayoutError, err:
916            view.flash(
917                'PDF file could not be created. Reportlab error message: %s'
918                % escape(err.message),
919                type="danger")
920            return view.redirect(view.url(view.context))
[16253]921        if mergefiles:
[16254]922            return self._mergeFiles(mergefiles, watermark, pdf_stream)
[8112]923        return pdf_stream
[7620]924
[15879]925    def _admissionText(self, student, portal_language):
926        inst_name = grok.getSite()['configuration'].name
927        text = trans(_(
928            'This is to inform you that you have been provisionally'
929            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
930            portal_language)
931        return text
[10576]932
[15879]933    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
[15880]934                                 pre_text=None, post_text=None,
[15889]935                                 topMargin = 1.5,
[16157]936                                 letterhead_path=None,
937                                 mergefiles=None, watermark=None):
[15879]938        """Render pdf admission letter.
[14461]939        """
[15879]940        if student is None:
941            return
942        style = getSampleStyleSheet()
[15880]943        if letterhead_path:
944            creator = getUtility(IPDFCreator, name='letter')
945        else:
[15888]946            creator = self.getPDFCreator(student)
[15879]947        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
948        data = []
949        doc_title = view.label
950        author = '%s (%s)' % (view.request.principal.title,
951                              view.request.principal.id)
952        footer_text = view.label.split('\n')
[16126]953        if len(footer_text) > 2:
[15879]954            # We can add a department in first line
955            footer_text = footer_text[1]
956        else:
957            # Only the first line is used for the footer
958            footer_text = footer_text[0]
959        if getattr(student, 'student_id', None) is not None:
960            footer_text = "%s - %s - " % (student.student_id, footer_text)
[10445]961
[15879]962        # Text before student data
963        if pre_text is None:
964            html = format_html(self._admissionText(student, portal_language))
965        else:
966            html = format_html(pre_text)
967        if html:
968            data.append(Paragraph(html, NOTE_STYLE))
969            data.append(Spacer(1, 20))
[14157]970
[15879]971        # Student data
972        data.append(render_student_data(view, student,
973                    omit_fields, lang=portal_language,
974                    slipname='admission_slip.pdf'))
[15163]975
[15879]976        # Text after student data
977        data.append(Spacer(1, 20))
978        if post_text is None:
979            datelist = student.history.messages[0].split()[0].split('-')
980            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
981            post_text = trans(_(
982                'Your Kofa student record was created on ${a}.',
983                mapping = {'a': creation_date}),
984                portal_language)
985        #html = format_html(post_text)
986        #data.append(Paragraph(html, NOTE_STYLE))
987
988        # Create pdf stream
989        view.response.setHeader(
990            'Content-Type', 'application/pdf')
991        pdf_stream = creator.create_pdf(
992            data, None, doc_title, author=author, footer=footer_text,
[15889]993            note=post_text, topMargin=topMargin,
994            letterhead_path=letterhead_path)
[16157]995        if mergefiles:
[16254]996            return self._mergeFiles(mergefiles, watermark, pdf_stream)
[15879]997        return pdf_stream
998
[10250]999    def renderPDFTranscript(self, view, filename='transcript.pdf',
1000                  student=None,
1001                  studentview=None,
[15163]1002                  note=None,
1003                  signatures=(),
1004                  sigs_in_footer=(),
1005                  digital_sigs=(),
[10250]1006                  show_scans=True, topMargin=1.5,
1007                  omit_fields=(),
[14292]1008                  tableheader=None,
[15163]1009                  no_passport=False,
1010                  save_file=False):
[14583]1011        """Render pdf slip of a transcripts.
[10250]1012        """
[10261]1013        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[10250]1014        # XXX: tell what the different parameters mean
1015        style = getSampleStyleSheet()
1016        creator = self.getPDFCreator(student)
1017        data = []
1018        doc_title = view.label
1019        author = '%s (%s)' % (view.request.principal.title,
1020                              view.request.principal.id)
1021        footer_text = view.label.split('\n')
1022        if len(footer_text) > 2:
1023            # We can add a department in first line
1024            footer_text = footer_text[1]
1025        else:
1026            # Only the first line is used for the footer
1027            footer_text = footer_text[0]
1028        if getattr(student, 'student_id', None) is not None:
1029            footer_text = "%s - %s - " % (student.student_id, footer_text)
1030
1031        # Insert student data table
1032        if student is not None:
1033            #bd_translation = trans(_('Base Data'), portal_language)
1034            #data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]1035            data.append(render_student_data(
[11550]1036                studentview, view.context,
1037                omit_fields, lang=portal_language,
[14292]1038                slipname=filename,
1039                no_passport=no_passport))
[10250]1040
1041        transcript_data = view.context.getTranscriptData()
1042        levels_data = transcript_data[0]
1043
1044        contextdata = []
[10261]1045        f_label = trans(_('Course of Study:'), portal_language)
[10250]1046        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10650]1047        f_text = formatted_text(view.context.certificate.longtitle)
[10250]1048        f_text = Paragraph(f_text, ENTRY1_STYLE)
1049        contextdata.append([f_label,f_text])
1050
[10261]1051        f_label = trans(_('Faculty:'), portal_language)
[10250]1052        f_label = Paragraph(f_label, ENTRY1_STYLE)
1053        f_text = formatted_text(
[10650]1054            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
[10250]1055        f_text = Paragraph(f_text, ENTRY1_STYLE)
1056        contextdata.append([f_label,f_text])
1057
[10261]1058        f_label = trans(_('Department:'), portal_language)
[10250]1059        f_label = Paragraph(f_label, ENTRY1_STYLE)
1060        f_text = formatted_text(
[10650]1061            view.context.certificate.__parent__.__parent__.longtitle)
[10250]1062        f_text = Paragraph(f_text, ENTRY1_STYLE)
1063        contextdata.append([f_label,f_text])
1064
[10261]1065        f_label = trans(_('Entry Session:'), portal_language)
[10250]1066        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]1067        f_text = formatted_text(
1068            view.session_dict.get(view.context.entry_session))
[10250]1069        f_text = Paragraph(f_text, ENTRY1_STYLE)
1070        contextdata.append([f_label,f_text])
1071
[10261]1072        f_label = trans(_('Entry Mode:'), portal_language)
[10250]1073        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]1074        f_text = formatted_text(view.studymode_dict.get(
1075            view.context.entry_mode))
[10250]1076        f_text = Paragraph(f_text, ENTRY1_STYLE)
1077        contextdata.append([f_label,f_text])
1078
[10262]1079        f_label = trans(_('Cumulative GPA:'), portal_language)
[10250]1080        f_label = Paragraph(f_label, ENTRY1_STYLE)
[14473]1081        format_float = getUtility(IKofaUtils).format_float
1082        cgpa = format_float(transcript_data[1], 3)
[15881]1083        if student.state == GRADUATED:
1084            f_text = formatted_text('%s (%s)' % (
1085                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
1086        else:
1087            f_text = formatted_text('%s' % cgpa)
[10250]1088        f_text = Paragraph(f_text, ENTRY1_STYLE)
1089        contextdata.append([f_label,f_text])
1090
1091        contexttable = Table(contextdata,style=SLIP_STYLE)
1092        data.append(contexttable)
1093
1094        transcripttables = render_transcript_data(
[10261]1095            view, tableheader, levels_data, lang=portal_language)
[10250]1096        data.extend(transcripttables)
1097
1098        # Insert signatures
1099        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
1100        # do not have a test for the following lines.
1101        if signatures and not sigs_in_footer:
1102            data.append(Spacer(1, 20))
1103            # Render one signature table per signature to
1104            # get date and signature in line.
1105            for signature in signatures:
1106                signaturetables = get_signature_tables(signature)
1107                data.append(signaturetables[0])
1108
[15163]1109        # Insert digital signatures
1110        if digital_sigs:
1111            data.append(Spacer(1, 20))
1112            sigs = digital_sigs.split('\n')
1113            for sig in sigs:
1114                data.append(Paragraph(sig, NOTE_STYLE))
1115
[10250]1116        view.response.setHeader(
1117            'Content-Type', 'application/pdf')
1118        try:
1119            pdf_stream = creator.create_pdf(
1120                data, None, doc_title, author=author, footer=footer_text,
[16966]1121                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin,
1122                view=view)
[10250]1123        except IOError:
[10261]1124            view.flash(_('Error in image file.'))
[10250]1125            return view.redirect(view.url(view.context))
[15163]1126        if save_file:
1127            self._saveTranscriptPDF(student, pdf_stream)
1128            return
[10250]1129        return pdf_stream
1130
[13898]1131    def renderPDFCourseticketsOverview(
[15423]1132            self, view, name, session, data, lecturers, orientation,
[15246]1133            title_length, note):
[14583]1134        """Render pdf slip of course tickets for a lecturer.
1135        """
[15423]1136        filename = '%s_%s_%s_%s.pdf' % (
1137            name, view.context.code, session, view.request.principal.id)
[15662]1138        try:
1139            session = academic_sessions_vocab.getTerm(session).title
1140        except LookupError:
1141            session = _('void')
[14702]1142        creator = getUtility(IPDFCreator, name=orientation)
[13898]1143        style = getSampleStyleSheet()
[15197]1144        pdf_data = []
1145        pdf_data += [Paragraph(
[14151]1146            translate(_('<b>Lecturer(s): ${a}</b>',
1147                      mapping = {'a':lecturers})), style["Normal"]),]
1148        pdf_data += [Paragraph(
1149            translate(_('<b>Credits: ${a}</b>',
1150                      mapping = {'a':view.context.credits})), style["Normal"]),]
[14314]1151        # Not used in base package.
1152        if data[1]:
1153            pdf_data += [Paragraph(
[14709]1154                translate(_('<b>${a}</b>',
[14314]1155                    mapping = {'a':data[1][0]})), style["Normal"]),]
1156            pdf_data += [Paragraph(
[14709]1157                translate(_('<b>${a}</b>',
[14708]1158                    mapping = {'a':data[1][1]})), style["Normal"]),]
1159            pdf_data += [Paragraph(
1160                translate(_('<b>Total Students: ${a}</b>',
1161                    mapping = {'a':data[1][2]})), style["Normal"]),]
1162            pdf_data += [Paragraph(
[14319]1163                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
[14708]1164                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
[14319]1165            pdf_data += [Paragraph(
1166                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
[14708]1167                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
[15821]1168            grade_stats = []
[15823]1169            for item in sorted(data[1][7].items()):
[15821]1170                grade_stats.append(('%s=%s' % (item[0], item[1])))
1171            grade_stats_string = ', '.join(grade_stats)
1172            pdf_data += [Paragraph(
1173                translate(_('<b>${a}</b>',
1174                mapping = {'a':grade_stats_string})), style["Normal"]),]
[13899]1175        pdf_data.append(Spacer(1, 20))
[15246]1176        colWidths = [None] * len(data[0][0])
[15528]1177        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE,
1178                           repeatRows=1)]
[15234]1179        # Process title if too long
1180        title = " ".join(view.context.title.split())
1181        ct = textwrap.fill(title, title_length)
[15526]1182        ft = "" # title
1183        #if len(textwrap.wrap(title, title_length)) > 1:
1184        #    ft = textwrap.wrap(title, title_length)[0] + ' ...'
[15197]1185        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1186            mapping = {'a':ct,
[14151]1187                       'b':view.context.code,
1188                       'd':session}))
[15423]1189        if name == 'attendance':
[15618]1190            doc_title += '\n' + translate(_('Attendance Sheet'))
[15423]1191        if name == 'coursetickets':
[15618]1192            doc_title += '\n' + translate(_('Course Tickets Overview'))
[15526]1193        #footer_title = translate(_('${a} (${b}) - ${d}',
1194        #    mapping = {'a':ft,
1195        #               'b':view.context.code,
1196        #               'd':session}))
1197        footer_title = translate(_('${b} - ${d}',
1198            mapping = {'b':view.context.code,
[14705]1199                       'd':session}))
[13898]1200        author = '%s (%s)' % (view.request.principal.title,
1201                              view.request.principal.id)
1202        view.response.setHeader(
1203            'Content-Type', 'application/pdf')
1204        view.response.setHeader(
1205            'Content-Disposition:', 'attachment; filename="%s' % filename)
1206        pdf_stream = creator.create_pdf(
[15246]1207            pdf_data, None, doc_title, author, footer_title + ' -', note
[13898]1208            )
1209        return pdf_stream
1210
[15998]1211    def updateCourseTickets(self, course):
1212        """Udate course tickets if course attributes were changed.
1213        """
1214        current_academic_session = grok.getSite()[
1215            'configuration'].current_academic_session
1216        if not current_academic_session:
1217            return
1218        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1219        coursetickets = cat.searchResults(
1220            code=(course.code, course.code),
1221            session=(current_academic_session,current_academic_session))
1222        number = 0
1223        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1224        for ticket in coursetickets:
1225            if ticket.credits == course.credits:
1226                continue
1227            if ticket.student.current_session != current_academic_session:
1228                continue
1229            if ticket.student.state not in (PAID,):
1230                continue
1231            number += 1
1232            ticket.student.__parent__.logger.info(
1233                '%s - %s %s/%s credits updated (%s->%s)' % (
1234                    ob_class, ticket.student.student_id,
1235                    ticket.level, ticket.code, course.credits,
1236                    ticket.credits))
1237            ticket.credits = course.credits
1238        return number
1239
[13132]1240    #: A dictionary which maps widget names to headlines. The headline
1241    #: is rendered in forms and on pdf slips above the respective
1242    #: display or input widget. There are no separating headlines
1243    #: in the base package.
[13129]1244    SEPARATORS_DICT = {}
[8410]1245
[13132]1246    #: A tuple containing names of file upload viewlets which are not shown
1247    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1248    #: in the base package. This attribute makes only sense, if intermediate
1249    #: custom packages are being used, like we do for all Nigerian portals.
[10021]1250    SKIP_UPLOAD_VIEWLETS = ()
1251
[13132]1252    #: A tuple containing the names of registration states in which changing of
1253    #: passport pictures is allowed.
[13129]1254    PORTRAIT_CHANGE_STATES = (ADMITTED,)
[10706]1255
[16609]1256    #: A tuple containing the names of registration states in which changing of
1257    #: scanned signatures is allowed.
1258    SIGNATURE_CHANGE_STATES = ()
1259
[12104]1260    #: A tuple containing all exporter names referring to students or
1261    #: subobjects thereof.
[15920]1262    STUDENT_EXPORTER_NAMES = (
1263            'students',
1264            'studentstudycourses',
[16847]1265            'studentstudycourses_1',
[16827]1266            #'studentstudycourses_2',
[15920]1267            'studentstudylevels',
[16827]1268            #'studentstudylevels_1',
1269            #'studentstudylevels_2',
[15920]1270            'coursetickets',
[16827]1271            #'coursetickets_1',
1272            #'coursetickets_2',
[15920]1273            'studentpayments',
1274            'bedtickets',
[15966]1275            'trimmed',
[15920]1276            'outstandingcourses',
1277            'unpaidpayments',
1278            'sfpaymentsoverview',
1279            'sessionpaymentsoverview',
1280            'studylevelsoverview',
1281            'combocard',
1282            'bursary',
1283            'accommodationpayments',
[15979]1284            'transcriptdata',
[16034]1285            'trimmedpayments',
[15979]1286            )
[12104]1287
[12971]1288    #: A tuple containing all exporter names needed for backing
1289    #: up student data
[16827]1290    STUDENT_BACKUP_EXPORTER_NAMES = (
1291            'students',
1292            'studentstudycourses',
1293            'studentstudycourses_1',
1294            'studentstudycourses_2',
1295            'studentstudylevels',
1296            'studentstudylevels_1',
1297            'studentstudylevels_2',
1298            'coursetickets',
1299            'coursetickets_1',
1300            'coursetickets_2',
1301            'studentpayments',
1302            'bedtickets')
[12971]1303
[15652]1304    # Maximum size of upload files in kB
1305    MAX_KB = 250
1306
[8410]1307    #: A prefix used when generating new student ids. Each student id will
[13129]1308    #: start with this string. The default is 'K' for Kofa.
[8410]1309    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.