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

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

Ease skipping file upload viewlets in custom packages.

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