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

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

Condense all pdf slips. Re-use ENTRY1_STYLE in render_student_data.

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