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

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

Put logic of customization of display_item and display_bedcoordinates into global utility methods. This simplifies customization.

  • Property svn:keywords set to Id
File size: 26.5 KB
Line 
1## $Id: utils.py 9987 2013-02-24 11:15:47Z 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            f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
225            img_path = getattr(getUtility(IExtFileStore).getFileByContext(
226                view.context, attr=viewlet.download_name), 'name', None)
227            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
228            if img_path is None:
229                pass
230            elif not img_path[-4:] in ('.jpg', '.JPG'):
231                # reportlab requires jpg images, I think.
232                f_text = Paragraph('%s (not displayable)' % (
233                    viewlet.title,), ENTRY1_STYLE)
234            else:
235                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
236            table_data.append([f_label, f_text])
237        if table_data:
238            # safety belt; empty tables lead to problems.
239            data.append(Table(table_data, style=SLIP_STYLE))
240    return data
241
242class StudentsUtils(grok.GlobalUtility):
243    """A collection of methods subject to customization.
244    """
245    grok.implements(IStudentsUtils)
246
247    def getReturningData(self, student):
248        """ Define what happens after school fee payment
249        depending on the student's senate verdict.
250
251        In the base configuration current level is always increased
252        by 100 no matter which verdict has been assigned.
253        """
254        new_level = student['studycourse'].current_level + 100
255        new_session = student['studycourse'].current_session + 1
256        return new_session, new_level
257
258    def setReturningData(self, student):
259        """ Define what happens after school fee payment
260        depending on the student's senate verdict.
261
262        This method folllows the same algorithm as getReturningData but
263        it also sets the new values.
264        """
265        new_session, new_level = self.getReturningData(student)
266        try:
267            student['studycourse'].current_level = new_level
268        except ConstraintNotSatisfied:
269            # Do not change level if level exceeds the
270            # certificate's end_level.
271            pass
272        student['studycourse'].current_session = new_session
273        verdict = student['studycourse'].current_verdict
274        student['studycourse'].current_verdict = '0'
275        student['studycourse'].previous_verdict = verdict
276        return
277
278    def _getSessionConfiguration(self, session):
279        try:
280            return grok.getSite()['configuration'][str(session)]
281        except KeyError:
282            return None
283
284    def setPaymentDetails(self, category, student,
285            previous_session, previous_level):
286        """Create Payment object and set the payment data of a student for
287        the payment category specified.
288
289        """
290        p_item = u''
291        amount = 0.0
292        if previous_session:
293            if previous_session < student['studycourse'].entry_session:
294                return _('The previous session must not fall below '
295                         'your entry session.'), None
296            if category == 'schoolfee':
297                # School fee is always paid for the following session
298                if previous_session > student['studycourse'].current_session:
299                    return _('This is not a previous session.'), None
300            else:
301                if previous_session > student['studycourse'].current_session - 1:
302                    return _('This is not a previous session.'), None
303            p_session = previous_session
304            p_level = previous_level
305            p_current = False
306        else:
307            p_session = student['studycourse'].current_session
308            p_level = student['studycourse'].current_level
309            p_current = True
310        academic_session = self._getSessionConfiguration(p_session)
311        if academic_session == None:
312            return _(u'Session configuration object is not available.'), None
313        # Determine fee.
314        if category == 'schoolfee':
315            try:
316                certificate = student['studycourse'].certificate
317                p_item = certificate.code
318            except (AttributeError, TypeError):
319                return _('Study course data are incomplete.'), None
320            if previous_session:
321                # Students can pay for previous sessions in all
322                # workflow states.  Fresh students are excluded by the
323                # update method of the PreviousPaymentAddFormPage.
324                if previous_level == 100:
325                    amount = getattr(certificate, 'school_fee_1', 0.0)
326                else:
327                    amount = getattr(certificate, 'school_fee_2', 0.0)
328            else:
329                if student.state == CLEARED:
330                    amount = getattr(certificate, 'school_fee_1', 0.0)
331                elif student.state == RETURNING:
332                    # In case of returning school fee payment the
333                    # payment session and level contain the values of
334                    # the session the student has paid for. Payment
335                    # session is always next session.
336                    p_session, p_level = self.getReturningData(student)
337                    academic_session = self._getSessionConfiguration(p_session)
338                    if academic_session == None:
339                        return _(
340                            u'Session configuration object is not available.'
341                            ), None
342                    amount = getattr(certificate, 'school_fee_2', 0.0)
343                elif student.is_postgrad and student.state == PAID:
344                    # Returning postgraduate students also pay for the
345                    # next session but their level always remains the
346                    # same.
347                    p_session += 1
348                    academic_session = self._getSessionConfiguration(p_session)
349                    if academic_session == None:
350                        return _(
351                            u'Session configuration object is not available.'
352                            ), None
353                    amount = getattr(certificate, 'school_fee_2', 0.0)
354        elif category == 'clearance':
355            try:
356                p_item = student['studycourse'].certificate.code
357            except (AttributeError, TypeError):
358                return _('Study course data are incomplete.'), None
359            amount = academic_session.clearance_fee
360        elif category == 'bed_allocation':
361            p_item = self.getAccommodationDetails(student)['bt']
362            amount = academic_session.booking_fee
363        elif category == 'hostel_maintenance':
364            amount = academic_session.maint_fee
365            bedticket = student['accommodation'].get(
366                str(student.current_session), None)
367            if bedticket:
368                p_item = bedticket.bed_coordinates
369            else:
370                # Should not happen because this is already checked
371                # in the browser module, but anyway ...
372                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
373                p_item = trans(_('no bed allocated'), portal_language)
374        if amount in (0.0, None):
375            return _('Amount could not be determined.'), None
376        for key in student['payments'].keys():
377            ticket = student['payments'][key]
378            if ticket.p_state == 'paid' and\
379               ticket.p_category == category and \
380               ticket.p_item == p_item and \
381               ticket.p_session == p_session:
382                  return _('This type of payment has already been made.'), None
383        payment = createObject(u'waeup.StudentOnlinePayment')
384        timestamp = ("%d" % int(time()*10000))[1:]
385        payment.p_id = "p%s" % timestamp
386        payment.p_category = category
387        payment.p_item = p_item
388        payment.p_session = p_session
389        payment.p_level = p_level
390        payment.p_current = p_current
391        payment.amount_auth = amount
392        return None, payment
393
394    def setBalanceDetails(self, category, student,
395            balance_session, balance_level, balance_amount):
396        """Create Payment object and set the payment data of a student for.
397
398        """
399        p_item = u'Balance'
400        p_session = balance_session
401        p_level = balance_level
402        p_current = False
403        amount = balance_amount
404        academic_session = self._getSessionConfiguration(p_session)
405        if academic_session == None:
406            return _(u'Session configuration object is not available.'), None
407        if amount in (0.0, None) or amount < 0:
408            return _('Amount must be greater than 0.'), None
409        for key in student['payments'].keys():
410            ticket = student['payments'][key]
411            if ticket.p_state == 'paid' and\
412               ticket.p_category == 'balance' and \
413               ticket.p_item == p_item and \
414               ticket.p_session == p_session:
415                  return _('This type of payment has already been made.'), None
416        payment = createObject(u'waeup.StudentOnlinePayment')
417        timestamp = ("%d" % int(time()*10000))[1:]
418        payment.p_id = "p%s" % timestamp
419        payment.p_category = category
420        payment.p_item = p_item
421        payment.p_session = p_session
422        payment.p_level = p_level
423        payment.p_current = p_current
424        payment.amount_auth = amount
425        return None, payment
426
427    def getAccommodationDetails(self, student):
428        """Determine the accommodation data of a student.
429        """
430        d = {}
431        d['error'] = u''
432        hostels = grok.getSite()['hostels']
433        d['booking_session'] = hostels.accommodation_session
434        d['allowed_states'] = hostels.accommodation_states
435        d['startdate'] = hostels.startdate
436        d['enddate'] = hostels.enddate
437        d['expired'] = hostels.expired
438        # Determine bed type
439        studycourse = student['studycourse']
440        certificate = getattr(studycourse,'certificate',None)
441        entry_session = studycourse.entry_session
442        current_level = studycourse.current_level
443        if None in (entry_session, current_level, certificate):
444            return d
445        end_level = certificate.end_level
446        if current_level == 10:
447            bt = 'pr'
448        elif entry_session == grok.getSite()['hostels'].accommodation_session:
449            bt = 'fr'
450        elif current_level >= end_level:
451            bt = 'fi'
452        else:
453            bt = 're'
454        if student.sex == 'f':
455            sex = 'female'
456        else:
457            sex = 'male'
458        special_handling = 'regular'
459        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
460        return d
461
462    def selectBed(self, available_beds):
463        """Select a bed from a list of available beds.
464
465        In the base configuration we select the first bed found,
466        but can also randomize the selection if we like.
467        """
468        return available_beds[0]
469
470    def _admissionText(self, student, portal_language):
471        inst_name = grok.getSite()['configuration'].name
472        text = trans(_(
473            'This is to inform you that you have been provisionally'
474            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
475            portal_language)
476        return text
477
478    def renderPDFAdmissionLetter(self, view, student=None):
479        """Render pdf admission letter.
480        """
481        if student is None:
482            return
483        style = getSampleStyleSheet()
484        creator = self.getPDFCreator(student)
485        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
486        data = []
487        doc_title = view.label
488        author = '%s (%s)' % (view.request.principal.title,
489                              view.request.principal.id)
490        footer_text = view.label.split('\n')
491        if len(footer_text) > 1:
492            # We can add a department in first line
493            footer_text = footer_text[1]
494        else:
495            # Only the first line is used for the footer
496            footer_text = footer_text[0]
497        if getattr(student, 'student_id', None) is not None:
498            footer_text = "%s - %s - " % (student.student_id, footer_text)
499
500        # Admission text
501        html = format_html(self._admissionText(student, portal_language))
502        data.append(Paragraph(html, NOTE_STYLE))
503        data.append(Spacer(1, 20))
504
505        # Student data
506        data.append(render_student_data(view))
507
508        # Insert history
509        data.append(Spacer(1, 20))
510        datelist = student.history.messages[0].split()[0].split('-')
511        creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
512        text = trans(_(
513            'Your Kofa student record was created on ${a}.',
514            mapping = {'a': creation_date}),
515            portal_language)
516        html = format_html(text)
517        data.append(Paragraph(html, NOTE_STYLE))
518
519        # Create pdf stream
520        view.response.setHeader(
521            'Content-Type', 'application/pdf')
522        pdf_stream = creator.create_pdf(
523            data, None, doc_title, author=author, footer=footer_text,
524            note=None)
525        return pdf_stream
526
527    def getPDFCreator(self, context):
528        """Get a pdf creator suitable for `context`.
529
530        The default implementation always returns the default creator.
531        """
532        return getUtility(IPDFCreator)
533
534    def renderPDF(self, view, filename='slip.pdf', student=None,
535                  studentview=None,
536                  tableheader_1=None, tabledata_1=None,
537                  tableheader_2=None, tabledata_2=None,
538                  tableheader_3=None, tabledata_3=None,
539                  note=None, signatures=None, sigs_in_footer=(),
540                  show_scans=True, topMargin=1.5):
541        """Render pdf slips for various pages.
542        """
543        # XXX: tell what the different parameters mean
544        style = getSampleStyleSheet()
545        creator = self.getPDFCreator(student)
546        data = []
547        doc_title = view.label
548        author = '%s (%s)' % (view.request.principal.title,
549                              view.request.principal.id)
550        footer_text = view.label.split('\n')
551        if len(footer_text) > 2:
552            # We can add a department in first line
553            footer_text = footer_text[1]
554        else:
555            # Only the first line is used for the footer
556            footer_text = footer_text[0]
557        if getattr(student, 'student_id', None) is not None:
558            footer_text = "%s - %s - " % (student.student_id, footer_text)
559
560        # Insert student data table
561        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
562        if student is not None:
563            bd_translation = trans(_('Base Data'), portal_language)
564            data.append(Paragraph(bd_translation, HEADING_STYLE))
565            data.append(render_student_data(studentview))
566
567        # Insert widgets
568        if view.form_fields:
569            data.append(Paragraph(view.title, HEADING_STYLE))
570            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
571            separators = getattr(self, 'SEPARATORS_DICT', {})
572            table = creator.getWidgetsTable(
573                view.form_fields, view.context, None, lang=portal_language,
574                separators=separators)
575            data.append(table)
576
577        # Insert scanned docs
578        if show_scans:
579            data.extend(docs_as_flowables(view, portal_language))
580
581        # Insert history
582        if filename.startswith('clearance'):
583            hist_translation = trans(_('Workflow History'), portal_language)
584            data.append(Paragraph(hist_translation, HEADING_STYLE))
585            data.extend(creator.fromStringList(student.history.messages))
586
587       # Insert 1st content table (optionally on second page)
588        if tabledata_1 and tableheader_1:
589            #data.append(PageBreak())
590            #data.append(Spacer(1, 20))
591            data.append(Paragraph(view.content_title_1, HEADING_STYLE))
592            data.append(Spacer(1, 8))
593            contenttable = render_table_data(tableheader_1,tabledata_1)
594            data.append(contenttable)
595
596       # Insert 2nd content table (optionally on second page)
597        if tabledata_2 and tableheader_2:
598            #data.append(PageBreak())
599            #data.append(Spacer(1, 20))
600            data.append(Paragraph(view.content_title_2, HEADING_STYLE))
601            data.append(Spacer(1, 8))
602            contenttable = render_table_data(tableheader_2,tabledata_2)
603            data.append(contenttable)
604
605       # Insert 3rd content table (optionally on second page)
606        if tabledata_3 and tableheader_3:
607            #data.append(PageBreak())
608            #data.append(Spacer(1, 20))
609            data.append(Paragraph(view.content_title_3, HEADING_STYLE))
610            data.append(Spacer(1, 8))
611            contenttable = render_table_data(tableheader_3,tabledata_3)
612            data.append(contenttable)
613
614        # Insert signatures
615        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
616        # do not have a test for the following lines.
617        if signatures and not sigs_in_footer:
618            data.append(Spacer(1, 20))
619            # Render one signature table per signature to
620            # get date and signature in line.
621            for signature in signatures:
622                signaturetables = get_signature_tables(signature)
623                data.append(signaturetables[0])
624
625        view.response.setHeader(
626            'Content-Type', 'application/pdf')
627        try:
628            pdf_stream = creator.create_pdf(
629                data, None, doc_title, author=author, footer=footer_text,
630                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
631        except IOError:
632            view.flash('Error in image file.')
633            return view.redirect(view.url(view.context))
634        return pdf_stream
635
636    def maxCredits(self, studylevel):
637        """Return maximum credits.
638
639        In some universities maximum credits is not constant, it
640        depends on the student's study level.
641        """
642        return 50
643
644    def maxCreditsExceeded(self, studylevel, course):
645        max_credits = self.maxCredits(studylevel)
646        if max_credits and \
647            studylevel.total_credits + course.credits > max_credits:
648            return max_credits
649        return 0
650
651    def getBedCoordinates(self, bedticket):
652        """Return bed coordinates.
653
654        This method can be used to customize the display_coordinates
655        property method.
656        """
657        return bedticket.bed_coordinates
658
659    VERDICTS_DICT = {
660        '0': _('(not yet)'),
661        'A': 'Successful student',
662        'B': 'Student with carryover courses',
663        'C': 'Student on probation',
664        }
665
666    SEPARATORS_DICT = {
667        }
668
669    #: A prefix used when generating new student ids. Each student id will
670    #: start with this string. The default is 'K' for ``Kofa``.
671    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.