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

Last change on this file since 9957 was 9957, checked in by Henrik Bettermann, 12 years ago

Show also level courses on slips.

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