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

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

Show entry_session on all slips.

  • Property svn:keywords set to Id
File size: 24.3 KB
RevLine 
[7191]1## $Id: utils.py 9762 2012-12-03 07:31:43Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
[7358]18"""General helper functions and utilities for the student section.
[6651]19"""
[7150]20import grok
[8595]21from time import time
[7318]22from reportlab.lib import colors
[7019]23from reportlab.lib.units import cm
24from reportlab.lib.pagesizes import A4
[9015]25from reportlab.lib.styles import getSampleStyleSheet
26from reportlab.platypus import Paragraph, Image, Table, Spacer
27from zope.component import getUtility, createObject
[7019]28from zope.formlib.form import setUpEditWidgets
[9015]29from zope.i18n import translate
[8596]30from waeup.kofa.interfaces import (
[9762]31    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
32    academic_sessions_vocab)
[7811]33from waeup.kofa.interfaces import MessageFactory as _
34from waeup.kofa.students.interfaces import IStudentsUtils
[6651]35
[7318]36SLIP_STYLE = [
37    ('VALIGN',(0,0),(-1,-1),'TOP'),
38    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
39    ]
[7019]40
[7318]41CONTENT_STYLE = [
42    ('VALIGN',(0,0),(-1,-1),'TOP'),
43    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
44    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
45    ('BACKGROUND',(0,0),(-1,0),colors.black),
46    ]
[7304]47
[7318]48FONT_SIZE = 10
49FONT_COLOR = 'black'
50
[7319]51def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
[7318]52    tag1 ='<font color=%s size=%d>' % (color, size)
[7319]53    return tag1 + '%s:</font>'
54
[8112]55def trans(text, lang):
56    # shortcut
57    return translate(text, 'waeup.kofa', target_language=lang)
58
[7511]59def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
60    """Turn `text`, `color` and `size` into an HTML snippet.
[7318]61
[7511]62    The snippet is suitable for use with reportlab and generating PDFs.
63    Wraps the `text` into a ``<font>`` tag with passed attributes.
64
65    Also non-strings are converted. Raw strings are expected to be
66    utf-8 encoded (usually the case for widgets etc.).
67
[7804]68    Finally, a br tag is added if widgets contain div tags
69    which are not supported by reportlab.
70
[7511]71    The returned snippet is unicode type.
72    """
[8142]73    try:
74        # In unit tests IKofaUtils has not been registered
75        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
76    except:
77        portal_language = 'en'
[7511]78    if not isinstance(text, unicode):
79        if isinstance(text, basestring):
80            text = text.decode('utf-8')
81        else:
82            text = unicode(text)
[9717]83    if text == 'None':
84        text = ''
[8141]85    # Mainly for boolean values we need our customized
86    # localisation of the zope domain
87    text = translate(text, 'zope', target_language=portal_language)
[7804]88    text = text.replace('</div>', '<br /></div>')
[7511]89    tag1 = u'<font color="%s" size="%d">' % (color, size)
90    return tag1 + u'%s</font>' % text
91
[8481]92def generate_student_id():
[8410]93    students = grok.getSite()['students']
94    new_id = students.unique_student_id
95    return new_id
[6742]96
[7186]97def set_up_widgets(view, ignore_request=False):
[7019]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
[7310]105def render_student_data(studentview):
[7318]106    """Render student table for an existing frame.
107    """
108    width, height = A4
[7186]109    set_up_widgets(studentview, ignore_request=True)
[7318]110    data_left = []
111    data_right = []
[7019]112    style = getSampleStyleSheet()
[7280]113    img = getUtility(IExtFileStore).getFileByContext(
114        studentview.context, attr='passport.jpg')
115    if img is None:
[7811]116        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
[7280]117        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7318]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)])
[7819]121    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9141]122
123    f_label = formatted_label(size=12) % _('Name')
124    f_label = Paragraph(f_label, style["Normal"])
125    f_text = formatted_text(studentview.context.display_fullname, size=12)
126    f_text = Paragraph(f_text, style["Normal"])
127    data_right.append([f_label,f_text])
128
[7019]129    for widget in studentview.widgets:
[9141]130        if 'name' in widget.name:
[7019]131            continue
[7714]132        f_label = formatted_label(size=12) % translate(
[7811]133            widget.label.strip(), 'waeup.kofa',
[7714]134            target_language=portal_language)
[7019]135        f_label = Paragraph(f_label, style["Normal"])
[7714]136        f_text = formatted_text(widget(), size=12)
[7019]137        f_text = Paragraph(f_text, style["Normal"])
[7318]138        data_right.append([f_label,f_text])
[9141]139
[9452]140    if getattr(studentview.context, 'certcode', None):
[9141]141        f_label = formatted_label(size=12) % _('Study Course')
142        f_label = Paragraph(f_label, style["Normal"])
[9191]143        f_text = formatted_text(
144            studentview.context['studycourse'].certificate.longtitle(), size=12)
[9141]145        f_text = Paragraph(f_text, style["Normal"])
146        data_right.append([f_label,f_text])
147
[9191]148        f_label = formatted_label(size=12) % _('Department')
149        f_label = Paragraph(f_label, style["Normal"])
150        f_text = formatted_text(
151            studentview.context[
152            'studycourse'].certificate.__parent__.__parent__.longtitle(),
153            size=12)
154        f_text = Paragraph(f_text, style["Normal"])
155        data_right.append([f_label,f_text])
156
157        f_label = formatted_label(size=12) % _('Faculty')
158        f_label = Paragraph(f_label, style["Normal"])
159        f_text = formatted_text(
160            studentview.context[
161            'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle(),
162            size=12)
163        f_text = Paragraph(f_text, style["Normal"])
164        data_right.append([f_label,f_text])
165
[9762]166        f_label = formatted_label(size=12) % _('Entry Session')
167        f_label = Paragraph(f_label, style["Normal"])
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, size=12)
171        f_text = Paragraph(f_text, style["Normal"])
172        data_right.append([f_label,f_text])
173
[7318]174    table_left = Table(data_left,style=SLIP_STYLE)
[8112]175    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
[7318]176    table = Table([[table_left, table_right],],style=SLIP_STYLE)
[7019]177    return table
178
[7304]179def render_table_data(tableheader,tabledata):
[7318]180    """Render children table for an existing frame.
181    """
[7304]182    data = []
[7318]183    #data.append([Spacer(1, 12)])
[7304]184    line = []
185    style = getSampleStyleSheet()
186    for element in tableheader:
[7511]187        field = formatted_text(element[0], color='white')
[7310]188        field = Paragraph(field, style["Normal"])
[7304]189        line.append(field)
190    data.append(line)
191    for ticket in tabledata:
192        line = []
193        for element in tableheader:
[7511]194              field = formatted_text(getattr(ticket,element[1],u' '))
[7318]195              field = Paragraph(field, style["Normal"])
[7304]196              line.append(field)
197        data.append(line)
[7310]198    table = Table(data,colWidths=[
[7318]199        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
[7304]200    return table
201
[9014]202def get_signature_table(signatures, lang='en'):
203    """Return a reportlab table containing signature fields (with date).
204    """
[9010]205    style = getSampleStyleSheet()
[9014]206    space_width = 0.4  # width in cm of space between signatures
207    table_width = 16.0 # supposed width of signature table in cms
208    # width of signature cells in cm...
209    sig_col_width = table_width - ((len(signatures) - 1) * space_width)
210    sig_col_width = sig_col_width / len(signatures)
[9010]211    data = []
[9014]212    col_widths = [] # widths of columns
213
214    sig_style = [
215        ('VALIGN',(0,-1),(-1,-1),'TOP'),
216        ('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
217        ('BOTTOMPADDING', (0,0), (-1,0), 36),
218        ('TOPPADDING', (0,-1), (-1,-1), 0),
219        ]
220    for num, elem in enumerate(signatures):
221        # draw a line above each signature cell (not: empty cells in between)
222        sig_style.append(
223            ('LINEABOVE', (num*2,-1), (num*2, -1), 1, colors.black))
224
225    row = []
[9010]226    for signature in signatures:
[9014]227        row.append(trans(_('Date:'), lang))
228        row.append('')
229        if len(signatures) > 1:
230            col_widths.extend([sig_col_width*cm, space_width*cm])
231        else:
232            col_widths.extend([sig_col_width/2*cm, sig_col_width/2*cm])
233            row.append('') # empty spaceholder on right
234    data.append(row[:-1])
235    data.extend(([''],)*3) # insert 3 empty rows...
236    row = []
[9010]237    for signature in signatures:
[9014]238        row.append(Paragraph(trans(signature, lang), style["Normal"]))
239        row.append('')
240    data.append(row[:-1])
241    table = Table(data, style=sig_style, repeatRows=len(data),
242                  colWidths=col_widths)
[9010]243    return table
244
[8112]245def docs_as_flowables(view, lang='en'):
246    """Create reportlab flowables out of scanned docs.
247    """
248    # XXX: fix circular import problem
249    from waeup.kofa.students.viewlets import FileManager
250    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
251    from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
252    style = getSampleStyleSheet()
253    data = []
[7318]254
[8112]255    # Collect viewlets
256    fm = FileManager(view.context, view.request, view)
257    fm.update()
258    if fm.viewlets:
259        sc_translation = trans(_('Scanned Documents'), lang)
260        data.append(Paragraph(sc_translation, style["Heading3"]))
261        # Insert list of scanned documents
262        table_data = []
263        for viewlet in fm.viewlets:
264            f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
265            img_path = getattr(getUtility(IExtFileStore).getFileByContext(
266                view.context, attr=viewlet.download_name), 'name', None)
[8120]267            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
[8112]268            if img_path is None:
269                pass
[9016]270            elif not img_path[-4:] in ('.jpg', '.JPG'):
[8112]271                # reportlab requires jpg images, I think.
[9016]272                f_text = Paragraph('%s (not displayable)' % (
[8112]273                    viewlet.title,), ENTRY1_STYLE)
274            else:
[8117]275                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
[8112]276            table_data.append([f_label, f_text])
277        if table_data:
278            # safety belt; empty tables lead to problems.
279            data.append(Table(table_data, style=SLIP_STYLE))
280    return data
281
[7150]282class StudentsUtils(grok.GlobalUtility):
283    """A collection of methods subject to customization.
284    """
285    grok.implements(IStudentsUtils)
[7019]286
[8268]287    def getReturningData(self, student):
[9005]288        """ Define what happens after school fee payment
[7841]289        depending on the student's senate verdict.
290
291        In the base configuration current level is always increased
292        by 100 no matter which verdict has been assigned.
293        """
[8268]294        new_level = student['studycourse'].current_level + 100
295        new_session = student['studycourse'].current_session + 1
296        return new_session, new_level
297
298    def setReturningData(self, student):
[9005]299        """ Define what happens after school fee payment
300        depending on the student's senate verdict.
[8268]301
[9005]302        This method folllows the same algorithm as getReturningData but
303        it also sets the new values.
[8268]304        """
305        new_session, new_level = self.getReturningData(student)
306        student['studycourse'].current_level = new_level
307        student['studycourse'].current_session = new_session
[7615]308        verdict = student['studycourse'].current_verdict
[8820]309        student['studycourse'].current_verdict = '0'
[7615]310        student['studycourse'].previous_verdict = verdict
311        return
312
[9519]313    def _getSessionConfiguration(self, session):
314        try:
315            return grok.getSite()['configuration'][str(session)]
316        except KeyError:
317            return None
318
[9148]319    def setPaymentDetails(self, category, student,
[9151]320            previous_session, previous_level):
[8595]321        """Create Payment object and set the payment data of a student for
322        the payment category specified.
323
[7841]324        """
[8595]325        p_item = u''
326        amount = 0.0
[9148]327        if previous_session:
[9517]328            if previous_session < student['studycourse'].entry_session:
329                return _('The previous session must not fall below '
330                         'your entry session.'), None
331            if category == 'schoolfee':
332                # School fee is always paid for the following session
333                if previous_session > student['studycourse'].current_session:
334                    return _('This is not a previous session.'), None
335            else:
336                if previous_session > student['studycourse'].current_session - 1:
337                    return _('This is not a previous session.'), None
[9148]338            p_session = previous_session
339            p_level = previous_level
340            p_current = False
341        else:
342            p_session = student['studycourse'].current_session
343            p_level = student['studycourse'].current_level
344            p_current = True
[9519]345        academic_session = self._getSessionConfiguration(p_session)
346        if academic_session == None:
[8595]347            return _(u'Session configuration object is not available.'), None
[9521]348        # Determine fee.
[7150]349        if category == 'schoolfee':
[8595]350            try:
[8596]351                certificate = student['studycourse'].certificate
352                p_item = certificate.code
[8595]353            except (AttributeError, TypeError):
354                return _('Study course data are incomplete.'), None
[9148]355            if previous_session:
[9517]356                # Students can pay for previous sessions in all workflow states.
357                # Fresh students are excluded by the update method of the
358                # PreviousPaymentAddFormPage.
[9148]359                if previous_level == 100:
360                    amount = getattr(certificate, 'school_fee_1', 0.0)
361                else:
362                    amount = getattr(certificate, 'school_fee_2', 0.0)
363            else:
364                if student.state == CLEARED:
365                    amount = getattr(certificate, 'school_fee_1', 0.0)
366                elif student.state == RETURNING:
367                    # In case of returning school fee payment the payment session
368                    # and level contain the values of the session the student
[9517]369                    # has paid for. Payment session is always next session.
[9148]370                    p_session, p_level = self.getReturningData(student)
[9519]371                    academic_session = self._getSessionConfiguration(p_session)
372                    if academic_session == None:
373                        return _(u'Session configuration object is not available.'), None
[9148]374                    amount = getattr(certificate, 'school_fee_2', 0.0)
375                elif student.is_postgrad and student.state == PAID:
376                    # Returning postgraduate students also pay for the next session
377                    # but their level always remains the same.
378                    p_session += 1
[9519]379                    academic_session = self._getSessionConfiguration(p_session)
380                    if academic_session == None:
381                        return _(u'Session configuration object is not available.'), None
[9148]382                    amount = getattr(certificate, 'school_fee_2', 0.0)
[7150]383        elif category == 'clearance':
[9178]384            try:
385                p_item = student['studycourse'].certificate.code
386            except (AttributeError, TypeError):
387                return _('Study course data are incomplete.'), None
[8595]388            amount = academic_session.clearance_fee
[7150]389        elif category == 'bed_allocation':
[8595]390            p_item = self.getAccommodationDetails(student)['bt']
391            amount = academic_session.booking_fee
[9423]392        elif category == 'hostel_maintenance':
393            amount = academic_session.maint_fee
[9429]394            bedticket = student['accommodation'].get(
395                str(student.current_session), None)
396            if bedticket:
397                p_item = bedticket.bed_coordinates
398            else:
399                # Should not happen because this is already checked
400                # in the browser module, but anyway ...
401                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
402                p_item = trans(_('no bed allocated'), portal_language)
[8595]403        if amount in (0.0, None):
[9517]404            return _('Amount could not be determined.'), None
[8595]405        for key in student['payments'].keys():
406            ticket = student['payments'][key]
407            if ticket.p_state == 'paid' and\
408               ticket.p_category == category and \
409               ticket.p_item == p_item and \
410               ticket.p_session == p_session:
[9517]411                  return _('This type of payment has already been made.'), None
[8708]412        payment = createObject(u'waeup.StudentOnlinePayment')
[8951]413        timestamp = ("%d" % int(time()*10000))[1:]
[8595]414        payment.p_id = "p%s" % timestamp
415        payment.p_category = category
416        payment.p_item = p_item
417        payment.p_session = p_session
418        payment.p_level = p_level
[9148]419        payment.p_current = p_current
[8595]420        payment.amount_auth = amount
421        return None, payment
[7019]422
[7186]423    def getAccommodationDetails(self, student):
[9219]424        """Determine the accommodation data of a student.
[7841]425        """
[7150]426        d = {}
427        d['error'] = u''
[8685]428        hostels = grok.getSite()['hostels']
429        d['booking_session'] = hostels.accommodation_session
430        d['allowed_states'] = hostels.accommodation_states
[8688]431        d['startdate'] = hostels.startdate
432        d['enddate'] = hostels.enddate
433        d['expired'] = hostels.expired
[7150]434        # Determine bed type
435        studycourse = student['studycourse']
[7369]436        certificate = getattr(studycourse,'certificate',None)
[7150]437        entry_session = studycourse.entry_session
438        current_level = studycourse.current_level
[9187]439        if None in (entry_session, current_level, certificate):
440            return d
[7369]441        end_level = certificate.end_level
[9148]442        if current_level == 10:
443            bt = 'pr'
444        elif entry_session == grok.getSite()['hostels'].accommodation_session:
[7150]445            bt = 'fr'
446        elif current_level >= end_level:
447            bt = 'fi'
448        else:
449            bt = 're'
450        if student.sex == 'f':
451            sex = 'female'
452        else:
453            sex = 'male'
454        special_handling = 'regular'
455        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
456        return d
[7019]457
[7186]458    def selectBed(self, available_beds):
[7841]459        """Select a bed from a list of available beds.
460
461        In the base configuration we select the first bed found,
462        but can also randomize the selection if we like.
463        """
[7150]464        return available_beds[0]
465
[9191]466    def renderPDFAdmissionLetter(self, view, student=None):
467        """Render pdf admission letter.
468        """
469        # XXX: we have to fix the import problems here.
470        from waeup.kofa.browser.interfaces import IPDFCreator
471        from waeup.kofa.browser.pdf import format_html, NOTE_STYLE
472        if student is None:
473            return
474        style = getSampleStyleSheet()
475        creator = getUtility(IPDFCreator)
476        data = []
477        doc_title = view.label
478        author = '%s (%s)' % (view.request.principal.title,
479                              view.request.principal.id)
480        footer_text = view.label
481        if getattr(student, 'student_id', None) is not None:
482            footer_text = "%s - %s - " % (student.student_id, footer_text)
483
484        # Admission text
485        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
486        inst_name = grok.getSite()['configuration'].name
487        text = trans(_(
488            'This is to inform you that you have been provisionally'
489            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
490            portal_language)
491        html = format_html(text)
492        data.append(Paragraph(html, NOTE_STYLE))
493        data.append(Spacer(1, 20))
494
495        # Student data
496        data.append(render_student_data(view))
497
498        # Insert history
499        data.append(Spacer(1, 20))
500        datelist = student.history.messages[0].split()[0].split('-')
501        creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
502        text = trans(_(
503            'Your Kofa student record was created on ${a}.',
504            mapping = {'a': creation_date}),
505            portal_language)
506        html = format_html(text)
507        data.append(Paragraph(html, NOTE_STYLE))
508
509        # Create pdf stream
510        view.response.setHeader(
511            'Content-Type', 'application/pdf')
512        pdf_stream = creator.create_pdf(
513            data, None, doc_title, author=author, footer=footer_text,
514            note=None)
515        return pdf_stream
516
[8257]517    def renderPDF(self, view, filename='slip.pdf', student=None,
518                  studentview=None, tableheader=None, tabledata=None,
[9555]519                  note=None, signatures=None, sigs_in_footer=(),
[9550]520                  show_scans=True):
[7841]521        """Render pdf slips for various pages.
522        """
[8112]523        # XXX: we have to fix the import problems here.
524        from waeup.kofa.browser.interfaces import IPDFCreator
525        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
526        style = getSampleStyleSheet()
527        creator = getUtility(IPDFCreator)
528        data = []
529        doc_title = view.label
530        author = '%s (%s)' % (view.request.principal.title,
531                              view.request.principal.id)
[7310]532        footer_text = view.label
[7714]533        if getattr(student, 'student_id', None) is not None:
[7310]534            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]535
[7318]536        # Insert student data table
[7819]537        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7310]538        if student is not None:
[8112]539            bd_translation = trans(_('Base Data'), portal_language)
540            data.append(Paragraph(bd_translation, style["Heading3"]))
541            data.append(render_student_data(studentview))
[7304]542
[7318]543        # Insert widgets
[9191]544        if view.form_fields:
545            data.append(Paragraph(view.title, style["Heading3"]))
546            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
547            separators = getattr(self, 'SEPARATORS_DICT', {})
548            table = creator.getWidgetsTable(
549                view.form_fields, view.context, None, lang=portal_language,
550                separators=separators)
551            data.append(table)
[7318]552
[8112]553        # Insert scanned docs
[9550]554        if show_scans:
555            data.extend(docs_as_flowables(view, portal_language))
[7318]556
[9452]557        # Insert history
558        if filename.startswith('clearance') or filename.startswith('course'):
559            hist_translation = trans(_('Workflow History'), portal_language)
560            data.append(Paragraph(hist_translation, style["Heading3"]))
561            data.extend(creator.fromStringList(student.history.messages))
562
563       # Insert content table (optionally on second page)
[7318]564        if tabledata and tableheader:
[8141]565            #data.append(PageBreak())
566            data.append(Spacer(1, 20))
[8112]567            data.append(Paragraph(view.content_title, style["Heading3"]))
[7304]568            contenttable = render_table_data(tableheader,tabledata)
[8112]569            data.append(contenttable)
[7318]570
[9010]571        # Insert signatures
[9555]572        if signatures and not sigs_in_footer:
[9010]573            data.append(Spacer(1, 20))
[9014]574            signaturetable = get_signature_table(signatures)
[9010]575            data.append(signaturetable)
576
[7150]577        view.response.setHeader(
578            'Content-Type', 'application/pdf')
[8112]579        try:
580            pdf_stream = creator.create_pdf(
[8257]581                data, None, doc_title, author=author, footer=footer_text,
[9555]582                note=note, sigs_in_footer=sigs_in_footer)
[8112]583        except IOError:
584            view.flash('Error in image file.')
585            return view.redirect(view.url(view.context))
586        return pdf_stream
[7620]587
[9532]588    TOTAL_CREDITS = 58
589
590    def maxCreditsExceeded(self, studylevel, course):
591        if self.TOTAL_CREDITS and \
592            studylevel.total_credits + course.credits > self.TOTAL_CREDITS:
593            return self.TOTAL_CREDITS
594        return 0
595
[7841]596    VERDICTS_DICT = {
[8820]597        '0': _('(not yet)'),
[7841]598        'A': 'Successful student',
599        'B': 'Student with carryover courses',
600        'C': 'Student on probation',
601        }
[8099]602
603    SEPARATORS_DICT = {
604        }
[8410]605
606    #: A prefix used when generating new student ids. Each student id will
607    #: start with this string. The default is 'K' for ``Kofa``.
608    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.