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

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

Do not change level if level exceeds the certificate's end_level.

  • Property svn:keywords set to Id
File size: 27.4 KB
Line 
1## $Id: utils.py 9922 2013-01-29 08:56:56Z 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 = (num - 1) * 2
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 = getUtility(IPDFCreator)
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
545        if getattr(student, 'student_id', None) is not None:
546            footer_text = "%s - %s - " % (student.student_id, footer_text)
547
548        # Admission text
549        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
550        inst_name = grok.getSite()['configuration'].name
551        text = trans(_(
552            'This is to inform you that you have been provisionally'
553            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
554            portal_language)
555        html = format_html(text)
556        data.append(Paragraph(html, NOTE_STYLE))
557        data.append(Spacer(1, 20))
558
559        # Student data
560        data.append(render_student_data(view))
561
562        # Insert history
563        data.append(Spacer(1, 20))
564        datelist = student.history.messages[0].split()[0].split('-')
565        creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
566        text = trans(_(
567            'Your Kofa student record was created on ${a}.',
568            mapping = {'a': creation_date}),
569            portal_language)
570        html = format_html(text)
571        data.append(Paragraph(html, NOTE_STYLE))
572
573        # Create pdf stream
574        view.response.setHeader(
575            'Content-Type', 'application/pdf')
576        pdf_stream = creator.create_pdf(
577            data, None, doc_title, author=author, footer=footer_text,
578            note=None)
579        return pdf_stream
580
581    def renderPDF(self, view, filename='slip.pdf', student=None,
582                  studentview=None,
583                  tableheader_1=None, tabledata_1=None,
584                  tableheader_2=None, tabledata_2=None,
585                  note=None, signatures=None, sigs_in_footer=(),
586                  show_scans=True, topMargin=1.5):
587        """Render pdf slips for various pages.
588        """
589        # XXX: tell what the different parameters mean
590        style = getSampleStyleSheet()
591        creator = getUtility(IPDFCreator)
592        data = []
593        doc_title = view.label
594        author = '%s (%s)' % (view.request.principal.title,
595                              view.request.principal.id)
596        footer_text = view.label.split('\n')
597        if len(footer_text) > 2:
598            # We can add a department in first line
599            footer_text = footer_text[1]
600        else:
601            # Only the first line is used for the footer
602            footer_text = footer_text[0]
603        if getattr(student, 'student_id', None) is not None:
604            footer_text = "%s - %s - " % (student.student_id, footer_text)
605
606        # Insert student data table
607        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
608        if student is not None:
609            bd_translation = trans(_('Base Data'), portal_language)
610            data.append(Paragraph(bd_translation, HEADING_STYLE))
611            data.append(render_student_data(studentview))
612
613        # Insert widgets
614        if view.form_fields:
615            data.append(Paragraph(view.title, HEADING_STYLE))
616            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
617            separators = getattr(self, 'SEPARATORS_DICT', {})
618            table = creator.getWidgetsTable(
619                view.form_fields, view.context, None, lang=portal_language,
620                separators=separators)
621            data.append(table)
622
623        # Insert scanned docs
624        if show_scans:
625            data.extend(docs_as_flowables(view, portal_language))
626
627        # Insert history
628        if filename.startswith('clearance'):
629            hist_translation = trans(_('Workflow History'), portal_language)
630            data.append(Paragraph(hist_translation, HEADING_STYLE))
631            data.extend(creator.fromStringList(student.history.messages))
632
633       # Insert 1st content table (optionally on second page)
634        if tabledata_1 and tableheader_1:
635            #data.append(PageBreak())
636            #data.append(Spacer(1, 20))
637            data.append(Paragraph(view.content_title_1, HEADING_STYLE))
638            data.append(Spacer(1, 8))
639            contenttable = render_table_data(tableheader_1,tabledata_1)
640            data.append(contenttable)
641
642       # Insert 2nd content table (optionally on second page)
643        if tabledata_2 and tableheader_2:
644            #data.append(PageBreak())
645            #data.append(Spacer(1, 20))
646            data.append(Paragraph(view.content_title_2, HEADING_STYLE))
647            data.append(Spacer(1, 8))
648            contenttable = render_table_data(tableheader_2,tabledata_2)
649            data.append(contenttable)
650
651        # Insert signatures
652        if signatures and not sigs_in_footer:
653            data.append(Spacer(1, 20))
654            signaturetable = get_signature_table(signatures)
655            data.append(signaturetable)
656
657        view.response.setHeader(
658            'Content-Type', 'application/pdf')
659        try:
660            pdf_stream = creator.create_pdf(
661                data, None, doc_title, author=author, footer=footer_text,
662                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
663        except IOError:
664            view.flash('Error in image file.')
665            return view.redirect(view.url(view.context))
666        return pdf_stream
667
668    def maxCredits(self, studylevel):
669        """Return maximum credits.
670
671        In some universities maximum credits is not constant, it
672        depends on the student's study level.
673        """
674        return 50
675
676    def maxCreditsExceeded(self, studylevel, course):
677        max_credits = self.maxCredits(studylevel)
678        if max_credits and \
679            studylevel.total_credits + course.credits > max_credits:
680            return max_credits
681        return 0
682
683    VERDICTS_DICT = {
684        '0': _('(not yet)'),
685        'A': 'Successful student',
686        'B': 'Student with carryover courses',
687        'C': 'Student on probation',
688        }
689
690    SEPARATORS_DICT = {
691        }
692
693    #: A prefix used when generating new student ids. Each student id will
694    #: start with this string. The default is 'K' for ``Kofa``.
695    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.