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

Last change on this file since 9918 was 9917, checked in by uli, 12 years ago

A way to overcrowd forms.

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