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

Last change on this file since 11587 was 11550, checked in by Henrik Bettermann, 11 years ago

Print QR codes on all student slips.

  • Property svn:keywords set to Id
File size: 36.1 KB
RevLine 
[7191]1## $Id: utils.py 11550 2014-03-28 08:54:30Z 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##
[7358]18"""General helper functions and utilities for the student section.
[6651]19"""
[7150]20import grok
[8595]21from time import time
[7318]22from reportlab.lib import colors
[7019]23from reportlab.lib.units import cm
24from reportlab.lib.pagesizes import A4
[9015]25from reportlab.lib.styles import getSampleStyleSheet
26from reportlab.platypus import Paragraph, Image, Table, Spacer
[9922]27from zope.schema.interfaces import ConstraintNotSatisfied
[9015]28from zope.component import getUtility, createObject
[7019]29from zope.formlib.form import setUpEditWidgets
[9015]30from zope.i18n import translate
[8596]31from waeup.kofa.interfaces import (
[9762]32    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
33    academic_sessions_vocab)
[7811]34from waeup.kofa.interfaces import MessageFactory as _
35from waeup.kofa.students.interfaces import IStudentsUtils
[10706]36from waeup.kofa.students.workflow import ADMITTED
[11535]37from waeup.kofa.students.vocabularies import StudyLevelSource
[9910]38from waeup.kofa.browser.pdf import (
[9965]39    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
[11550]40    get_signature_tables, get_qrcode)
[9910]41from waeup.kofa.browser.interfaces import IPDFCreator
[10256]42from waeup.kofa.utils.helpers import to_timezone
[6651]43
[7318]44SLIP_STYLE = [
45    ('VALIGN',(0,0),(-1,-1),'TOP'),
46    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
47    ]
[7019]48
[7318]49CONTENT_STYLE = [
50    ('VALIGN',(0,0),(-1,-1),'TOP'),
51    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
52    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
[9906]53    #('BACKGROUND',(0,0),(-1,0),colors.black),
54    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
55    ('BOX', (0,0), (-1,-1), 1, colors.black),
56
[7318]57    ]
[7304]58
[7318]59FONT_SIZE = 10
60FONT_COLOR = 'black'
61
[8112]62def trans(text, lang):
63    # shortcut
64    return translate(text, 'waeup.kofa', target_language=lang)
65
[10261]66def formatted_text(text, color=FONT_COLOR, lang='en'):
[7511]67    """Turn `text`, `color` and `size` into an HTML snippet.
[7318]68
[7511]69    The snippet is suitable for use with reportlab and generating PDFs.
70    Wraps the `text` into a ``<font>`` tag with passed attributes.
71
72    Also non-strings are converted. Raw strings are expected to be
73    utf-8 encoded (usually the case for widgets etc.).
74
[7804]75    Finally, a br tag is added if widgets contain div tags
76    which are not supported by reportlab.
77
[7511]78    The returned snippet is unicode type.
79    """
80    if not isinstance(text, unicode):
81        if isinstance(text, basestring):
82            text = text.decode('utf-8')
83        else:
84            text = unicode(text)
[9717]85    if text == 'None':
86        text = ''
[8141]87    # Mainly for boolean values we need our customized
88    # localisation of the zope domain
[10261]89    text = translate(text, 'zope', target_language=lang)
[7804]90    text = text.replace('</div>', '<br /></div>')
[9910]91    tag1 = u'<font color="%s">' % (color)
[7511]92    return tag1 + u'%s</font>' % text
93
[8481]94def generate_student_id():
[8410]95    students = grok.getSite()['students']
96    new_id = students.unique_student_id
97    return new_id
[6742]98
[7186]99def set_up_widgets(view, ignore_request=False):
[7019]100    view.adapters = {}
101    view.widgets = setUpEditWidgets(
102        view.form_fields, view.prefix, view.context, view.request,
103        adapters=view.adapters, for_display=True,
104        ignore_request=ignore_request
105        )
106
[11550]107def render_student_data(studentview, context, omit_fields=(),
108                        lang='en', slipname=None):
[7318]109    """Render student table for an existing frame.
110    """
111    width, height = A4
[7186]112    set_up_widgets(studentview, ignore_request=True)
[7318]113    data_left = []
[11550]114    data_middle = []
[7019]115    style = getSampleStyleSheet()
[7280]116    img = getUtility(IExtFileStore).getFileByContext(
117        studentview.context, attr='passport.jpg')
118    if img is None:
[7811]119        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
[7280]120        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7318]121    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
122    data_left.append([doc_img])
123    #data.append([Spacer(1, 12)])
[9141]124
[10261]125    f_label = trans(_('Name:'), lang)
[9910]126    f_label = Paragraph(f_label, ENTRY1_STYLE)
[9911]127    f_text = formatted_text(studentview.context.display_fullname)
[9910]128    f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]129    data_middle.append([f_label,f_text])
[9141]130
[7019]131    for widget in studentview.widgets:
[9141]132        if 'name' in widget.name:
[7019]133            continue
[9911]134        f_label = translate(
[7811]135            widget.label.strip(), 'waeup.kofa',
[10261]136            target_language=lang)
[9911]137        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
[10261]138        f_text = formatted_text(widget(), lang=lang)
[9910]139        f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]140        data_middle.append([f_label,f_text])
[9141]141
[9452]142    if getattr(studentview.context, 'certcode', None):
[10250]143        if not 'certificate' in omit_fields:
[10261]144            f_label = trans(_('Study Course:'), lang)
[10250]145            f_label = Paragraph(f_label, ENTRY1_STYLE)
146            f_text = formatted_text(
[10650]147                studentview.context['studycourse'].certificate.longtitle)
[10250]148            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]149            data_middle.append([f_label,f_text])
[10250]150        if not 'department' in omit_fields:
[10261]151            f_label = trans(_('Department:'), lang)
[10250]152            f_label = Paragraph(f_label, ENTRY1_STYLE)
153            f_text = formatted_text(
154                studentview.context[
[10650]155                'studycourse'].certificate.__parent__.__parent__.longtitle,
[10250]156                )
157            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]158            data_middle.append([f_label,f_text])
[10250]159        if not 'faculty' in omit_fields:
[10261]160            f_label = trans(_('Faculty:'), lang)
[10250]161            f_label = Paragraph(f_label, ENTRY1_STYLE)
162            f_text = formatted_text(
163                studentview.context[
[10650]164                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
[10250]165                )
166            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]167            data_middle.append([f_label,f_text])
[10688]168        if not 'current_mode' in omit_fields:
169            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
[11535]170            sm = studymodes_dict[studentview.context.current_mode]
[10688]171            f_label = trans(_('Study Mode:'), lang)
172            f_label = Paragraph(f_label, ENTRY1_STYLE)
173            f_text = formatted_text(sm)
174            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]175            data_middle.append([f_label,f_text])
[10250]176        if not 'entry_session' in omit_fields:
[10261]177            f_label = trans(_('Entry Session:'), lang)
[10250]178            f_label = Paragraph(f_label, ENTRY1_STYLE)
[11535]179            entry_session = studentview.context.entry_session
[10250]180            entry_session = academic_sessions_vocab.getTerm(entry_session).title
181            f_text = formatted_text(entry_session)
182            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]183            data_middle.append([f_label,f_text])
[11535]184        # Requested by Uniben, does not really make sense
185        if not 'current_level' in omit_fields:
186            f_label = trans(_('Current Level:'), lang)
187            f_label = Paragraph(f_label, ENTRY1_STYLE)
188            current_level = studentview.context['studycourse'].current_level
189            studylevelsource = StudyLevelSource().factory
190            current_level = studylevelsource.getTitle(
191                studentview.context, current_level)
192            f_text = formatted_text(current_level)
193            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]194            data_middle.append([f_label,f_text])
[10256]195        if not 'date_of_birth' in omit_fields:
[10261]196            f_label = trans(_('Date of Birth:'), lang)
[10256]197            f_label = Paragraph(f_label, ENTRY1_STYLE)
198            date_of_birth = studentview.context.date_of_birth
199            tz = getUtility(IKofaUtils).tzinfo
200            date_of_birth = to_timezone(date_of_birth, tz)
201            if date_of_birth is not None:
202                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
203            f_text = formatted_text(date_of_birth)
204            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]205            data_middle.append([f_label,f_text])
[9141]206
[11550]207    # append QR code to the right
208    if slipname:
209        url = studentview.url(context, slipname)
210        data_right = [[get_qrcode(url, width=70.0)]]
211        table_right = Table(data_right,style=SLIP_STYLE)
212    else:
213        table_right = None
214
[7318]215    table_left = Table(data_left,style=SLIP_STYLE)
[11550]216    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
217    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
[7019]218    return table
219
[10261]220def render_table_data(tableheader, tabledata, lang='en'):
[7318]221    """Render children table for an existing frame.
222    """
[7304]223    data = []
[7318]224    #data.append([Spacer(1, 12)])
[7304]225    line = []
226    style = getSampleStyleSheet()
227    for element in tableheader:
[10261]228        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
[7310]229        field = Paragraph(field, style["Normal"])
[7304]230        line.append(field)
231    data.append(line)
232    for ticket in tabledata:
233        line = []
234        for element in tableheader:
[7511]235              field = formatted_text(getattr(ticket,element[1],u' '))
[7318]236              field = Paragraph(field, style["Normal"])
[7304]237              line.append(field)
238        data.append(line)
[7310]239    table = Table(data,colWidths=[
[7318]240        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
[7304]241    return table
242
[10261]243def render_transcript_data(view, tableheader, levels_data, lang='en'):
[10250]244    """Render children table for an existing frame.
245    """
246    data = []
247    style = getSampleStyleSheet()
248    for level in levels_data:
249        level_obj = level['level']
[10251]250        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
251        headerline = []
252        tabledata = []
[10261]253        subheader = '%s %s, %s %s' % (
254            trans(_('Session'), lang),
[10250]255            view.session_dict[level_obj.level_session],
[10261]256            trans(_('Level'), lang),
[10266]257            view.level_dict[level_obj.level])
[10250]258        data.append(Paragraph(subheader, HEADING_STYLE))
259        for element in tableheader:
260            field = '<strong>%s</strong>' % formatted_text(element[0])
261            field = Paragraph(field, style["Normal"])
[10251]262            headerline.append(field)
263        tabledata.append(headerline)
[10250]264        for ticket in tickets:
[10251]265            ticketline = []
[10250]266            for element in tableheader:
267                  field = formatted_text(getattr(ticket,element[1],u' '))
268                  field = Paragraph(field, style["Normal"])
[10251]269                  ticketline.append(field)
270            tabledata.append(ticketline)
[10250]271        table = Table(tabledata,colWidths=[
272            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
273        data.append(table)
[10479]274        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
[10261]275        data.append(Paragraph(sgpa, style["Normal"]))
[10250]276    return data
277
[8112]278def docs_as_flowables(view, lang='en'):
279    """Create reportlab flowables out of scanned docs.
280    """
281    # XXX: fix circular import problem
282    from waeup.kofa.students.viewlets import FileManager
283    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
284    style = getSampleStyleSheet()
285    data = []
[7318]286
[8112]287    # Collect viewlets
288    fm = FileManager(view.context, view.request, view)
289    fm.update()
290    if fm.viewlets:
291        sc_translation = trans(_('Scanned Documents'), lang)
[9910]292        data.append(Paragraph(sc_translation, HEADING_STYLE))
[8112]293        # Insert list of scanned documents
294        table_data = []
295        for viewlet in fm.viewlets:
[10020]296            if viewlet.file_exists:
297                # Show viewlet only if file exists
298                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
299                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
300                    view.context, attr=viewlet.download_name), 'name', None)
301                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
302                if img_path is None:
303                    pass
304                elif not img_path[-4:] in ('.jpg', '.JPG'):
305                    # reportlab requires jpg images, I think.
306                    f_text = Paragraph('%s (not displayable)' % (
307                        viewlet.title,), ENTRY1_STYLE)
308                else:
309                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
310                table_data.append([f_label, f_text])
[8112]311        if table_data:
312            # safety belt; empty tables lead to problems.
313            data.append(Table(table_data, style=SLIP_STYLE))
314    return data
315
[7150]316class StudentsUtils(grok.GlobalUtility):
317    """A collection of methods subject to customization.
318    """
319    grok.implements(IStudentsUtils)
[7019]320
[8268]321    def getReturningData(self, student):
[9005]322        """ Define what happens after school fee payment
[7841]323        depending on the student's senate verdict.
324
325        In the base configuration current level is always increased
326        by 100 no matter which verdict has been assigned.
327        """
[8268]328        new_level = student['studycourse'].current_level + 100
329        new_session = student['studycourse'].current_session + 1
330        return new_session, new_level
331
332    def setReturningData(self, student):
[9005]333        """ Define what happens after school fee payment
334        depending on the student's senate verdict.
[8268]335
[9005]336        This method folllows the same algorithm as getReturningData but
337        it also sets the new values.
[8268]338        """
339        new_session, new_level = self.getReturningData(student)
[9922]340        try:
341            student['studycourse'].current_level = new_level
342        except ConstraintNotSatisfied:
343            # Do not change level if level exceeds the
344            # certificate's end_level.
345            pass
[8268]346        student['studycourse'].current_session = new_session
[7615]347        verdict = student['studycourse'].current_verdict
[8820]348        student['studycourse'].current_verdict = '0'
[7615]349        student['studycourse'].previous_verdict = verdict
350        return
351
[9519]352    def _getSessionConfiguration(self, session):
353        try:
354            return grok.getSite()['configuration'][str(session)]
355        except KeyError:
356            return None
357
[11451]358    def _isPaymentDisabled(self, p_session, category, student):
359        academic_session = self._getSessionConfiguration(p_session)
[11452]360        if category == 'schoolfee' and \
361            'sf_all' in academic_session.payment_disabled:
[11451]362            return True
363        return False
364
[9148]365    def setPaymentDetails(self, category, student,
[9151]366            previous_session, previous_level):
[8595]367        """Create Payment object and set the payment data of a student for
368        the payment category specified.
369
[7841]370        """
[8595]371        p_item = u''
372        amount = 0.0
[9148]373        if previous_session:
[9517]374            if previous_session < student['studycourse'].entry_session:
375                return _('The previous session must not fall below '
376                         'your entry session.'), None
377            if category == 'schoolfee':
378                # School fee is always paid for the following session
379                if previous_session > student['studycourse'].current_session:
380                    return _('This is not a previous session.'), None
381            else:
382                if previous_session > student['studycourse'].current_session - 1:
383                    return _('This is not a previous session.'), None
[9148]384            p_session = previous_session
385            p_level = previous_level
386            p_current = False
387        else:
388            p_session = student['studycourse'].current_session
389            p_level = student['studycourse'].current_level
390            p_current = True
[9519]391        academic_session = self._getSessionConfiguration(p_session)
392        if academic_session == None:
[8595]393            return _(u'Session configuration object is not available.'), None
[9521]394        # Determine fee.
[7150]395        if category == 'schoolfee':
[8595]396            try:
[8596]397                certificate = student['studycourse'].certificate
398                p_item = certificate.code
[8595]399            except (AttributeError, TypeError):
400                return _('Study course data are incomplete.'), None
[9148]401            if previous_session:
[9916]402                # Students can pay for previous sessions in all
403                # workflow states.  Fresh students are excluded by the
404                # update method of the PreviousPaymentAddFormPage.
[9148]405                if previous_level == 100:
406                    amount = getattr(certificate, 'school_fee_1', 0.0)
407                else:
408                    amount = getattr(certificate, 'school_fee_2', 0.0)
409            else:
410                if student.state == CLEARED:
411                    amount = getattr(certificate, 'school_fee_1', 0.0)
412                elif student.state == RETURNING:
[9916]413                    # In case of returning school fee payment the
414                    # payment session and level contain the values of
415                    # the session the student has paid for. Payment
416                    # session is always next session.
[9148]417                    p_session, p_level = self.getReturningData(student)
[9519]418                    academic_session = self._getSessionConfiguration(p_session)
419                    if academic_session == None:
[9916]420                        return _(
421                            u'Session configuration object is not available.'
422                            ), None
[9148]423                    amount = getattr(certificate, 'school_fee_2', 0.0)
424                elif student.is_postgrad and student.state == PAID:
[9916]425                    # Returning postgraduate students also pay for the
426                    # next session but their level always remains the
427                    # same.
[9148]428                    p_session += 1
[9519]429                    academic_session = self._getSessionConfiguration(p_session)
430                    if academic_session == None:
[9916]431                        return _(
432                            u'Session configuration object is not available.'
433                            ), None
[9148]434                    amount = getattr(certificate, 'school_fee_2', 0.0)
[7150]435        elif category == 'clearance':
[9178]436            try:
437                p_item = student['studycourse'].certificate.code
438            except (AttributeError, TypeError):
439                return _('Study course data are incomplete.'), None
[8595]440            amount = academic_session.clearance_fee
[7150]441        elif category == 'bed_allocation':
[8595]442            p_item = self.getAccommodationDetails(student)['bt']
443            amount = academic_session.booking_fee
[9423]444        elif category == 'hostel_maintenance':
[10681]445            amount = 0.0
[9429]446            bedticket = student['accommodation'].get(
447                str(student.current_session), None)
448            if bedticket:
449                p_item = bedticket.bed_coordinates
[10681]450                if bedticket.bed.__parent__.maint_fee > 0:
451                    amount = bedticket.bed.__parent__.maint_fee
452                else:
453                    # fallback
454                    amount = academic_session.maint_fee
[9429]455            else:
456                # Should not happen because this is already checked
457                # in the browser module, but anyway ...
458                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
459                p_item = trans(_('no bed allocated'), portal_language)
[10449]460        elif category == 'transcript':
461            amount = academic_session.transcript_fee
[8595]462        if amount in (0.0, None):
[9517]463            return _('Amount could not be determined.'), None
[8595]464        for key in student['payments'].keys():
465            ticket = student['payments'][key]
466            if ticket.p_state == 'paid' and\
467               ticket.p_category == category and \
468               ticket.p_item == p_item and \
469               ticket.p_session == p_session:
[9517]470                  return _('This type of payment has already been made.'), None
[11451]471        if self._isPaymentDisabled(p_session, category, student):
472            return _('Payment temporarily disabled.'), None
[8708]473        payment = createObject(u'waeup.StudentOnlinePayment')
[8951]474        timestamp = ("%d" % int(time()*10000))[1:]
[8595]475        payment.p_id = "p%s" % timestamp
476        payment.p_category = category
477        payment.p_item = p_item
478        payment.p_session = p_session
479        payment.p_level = p_level
[9148]480        payment.p_current = p_current
[8595]481        payment.amount_auth = amount
482        return None, payment
[7019]483
[9868]484    def setBalanceDetails(self, category, student,
[9864]485            balance_session, balance_level, balance_amount):
486        """Create Payment object and set the payment data of a student for.
487
488        """
[9868]489        p_item = u'Balance'
[9864]490        p_session = balance_session
491        p_level = balance_level
492        p_current = False
493        amount = balance_amount
494        academic_session = self._getSessionConfiguration(p_session)
495        if academic_session == None:
496            return _(u'Session configuration object is not available.'), None
[9874]497        if amount in (0.0, None) or amount < 0:
498            return _('Amount must be greater than 0.'), None
[9864]499        for key in student['payments'].keys():
500            ticket = student['payments'][key]
501            if ticket.p_state == 'paid' and\
502               ticket.p_category == 'balance' and \
503               ticket.p_item == p_item and \
504               ticket.p_session == p_session:
505                  return _('This type of payment has already been made.'), None
506        payment = createObject(u'waeup.StudentOnlinePayment')
507        timestamp = ("%d" % int(time()*10000))[1:]
508        payment.p_id = "p%s" % timestamp
[9868]509        payment.p_category = category
[9864]510        payment.p_item = p_item
511        payment.p_session = p_session
512        payment.p_level = p_level
513        payment.p_current = p_current
514        payment.amount_auth = amount
515        return None, payment
516
[7186]517    def getAccommodationDetails(self, student):
[9219]518        """Determine the accommodation data of a student.
[7841]519        """
[7150]520        d = {}
521        d['error'] = u''
[8685]522        hostels = grok.getSite()['hostels']
523        d['booking_session'] = hostels.accommodation_session
524        d['allowed_states'] = hostels.accommodation_states
[8688]525        d['startdate'] = hostels.startdate
526        d['enddate'] = hostels.enddate
527        d['expired'] = hostels.expired
[7150]528        # Determine bed type
529        studycourse = student['studycourse']
[7369]530        certificate = getattr(studycourse,'certificate',None)
[7150]531        entry_session = studycourse.entry_session
532        current_level = studycourse.current_level
[9187]533        if None in (entry_session, current_level, certificate):
534            return d
[7369]535        end_level = certificate.end_level
[9148]536        if current_level == 10:
537            bt = 'pr'
538        elif entry_session == grok.getSite()['hostels'].accommodation_session:
[7150]539            bt = 'fr'
540        elif current_level >= end_level:
541            bt = 'fi'
542        else:
543            bt = 're'
544        if student.sex == 'f':
545            sex = 'female'
546        else:
547            sex = 'male'
548        special_handling = 'regular'
549        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
550        return d
[7019]551
[7186]552    def selectBed(self, available_beds):
[7841]553        """Select a bed from a list of available beds.
554
555        In the base configuration we select the first bed found,
556        but can also randomize the selection if we like.
557        """
[7150]558        return available_beds[0]
559
[9981]560    def _admissionText(self, student, portal_language):
[9979]561        inst_name = grok.getSite()['configuration'].name
562        text = trans(_(
563            'This is to inform you that you have been provisionally'
564            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
565            portal_language)
566        return text
567
[10686]568    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
569                                 pre_text=None, post_text=None,):
[9191]570        """Render pdf admission letter.
571        """
572        if student is None:
573            return
574        style = getSampleStyleSheet()
[9949]575        creator = self.getPDFCreator(student)
[9979]576        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9191]577        data = []
578        doc_title = view.label
579        author = '%s (%s)' % (view.request.principal.title,
580                              view.request.principal.id)
[9944]581        footer_text = view.label.split('\n')
582        if len(footer_text) > 1:
583            # We can add a department in first line
584            footer_text = footer_text[1]
585        else:
586            # Only the first line is used for the footer
587            footer_text = footer_text[0]
[9191]588        if getattr(student, 'student_id', None) is not None:
589            footer_text = "%s - %s - " % (student.student_id, footer_text)
590
[10702]591        # Text before student data
[10686]592        if pre_text is None:
593            html = format_html(self._admissionText(student, portal_language))
594        else:
595            html = format_html(pre_text)
[9191]596        data.append(Paragraph(html, NOTE_STYLE))
597        data.append(Spacer(1, 20))
598
599        # Student data
[11550]600        data.append(render_student_data(view, student,
601                    omit_fields, lang=portal_language,
602                    slipname='admission_slip.pdf'))
[9191]603
[10702]604        # Text after student data
[9191]605        data.append(Spacer(1, 20))
[10686]606        if post_text is None:
607            datelist = student.history.messages[0].split()[0].split('-')
608            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
[10702]609            post_text = trans(_(
[10686]610                'Your Kofa student record was created on ${a}.',
611                mapping = {'a': creation_date}),
612                portal_language)
[10702]613        #html = format_html(post_text)
614        #data.append(Paragraph(html, NOTE_STYLE))
[9191]615
616        # Create pdf stream
617        view.response.setHeader(
618            'Content-Type', 'application/pdf')
619        pdf_stream = creator.create_pdf(
620            data, None, doc_title, author=author, footer=footer_text,
[10702]621            note=post_text)
[9191]622        return pdf_stream
623
[9949]624    def getPDFCreator(self, context):
625        """Get a pdf creator suitable for `context`.
626
627        The default implementation always returns the default creator.
628        """
629        return getUtility(IPDFCreator)
630
[8257]631    def renderPDF(self, view, filename='slip.pdf', student=None,
[9906]632                  studentview=None,
[10439]633                  tableheader=[], tabledata=[],
[9555]634                  note=None, signatures=None, sigs_in_footer=(),
[10250]635                  show_scans=True, topMargin=1.5,
636                  omit_fields=()):
[7841]637        """Render pdf slips for various pages.
638        """
[10261]639        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9916]640        # XXX: tell what the different parameters mean
[8112]641        style = getSampleStyleSheet()
[9949]642        creator = self.getPDFCreator(student)
[8112]643        data = []
644        doc_title = view.label
645        author = '%s (%s)' % (view.request.principal.title,
646                              view.request.principal.id)
[9913]647        footer_text = view.label.split('\n')
648        if len(footer_text) > 2:
649            # We can add a department in first line
650            footer_text = footer_text[1]
651        else:
[9917]652            # Only the first line is used for the footer
[9913]653            footer_text = footer_text[0]
[7714]654        if getattr(student, 'student_id', None) is not None:
[7310]655            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]656
[7318]657        # Insert student data table
[7310]658        if student is not None:
[8112]659            bd_translation = trans(_('Base Data'), portal_language)
[9910]660            data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]661            data.append(render_student_data(
[11550]662                studentview, view.context, omit_fields, lang=portal_language,
663                slipname=filename))
[7304]664
[7318]665        # Insert widgets
[9191]666        if view.form_fields:
[9910]667            data.append(Paragraph(view.title, HEADING_STYLE))
[9191]668            separators = getattr(self, 'SEPARATORS_DICT', {})
669            table = creator.getWidgetsTable(
670                view.form_fields, view.context, None, lang=portal_language,
671                separators=separators)
672            data.append(table)
[7318]673
[8112]674        # Insert scanned docs
[9550]675        if show_scans:
676            data.extend(docs_as_flowables(view, portal_language))
[7318]677
[9452]678        # Insert history
[9910]679        if filename.startswith('clearance'):
[9452]680            hist_translation = trans(_('Workflow History'), portal_language)
[9910]681            data.append(Paragraph(hist_translation, HEADING_STYLE))
[9452]682            data.extend(creator.fromStringList(student.history.messages))
683
[10438]684        # Insert content tables (optionally on second page)
[10439]685        if hasattr(view, 'tabletitle'):
686            for i in range(len(view.tabletitle)):
687                if tabledata[i] and tableheader[i]:
688                    #data.append(PageBreak())
689                    #data.append(Spacer(1, 20))
690                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
691                    data.append(Spacer(1, 8))
692                    contenttable = render_table_data(tableheader[i],tabledata[i])
693                    data.append(contenttable)
[7318]694
[9010]695        # Insert signatures
[9965]696        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
697        # do not have a test for the following lines.
[9555]698        if signatures and not sigs_in_footer:
[9010]699            data.append(Spacer(1, 20))
[9966]700            # Render one signature table per signature to
701            # get date and signature in line.
702            for signature in signatures:
703                signaturetables = get_signature_tables(signature)
704                data.append(signaturetables[0])
[9010]705
[7150]706        view.response.setHeader(
707            'Content-Type', 'application/pdf')
[8112]708        try:
709            pdf_stream = creator.create_pdf(
[8257]710                data, None, doc_title, author=author, footer=footer_text,
[9948]711                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
[8112]712        except IOError:
713            view.flash('Error in image file.')
714            return view.redirect(view.url(view.context))
715        return pdf_stream
[7620]716
[10578]717    gpa_boundaries = ((1, 'Fail'),
718                      (1.5, 'Pass'),
719                      (2.4, '3rd Class'),
720                      (3.5, '2nd Class Lower'),
721                      (4.5, '2nd Class Upper'),
722                      (5, '1st Class'))
[10576]723
[10445]724    def getClassFromCGPA(self, gpa):
[10578]725        if gpa < self.gpa_boundaries[0][0]:
726            return 0, self.gpa_boundaries[0][1]
727        if gpa < self.gpa_boundaries[1][0]:
728            return 1, self.gpa_boundaries[1][1]
729        if gpa < self.gpa_boundaries[2][0]:
730            return 2, self.gpa_boundaries[2][1]
731        if gpa < self.gpa_boundaries[3][0]:
732            return 3, self.gpa_boundaries[3][1]
733        if gpa < self.gpa_boundaries[4][0]:
734            return 4, self.gpa_boundaries[4][1]
735        if gpa <= self.gpa_boundaries[5][0]:
736            return 5, self.gpa_boundaries[5][1]
737        return 'N/A'
[10445]738
[10250]739    def renderPDFTranscript(self, view, filename='transcript.pdf',
740                  student=None,
741                  studentview=None,
742                  note=None, signatures=None, sigs_in_footer=(),
743                  show_scans=True, topMargin=1.5,
744                  omit_fields=(),
745                  tableheader=None):
746        """Render pdf slips for transcript.
747        """
[10261]748        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[10250]749        # XXX: tell what the different parameters mean
750        style = getSampleStyleSheet()
751        creator = self.getPDFCreator(student)
752        data = []
753        doc_title = view.label
754        author = '%s (%s)' % (view.request.principal.title,
755                              view.request.principal.id)
756        footer_text = view.label.split('\n')
757        if len(footer_text) > 2:
758            # We can add a department in first line
759            footer_text = footer_text[1]
760        else:
761            # Only the first line is used for the footer
762            footer_text = footer_text[0]
763        if getattr(student, 'student_id', None) is not None:
764            footer_text = "%s - %s - " % (student.student_id, footer_text)
765
766        # Insert student data table
767        if student is not None:
768            #bd_translation = trans(_('Base Data'), portal_language)
769            #data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]770            data.append(render_student_data(
[11550]771                studentview, view.context,
772                omit_fields, lang=portal_language,
773                slipname=filename))
[10250]774
775        transcript_data = view.context.getTranscriptData()
776        levels_data = transcript_data[0]
777        gpa = transcript_data[1]
778
779        contextdata = []
[10261]780        f_label = trans(_('Course of Study:'), portal_language)
[10250]781        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10650]782        f_text = formatted_text(view.context.certificate.longtitle)
[10250]783        f_text = Paragraph(f_text, ENTRY1_STYLE)
784        contextdata.append([f_label,f_text])
785
[10261]786        f_label = trans(_('Faculty:'), portal_language)
[10250]787        f_label = Paragraph(f_label, ENTRY1_STYLE)
788        f_text = formatted_text(
[10650]789            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
[10250]790        f_text = Paragraph(f_text, ENTRY1_STYLE)
791        contextdata.append([f_label,f_text])
792
[10261]793        f_label = trans(_('Department:'), portal_language)
[10250]794        f_label = Paragraph(f_label, ENTRY1_STYLE)
795        f_text = formatted_text(
[10650]796            view.context.certificate.__parent__.__parent__.longtitle)
[10250]797        f_text = Paragraph(f_text, ENTRY1_STYLE)
798        contextdata.append([f_label,f_text])
799
[10261]800        f_label = trans(_('Entry Session:'), portal_language)
[10250]801        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]802        f_text = formatted_text(
803            view.session_dict.get(view.context.entry_session))
[10250]804        f_text = Paragraph(f_text, ENTRY1_STYLE)
805        contextdata.append([f_label,f_text])
806
[10261]807        f_label = trans(_('Entry Mode:'), portal_language)
[10250]808        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]809        f_text = formatted_text(view.studymode_dict.get(
810            view.context.entry_mode))
[10250]811        f_text = Paragraph(f_text, ENTRY1_STYLE)
812        contextdata.append([f_label,f_text])
813
[10262]814        f_label = trans(_('Cumulative GPA:'), portal_language)
[10250]815        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10576]816        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
[10250]817        f_text = Paragraph(f_text, ENTRY1_STYLE)
818        contextdata.append([f_label,f_text])
819
820        contexttable = Table(contextdata,style=SLIP_STYLE)
821        data.append(contexttable)
822
823        transcripttables = render_transcript_data(
[10261]824            view, tableheader, levels_data, lang=portal_language)
[10250]825        data.extend(transcripttables)
826
827        # Insert signatures
828        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
829        # do not have a test for the following lines.
830        if signatures and not sigs_in_footer:
831            data.append(Spacer(1, 20))
832            # Render one signature table per signature to
833            # get date and signature in line.
834            for signature in signatures:
835                signaturetables = get_signature_tables(signature)
836                data.append(signaturetables[0])
837
838        view.response.setHeader(
839            'Content-Type', 'application/pdf')
840        try:
841            pdf_stream = creator.create_pdf(
842                data, None, doc_title, author=author, footer=footer_text,
843                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
844        except IOError:
[10261]845            view.flash(_('Error in image file.'))
[10250]846            return view.redirect(view.url(view.context))
847        return pdf_stream
848
[9830]849    def maxCredits(self, studylevel):
850        """Return maximum credits.
[9532]851
[9830]852        In some universities maximum credits is not constant, it
853        depends on the student's study level.
854        """
855        return 50
856
[9532]857    def maxCreditsExceeded(self, studylevel, course):
[9830]858        max_credits = self.maxCredits(studylevel)
859        if max_credits and \
860            studylevel.total_credits + course.credits > max_credits:
861            return max_credits
[9532]862        return 0
863
[9987]864    def getBedCoordinates(self, bedticket):
865        """Return bed coordinates.
866
867        This method can be used to customize the display_coordinates
868        property method.
869        """
870        return bedticket.bed_coordinates
871
[7841]872    VERDICTS_DICT = {
[8820]873        '0': _('(not yet)'),
[7841]874        'A': 'Successful student',
875        'B': 'Student with carryover courses',
876        'C': 'Student on probation',
877        }
[8099]878
879    SEPARATORS_DICT = {
880        }
[8410]881
[10021]882    SKIP_UPLOAD_VIEWLETS = ()
883
[10803]884    PWCHANGE_STATES = (ADMITTED,)
[10706]885
[8410]886    #: A prefix used when generating new student ids. Each student id will
887    #: start with this string. The default is 'K' for ``Kofa``.
888    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.