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

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

Do not display 'None' in score (and any other) column.

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