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

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

Make renderPDFAdmissionLetter more flexible.

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