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

Last change on this file since 15235 was 15235, checked in by Henrik Bettermann, 6 years ago

ct is not a list.

  • Property svn:keywords set to Id
File size: 47.4 KB
RevLine 
[7191]1## $Id: utils.py 15235 2018-11-13 07:20:14Z 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##
[13076]18"""General helper functions and utilities for the students section.
[6651]19"""
[7150]20import grok
[15234]21import textwrap
[8595]22from time import time
[15163]23from cStringIO import StringIO
[7318]24from reportlab.lib import colors
[7019]25from reportlab.lib.units import cm
26from reportlab.lib.pagesizes import A4
[9015]27from reportlab.lib.styles import getSampleStyleSheet
28from reportlab.platypus import Paragraph, Image, Table, Spacer
[14256]29from reportlab.platypus.doctemplate import LayoutError
[11589]30from zope.event import notify
[9922]31from zope.schema.interfaces import ConstraintNotSatisfied
[9015]32from zope.component import getUtility, createObject
[7019]33from zope.formlib.form import setUpEditWidgets
[9015]34from zope.i18n import translate
[8596]35from waeup.kofa.interfaces import (
[9762]36    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
[15163]37    academic_sessions_vocab, IFileStoreNameChooser)
[7811]38from waeup.kofa.interfaces import MessageFactory as _
39from waeup.kofa.students.interfaces import IStudentsUtils
[10706]40from waeup.kofa.students.workflow import ADMITTED
[11589]41from waeup.kofa.students.vocabularies import StudyLevelSource, MatNumNotInSource
[9910]42from waeup.kofa.browser.pdf import (
[9965]43    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
[11550]44    get_signature_tables, get_qrcode)
[9910]45from waeup.kofa.browser.interfaces import IPDFCreator
[10256]46from waeup.kofa.utils.helpers import to_timezone
[6651]47
[7318]48SLIP_STYLE = [
49    ('VALIGN',(0,0),(-1,-1),'TOP'),
50    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
51    ]
[7019]52
[7318]53CONTENT_STYLE = [
54    ('VALIGN',(0,0),(-1,-1),'TOP'),
55    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
56    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
[9906]57    #('BACKGROUND',(0,0),(-1,0),colors.black),
58    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
59    ('BOX', (0,0), (-1,-1), 1, colors.black),
[7318]60    ]
[7304]61
[7318]62FONT_SIZE = 10
63FONT_COLOR = 'black'
64
[8112]65def trans(text, lang):
66    # shortcut
67    return translate(text, 'waeup.kofa', target_language=lang)
68
[10261]69def formatted_text(text, color=FONT_COLOR, lang='en'):
[7511]70    """Turn `text`, `color` and `size` into an HTML snippet.
[7318]71
[7511]72    The snippet is suitable for use with reportlab and generating PDFs.
73    Wraps the `text` into a ``<font>`` tag with passed attributes.
74
75    Also non-strings are converted. Raw strings are expected to be
76    utf-8 encoded (usually the case for widgets etc.).
77
[7804]78    Finally, a br tag is added if widgets contain div tags
79    which are not supported by reportlab.
80
[7511]81    The returned snippet is unicode type.
82    """
83    if not isinstance(text, unicode):
84        if isinstance(text, basestring):
85            text = text.decode('utf-8')
86        else:
87            text = unicode(text)
[9717]88    if text == 'None':
89        text = ''
[13665]90    # Very long matriculation numbers need to be wrapped
91    if text.find(' ') == -1 and len(text.split('/')) > 6:
92        text = '/'.join(text.split('/')[:5]) + \
93            '/ ' + '/'.join(text.split('/')[5:])
[8141]94    # Mainly for boolean values we need our customized
95    # localisation of the zope domain
[10261]96    text = translate(text, 'zope', target_language=lang)
[7804]97    text = text.replace('</div>', '<br /></div>')
[9910]98    tag1 = u'<font color="%s">' % (color)
[7511]99    return tag1 + u'%s</font>' % text
100
[8481]101def generate_student_id():
[8410]102    students = grok.getSite()['students']
103    new_id = students.unique_student_id
104    return new_id
[6742]105
[7186]106def set_up_widgets(view, ignore_request=False):
[7019]107    view.adapters = {}
108    view.widgets = setUpEditWidgets(
109        view.form_fields, view.prefix, view.context, view.request,
110        adapters=view.adapters, for_display=True,
111        ignore_request=ignore_request
112        )
113
[11550]114def render_student_data(studentview, context, omit_fields=(),
[14292]115                        lang='en', slipname=None, no_passport=False):
[7318]116    """Render student table for an existing frame.
117    """
118    width, height = A4
[7186]119    set_up_widgets(studentview, ignore_request=True)
[7318]120    data_left = []
[11550]121    data_middle = []
[7019]122    style = getSampleStyleSheet()
[7280]123    img = getUtility(IExtFileStore).getFileByContext(
124        studentview.context, attr='passport.jpg')
125    if img is None:
[7811]126        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
[7280]127        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7318]128    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
129    data_left.append([doc_img])
130    #data.append([Spacer(1, 12)])
[9141]131
[10261]132    f_label = trans(_('Name:'), lang)
[9910]133    f_label = Paragraph(f_label, ENTRY1_STYLE)
[9911]134    f_text = formatted_text(studentview.context.display_fullname)
[9910]135    f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]136    data_middle.append([f_label,f_text])
[9141]137
[7019]138    for widget in studentview.widgets:
[9141]139        if 'name' in widget.name:
[7019]140            continue
[9911]141        f_label = translate(
[7811]142            widget.label.strip(), 'waeup.kofa',
[10261]143            target_language=lang)
[9911]144        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
[10261]145        f_text = formatted_text(widget(), lang=lang)
[9910]146        f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]147        data_middle.append([f_label,f_text])
[9141]148
[9452]149    if getattr(studentview.context, 'certcode', None):
[10250]150        if not 'certificate' in omit_fields:
[10261]151            f_label = trans(_('Study Course:'), lang)
[10250]152            f_label = Paragraph(f_label, ENTRY1_STYLE)
153            f_text = formatted_text(
[10650]154                studentview.context['studycourse'].certificate.longtitle)
[10250]155            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]156            data_middle.append([f_label,f_text])
[10250]157        if not 'department' in omit_fields:
[10261]158            f_label = trans(_('Department:'), lang)
[10250]159            f_label = Paragraph(f_label, ENTRY1_STYLE)
160            f_text = formatted_text(
161                studentview.context[
[10650]162                'studycourse'].certificate.__parent__.__parent__.longtitle,
[10250]163                )
164            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]165            data_middle.append([f_label,f_text])
[10250]166        if not 'faculty' in omit_fields:
[10261]167            f_label = trans(_('Faculty:'), lang)
[10250]168            f_label = Paragraph(f_label, ENTRY1_STYLE)
169            f_text = formatted_text(
170                studentview.context[
[10650]171                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
[10250]172                )
173            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]174            data_middle.append([f_label,f_text])
[10688]175        if not 'current_mode' in omit_fields:
176            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
[11535]177            sm = studymodes_dict[studentview.context.current_mode]
[10688]178            f_label = trans(_('Study Mode:'), lang)
179            f_label = Paragraph(f_label, ENTRY1_STYLE)
180            f_text = formatted_text(sm)
181            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]182            data_middle.append([f_label,f_text])
[10250]183        if not 'entry_session' in omit_fields:
[10261]184            f_label = trans(_('Entry Session:'), lang)
[10250]185            f_label = Paragraph(f_label, ENTRY1_STYLE)
[11535]186            entry_session = studentview.context.entry_session
[10250]187            entry_session = academic_sessions_vocab.getTerm(entry_session).title
188            f_text = formatted_text(entry_session)
189            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]190            data_middle.append([f_label,f_text])
[11535]191        # Requested by Uniben, does not really make sense
192        if not 'current_level' in omit_fields:
193            f_label = trans(_('Current Level:'), lang)
194            f_label = Paragraph(f_label, ENTRY1_STYLE)
195            current_level = studentview.context['studycourse'].current_level
196            studylevelsource = StudyLevelSource().factory
197            current_level = studylevelsource.getTitle(
198                studentview.context, current_level)
199            f_text = formatted_text(current_level)
200            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]201            data_middle.append([f_label,f_text])
[10256]202        if not 'date_of_birth' in omit_fields:
[10261]203            f_label = trans(_('Date of Birth:'), lang)
[10256]204            f_label = Paragraph(f_label, ENTRY1_STYLE)
205            date_of_birth = studentview.context.date_of_birth
206            tz = getUtility(IKofaUtils).tzinfo
207            date_of_birth = to_timezone(date_of_birth, tz)
208            if date_of_birth is not None:
209                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
210            f_text = formatted_text(date_of_birth)
211            f_text = Paragraph(f_text, ENTRY1_STYLE)
[11550]212            data_middle.append([f_label,f_text])
[9141]213
[14292]214    if no_passport:
[14294]215        table = Table(data_middle,style=SLIP_STYLE)
[14292]216        table.hAlign = 'LEFT'
217        return table
218
[11550]219    # append QR code to the right
220    if slipname:
221        url = studentview.url(context, slipname)
222        data_right = [[get_qrcode(url, width=70.0)]]
223        table_right = Table(data_right,style=SLIP_STYLE)
224    else:
225        table_right = None
226
[7318]227    table_left = Table(data_left,style=SLIP_STYLE)
[11550]228    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
229    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
[7019]230    return table
231
[10261]232def render_table_data(tableheader, tabledata, lang='en'):
[7318]233    """Render children table for an existing frame.
234    """
[7304]235    data = []
[7318]236    #data.append([Spacer(1, 12)])
[7304]237    line = []
238    style = getSampleStyleSheet()
239    for element in tableheader:
[10261]240        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
[7310]241        field = Paragraph(field, style["Normal"])
[7304]242        line.append(field)
243    data.append(line)
244    for ticket in tabledata:
245        line = []
246        for element in tableheader:
[7511]247              field = formatted_text(getattr(ticket,element[1],u' '))
[7318]248              field = Paragraph(field, style["Normal"])
[7304]249              line.append(field)
250        data.append(line)
[7310]251    table = Table(data,colWidths=[
[7318]252        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
[7304]253    return table
254
[10261]255def render_transcript_data(view, tableheader, levels_data, lang='en'):
[10250]256    """Render children table for an existing frame.
257    """
258    data = []
259    style = getSampleStyleSheet()
[14473]260    format_float = getUtility(IKofaUtils).format_float
[10250]261    for level in levels_data:
262        level_obj = level['level']
[10251]263        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
264        headerline = []
265        tabledata = []
[15203]266        if 'evel' in view.level_dict.get('ticket.level', str(level_obj.level)):
267            subheader = '%s %s, %s' % (
268                trans(_('Session'), lang),
269                view.session_dict[level_obj.level_session],
270                view.level_dict.get('ticket.level', str(level_obj.level)))
271        else:
272            subheader = '%s %s, %s %s' % (
273                trans(_('Session'), lang),
274                view.session_dict[level_obj.level_session],
275                trans(_('Level'), lang),
[15212]276                view.level_dict.get(level_obj.level, str(level_obj.level)))
[10250]277        data.append(Paragraph(subheader, HEADING_STYLE))
278        for element in tableheader:
279            field = '<strong>%s</strong>' % formatted_text(element[0])
280            field = Paragraph(field, style["Normal"])
[10251]281            headerline.append(field)
282        tabledata.append(headerline)
[10250]283        for ticket in tickets:
[10251]284            ticketline = []
[10250]285            for element in tableheader:
286                  field = formatted_text(getattr(ticket,element[1],u' '))
287                  field = Paragraph(field, style["Normal"])
[10251]288                  ticketline.append(field)
289            tabledata.append(ticketline)
[10250]290        table = Table(tabledata,colWidths=[
291            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
292        data.append(table)
[14473]293        sgpa = format_float(level['sgpa'], 2)
294        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), sgpa)
295        #sgpa = '%s: %.2f' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
[10261]296        data.append(Paragraph(sgpa, style["Normal"]))
[10250]297    return data
298
[8112]299def docs_as_flowables(view, lang='en'):
300    """Create reportlab flowables out of scanned docs.
301    """
302    # XXX: fix circular import problem
[12448]303    from waeup.kofa.browser.fileviewlets import FileManager
[8112]304    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
305    style = getSampleStyleSheet()
306    data = []
[7318]307
[8112]308    # Collect viewlets
309    fm = FileManager(view.context, view.request, view)
310    fm.update()
311    if fm.viewlets:
312        sc_translation = trans(_('Scanned Documents'), lang)
[9910]313        data.append(Paragraph(sc_translation, HEADING_STYLE))
[8112]314        # Insert list of scanned documents
315        table_data = []
316        for viewlet in fm.viewlets:
[10020]317            if viewlet.file_exists:
318                # Show viewlet only if file exists
319                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
320                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
321                    view.context, attr=viewlet.download_name), 'name', None)
322                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
323                if img_path is None:
324                    pass
325                elif not img_path[-4:] in ('.jpg', '.JPG'):
326                    # reportlab requires jpg images, I think.
327                    f_text = Paragraph('%s (not displayable)' % (
328                        viewlet.title,), ENTRY1_STYLE)
329                else:
330                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
331                table_data.append([f_label, f_text])
[8112]332        if table_data:
333            # safety belt; empty tables lead to problems.
334            data.append(Table(table_data, style=SLIP_STYLE))
335    return data
336
[7150]337class StudentsUtils(grok.GlobalUtility):
338    """A collection of methods subject to customization.
339    """
340    grok.implements(IStudentsUtils)
[7019]341
[8268]342    def getReturningData(self, student):
[9005]343        """ Define what happens after school fee payment
[7841]344        depending on the student's senate verdict.
345        In the base configuration current level is always increased
346        by 100 no matter which verdict has been assigned.
347        """
[8268]348        new_level = student['studycourse'].current_level + 100
349        new_session = student['studycourse'].current_session + 1
350        return new_session, new_level
351
352    def setReturningData(self, student):
[9005]353        """ Define what happens after school fee payment
354        depending on the student's senate verdict.
[13124]355        This method folllows the same algorithm as `getReturningData` but
[9005]356        it also sets the new values.
[8268]357        """
358        new_session, new_level = self.getReturningData(student)
[9922]359        try:
360            student['studycourse'].current_level = new_level
361        except ConstraintNotSatisfied:
362            # Do not change level if level exceeds the
363            # certificate's end_level.
364            pass
[8268]365        student['studycourse'].current_session = new_session
[7615]366        verdict = student['studycourse'].current_verdict
[8820]367        student['studycourse'].current_verdict = '0'
[7615]368        student['studycourse'].previous_verdict = verdict
369        return
370
[9519]371    def _getSessionConfiguration(self, session):
372        try:
373            return grok.getSite()['configuration'][str(session)]
374        except KeyError:
375            return None
376
[11451]377    def _isPaymentDisabled(self, p_session, category, student):
378        academic_session = self._getSessionConfiguration(p_session)
[11452]379        if category == 'schoolfee' and \
380            'sf_all' in academic_session.payment_disabled:
[11451]381            return True
382        return False
383
[11641]384    def samePaymentMade(self, student, category, p_item, p_session):
385        for key in student['payments'].keys():
386            ticket = student['payments'][key]
387            if ticket.p_state == 'paid' and\
388               ticket.p_category == category and \
389               ticket.p_item == p_item and \
390               ticket.p_session == p_session:
391                  return True
392        return False
393
[9148]394    def setPaymentDetails(self, category, student,
[9151]395            previous_session, previous_level):
[13124]396        """Create a payment ticket and set the payment data of a
[13040]397        student for the payment category specified.
[7841]398        """
[8595]399        p_item = u''
400        amount = 0.0
[9148]401        if previous_session:
[9517]402            if previous_session < student['studycourse'].entry_session:
403                return _('The previous session must not fall below '
404                         'your entry session.'), None
405            if category == 'schoolfee':
406                # School fee is always paid for the following session
407                if previous_session > student['studycourse'].current_session:
408                    return _('This is not a previous session.'), None
409            else:
410                if previous_session > student['studycourse'].current_session - 1:
411                    return _('This is not a previous session.'), None
[9148]412            p_session = previous_session
413            p_level = previous_level
414            p_current = False
415        else:
416            p_session = student['studycourse'].current_session
417            p_level = student['studycourse'].current_level
418            p_current = True
[9519]419        academic_session = self._getSessionConfiguration(p_session)
420        if academic_session == None:
[8595]421            return _(u'Session configuration object is not available.'), None
[9521]422        # Determine fee.
[7150]423        if category == 'schoolfee':
[8595]424            try:
[8596]425                certificate = student['studycourse'].certificate
426                p_item = certificate.code
[8595]427            except (AttributeError, TypeError):
428                return _('Study course data are incomplete.'), None
[9148]429            if previous_session:
[9916]430                # Students can pay for previous sessions in all
431                # workflow states.  Fresh students are excluded by the
432                # update method of the PreviousPaymentAddFormPage.
[9148]433                if previous_level == 100:
434                    amount = getattr(certificate, 'school_fee_1', 0.0)
435                else:
436                    amount = getattr(certificate, 'school_fee_2', 0.0)
437            else:
438                if student.state == CLEARED:
439                    amount = getattr(certificate, 'school_fee_1', 0.0)
440                elif student.state == RETURNING:
[9916]441                    # In case of returning school fee payment the
442                    # payment session and level contain the values of
443                    # the session the student has paid for. Payment
444                    # session is always next session.
[9148]445                    p_session, p_level = self.getReturningData(student)
[9519]446                    academic_session = self._getSessionConfiguration(p_session)
447                    if academic_session == None:
[9916]448                        return _(
449                            u'Session configuration object is not available.'
450                            ), None
[9148]451                    amount = getattr(certificate, 'school_fee_2', 0.0)
452                elif student.is_postgrad and student.state == PAID:
[9916]453                    # Returning postgraduate students also pay for the
454                    # next session but their level always remains the
455                    # same.
[9148]456                    p_session += 1
[9519]457                    academic_session = self._getSessionConfiguration(p_session)
458                    if academic_session == None:
[9916]459                        return _(
460                            u'Session configuration object is not available.'
461                            ), None
[9148]462                    amount = getattr(certificate, 'school_fee_2', 0.0)
[7150]463        elif category == 'clearance':
[9178]464            try:
465                p_item = student['studycourse'].certificate.code
466            except (AttributeError, TypeError):
467                return _('Study course data are incomplete.'), None
[8595]468            amount = academic_session.clearance_fee
[7150]469        elif category == 'bed_allocation':
[8595]470            p_item = self.getAccommodationDetails(student)['bt']
471            amount = academic_session.booking_fee
[9423]472        elif category == 'hostel_maintenance':
[10681]473            amount = 0.0
[9429]474            bedticket = student['accommodation'].get(
475                str(student.current_session), None)
[13501]476            if bedticket is not None and bedticket.bed is not None:
[9429]477                p_item = bedticket.bed_coordinates
[10681]478                if bedticket.bed.__parent__.maint_fee > 0:
479                    amount = bedticket.bed.__parent__.maint_fee
480                else:
481                    # fallback
482                    amount = academic_session.maint_fee
[9429]483            else:
[13505]484                return _(u'No bed allocated.'), None
[10449]485        elif category == 'transcript':
486            amount = academic_session.transcript_fee
[13574]487        elif category == 'transfer':
488            amount = academic_session.transfer_fee
[13031]489        elif category == 'late_registration':
490            amount = academic_session.late_registration_fee
[8595]491        if amount in (0.0, None):
[9517]492            return _('Amount could not be determined.'), None
[11641]493        if self.samePaymentMade(student, category, p_item, p_session):
494            return _('This type of payment has already been made.'), None
[11451]495        if self._isPaymentDisabled(p_session, category, student):
[13797]496            return _('This category of payments has been disabled.'), None
[8708]497        payment = createObject(u'waeup.StudentOnlinePayment')
[8951]498        timestamp = ("%d" % int(time()*10000))[1:]
[8595]499        payment.p_id = "p%s" % timestamp
500        payment.p_category = category
501        payment.p_item = p_item
502        payment.p_session = p_session
503        payment.p_level = p_level
[9148]504        payment.p_current = p_current
[8595]505        payment.amount_auth = amount
506        return None, payment
[7019]507
[9868]508    def setBalanceDetails(self, category, student,
[9864]509            balance_session, balance_level, balance_amount):
[13124]510        """Create a balance payment ticket and set the payment data
511        as selected by the student.
[9864]512        """
[9868]513        p_item = u'Balance'
[9864]514        p_session = balance_session
515        p_level = balance_level
516        p_current = False
517        amount = balance_amount
518        academic_session = self._getSessionConfiguration(p_session)
519        if academic_session == None:
520            return _(u'Session configuration object is not available.'), None
[9874]521        if amount in (0.0, None) or amount < 0:
522            return _('Amount must be greater than 0.'), None
[9864]523        payment = createObject(u'waeup.StudentOnlinePayment')
524        timestamp = ("%d" % int(time()*10000))[1:]
525        payment.p_id = "p%s" % timestamp
[9868]526        payment.p_category = category
[9864]527        payment.p_item = p_item
528        payment.p_session = p_session
529        payment.p_level = p_level
530        payment.p_current = p_current
531        payment.amount_auth = amount
532        return None, payment
533
[12896]534    def increaseMatricInteger(self, student):
535        """Increase counter for matric numbers.
536        This counter can be a centrally stored attribute or an attribute of
537        faculties, departments or certificates. In the base package the counter
[13124]538        is as an attribute of the site configuration container.
[12896]539        """
540        grok.getSite()['configuration'].next_matric_integer += 1
541        return
542
[11595]543    def constructMatricNumber(self, student):
[12896]544        """Fetch the matric number counter which fits the student and
545        construct the new matric number of the student.
[12902]546        In the base package the counter is returned which is as an attribute
[13124]547        of the site configuration container.
[12896]548        """
[11595]549        next_integer = grok.getSite()['configuration'].next_matric_integer
550        if next_integer == 0:
[11619]551            return _('Matriculation number cannot be set.'), None
552        return None, unicode(next_integer)
[11589]553
554    def setMatricNumber(self, student):
[13124]555        """Set matriculation number of student. If the student's matric number
556        is unset a new matric number is
[12896]557        constructed according to the matriculation number construction rules
[13124]558        defined in the `constructMatricNumber` method. The new matric number is
[12896]559        set, the students catalog updated. The corresponding matric number
560        counter is increased by one.
[11589]561
562        This method is tested but not used in the base package. It can
563        be used in custom packages by adding respective views
[13124]564        and by customizing `increaseMatricInteger` and `constructMatricNumber`
[12896]565        according to the university's matriculation number construction rules.
[11589]566
[12896]567        The method can be disabled by setting the counter to zero.
[11589]568        """
569        if student.matric_number is not None:
570            return _('Matriculation number already set.'), None
[11590]571        if student.certcode is None:
572            return _('No certificate assigned.'), None
[11619]573        error, matric_number = self.constructMatricNumber(student)
574        if error:
575            return error, None
[11589]576        try:
[11592]577            student.matric_number = matric_number
[11589]578        except MatNumNotInSource:
[13224]579            return _('Matriculation number %s exists.' % matric_number), None
[11589]580        notify(grok.ObjectModifiedEvent(student))
[12896]581        self.increaseMatricInteger(student)
[11595]582        return None, matric_number
[11589]583
[7186]584    def getAccommodationDetails(self, student):
[9219]585        """Determine the accommodation data of a student.
[7841]586        """
[7150]587        d = {}
588        d['error'] = u''
[8685]589        hostels = grok.getSite()['hostels']
590        d['booking_session'] = hostels.accommodation_session
591        d['allowed_states'] = hostels.accommodation_states
[8688]592        d['startdate'] = hostels.startdate
593        d['enddate'] = hostels.enddate
594        d['expired'] = hostels.expired
[7150]595        # Determine bed type
596        studycourse = student['studycourse']
[7369]597        certificate = getattr(studycourse,'certificate',None)
[7150]598        entry_session = studycourse.entry_session
599        current_level = studycourse.current_level
[9187]600        if None in (entry_session, current_level, certificate):
601            return d
[7369]602        end_level = certificate.end_level
[9148]603        if current_level == 10:
604            bt = 'pr'
605        elif entry_session == grok.getSite()['hostels'].accommodation_session:
[7150]606            bt = 'fr'
607        elif current_level >= end_level:
608            bt = 'fi'
609        else:
610            bt = 're'
611        if student.sex == 'f':
612            sex = 'female'
613        else:
614            sex = 'male'
615        special_handling = 'regular'
616        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
617        return d
[7019]618
[13247]619    def checkAccommodationRequirements(self, student, acc_details):
620        if acc_details.get('expired', False):
621            startdate = acc_details.get('startdate')
622            enddate = acc_details.get('enddate')
623            if startdate and enddate:
624                tz = getUtility(IKofaUtils).tzinfo
625                startdate = to_timezone(
626                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
627                enddate = to_timezone(
628                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
629                return _("Outside booking period: ${a} - ${b}",
630                         mapping = {'a': startdate, 'b': enddate})
631            else:
632                return _("Outside booking period.")
633        if not acc_details.get('bt'):
634            return _("Your data are incomplete.")
635        if not student.state in acc_details['allowed_states']:
636            return _("You are in the wrong registration state.")
637        if student['studycourse'].current_session != acc_details[
638            'booking_session']:
639            return _('Your current session does not '
640                     'match accommodation session.')
641        if str(acc_details['booking_session']) in student['accommodation'].keys():
642            return _('You already booked a bed space in '
643                     'current accommodation session.')
644        return
645
[13457]646    def selectBed(self, available_beds, desired_hostel=None):
647        """Select a bed from a filtered list of available beds.
648        In the base configuration beds are sorted by the sort id
649        of the hostel and the bed number. The first bed found in
650        this sorted list is taken.
[7841]651        """
[13457]652        sorted_beds = sorted(available_beds,
653                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
654        if desired_hostel:
655            # Filter desired hostel beds
656            filtered_beds = [bed for bed in sorted_beds
657                             if bed.bed_id.startswith(desired_hostel)]
658            if not filtered_beds:
659                return
660            return filtered_beds[0]
661        return sorted_beds[0]
[7150]662
[9981]663    def _admissionText(self, student, portal_language):
[9979]664        inst_name = grok.getSite()['configuration'].name
665        text = trans(_(
666            'This is to inform you that you have been provisionally'
667            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
668            portal_language)
669        return text
670
[10686]671    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
672                                 pre_text=None, post_text=None,):
[9191]673        """Render pdf admission letter.
674        """
675        if student is None:
676            return
677        style = getSampleStyleSheet()
[9949]678        creator = self.getPDFCreator(student)
[9979]679        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9191]680        data = []
681        doc_title = view.label
682        author = '%s (%s)' % (view.request.principal.title,
683                              view.request.principal.id)
[9944]684        footer_text = view.label.split('\n')
685        if len(footer_text) > 1:
686            # We can add a department in first line
687            footer_text = footer_text[1]
688        else:
689            # Only the first line is used for the footer
690            footer_text = footer_text[0]
[9191]691        if getattr(student, 'student_id', None) is not None:
692            footer_text = "%s - %s - " % (student.student_id, footer_text)
693
[10702]694        # Text before student data
[10686]695        if pre_text is None:
696            html = format_html(self._admissionText(student, portal_language))
697        else:
698            html = format_html(pre_text)
[11875]699        if html:
700            data.append(Paragraph(html, NOTE_STYLE))
701            data.append(Spacer(1, 20))
[9191]702
703        # Student data
[11550]704        data.append(render_student_data(view, student,
705                    omit_fields, lang=portal_language,
706                    slipname='admission_slip.pdf'))
[9191]707
[10702]708        # Text after student data
[9191]709        data.append(Spacer(1, 20))
[10686]710        if post_text is None:
711            datelist = student.history.messages[0].split()[0].split('-')
712            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
[10702]713            post_text = trans(_(
[10686]714                'Your Kofa student record was created on ${a}.',
715                mapping = {'a': creation_date}),
716                portal_language)
[10702]717        #html = format_html(post_text)
718        #data.append(Paragraph(html, NOTE_STYLE))
[9191]719
720        # Create pdf stream
721        view.response.setHeader(
722            'Content-Type', 'application/pdf')
723        pdf_stream = creator.create_pdf(
724            data, None, doc_title, author=author, footer=footer_text,
[10702]725            note=post_text)
[9191]726        return pdf_stream
727
[9949]728    def getPDFCreator(self, context):
729        """Get a pdf creator suitable for `context`.
730        The default implementation always returns the default creator.
731        """
732        return getUtility(IPDFCreator)
733
[8257]734    def renderPDF(self, view, filename='slip.pdf', student=None,
[9906]735                  studentview=None,
[10439]736                  tableheader=[], tabledata=[],
[9555]737                  note=None, signatures=None, sigs_in_footer=(),
[10250]738                  show_scans=True, topMargin=1.5,
739                  omit_fields=()):
[14151]740        """Render pdf slips for various pages (also some pages
741        in the applicants module).
[7841]742        """
[10261]743        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9916]744        # XXX: tell what the different parameters mean
[8112]745        style = getSampleStyleSheet()
[9949]746        creator = self.getPDFCreator(student)
[8112]747        data = []
748        doc_title = view.label
749        author = '%s (%s)' % (view.request.principal.title,
750                              view.request.principal.id)
[9913]751        footer_text = view.label.split('\n')
[13304]752        if len(footer_text) > 1:
753            # We can add a department in first line, second line is used
[9913]754            footer_text = footer_text[1]
755        else:
[9917]756            # Only the first line is used for the footer
[9913]757            footer_text = footer_text[0]
[7714]758        if getattr(student, 'student_id', None) is not None:
[7310]759            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]760
[7318]761        # Insert student data table
[7310]762        if student is not None:
[8112]763            bd_translation = trans(_('Base Data'), portal_language)
[9910]764            data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]765            data.append(render_student_data(
[11550]766                studentview, view.context, omit_fields, lang=portal_language,
767                slipname=filename))
[7304]768
[7318]769        # Insert widgets
[9191]770        if view.form_fields:
[9910]771            data.append(Paragraph(view.title, HEADING_STYLE))
[9191]772            separators = getattr(self, 'SEPARATORS_DICT', {})
773            table = creator.getWidgetsTable(
774                view.form_fields, view.context, None, lang=portal_language,
775                separators=separators)
776            data.append(table)
[7318]777
[8112]778        # Insert scanned docs
[9550]779        if show_scans:
780            data.extend(docs_as_flowables(view, portal_language))
[7318]781
[9452]782        # Insert history
[9910]783        if filename.startswith('clearance'):
[9452]784            hist_translation = trans(_('Workflow History'), portal_language)
[9910]785            data.append(Paragraph(hist_translation, HEADING_STYLE))
[9452]786            data.extend(creator.fromStringList(student.history.messages))
787
[10438]788        # Insert content tables (optionally on second page)
[10439]789        if hasattr(view, 'tabletitle'):
790            for i in range(len(view.tabletitle)):
791                if tabledata[i] and tableheader[i]:
792                    #data.append(PageBreak())
793                    #data.append(Spacer(1, 20))
794                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
795                    data.append(Spacer(1, 8))
796                    contenttable = render_table_data(tableheader[i],tabledata[i])
797                    data.append(contenttable)
[7318]798
[9010]799        # Insert signatures
[9965]800        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
801        # do not have a test for the following lines.
[9555]802        if signatures and not sigs_in_footer:
[9010]803            data.append(Spacer(1, 20))
[9966]804            # Render one signature table per signature to
805            # get date and signature in line.
806            for signature in signatures:
807                signaturetables = get_signature_tables(signature)
808                data.append(signaturetables[0])
[9010]809
[7150]810        view.response.setHeader(
811            'Content-Type', 'application/pdf')
[8112]812        try:
813            pdf_stream = creator.create_pdf(
[8257]814                data, None, doc_title, author=author, footer=footer_text,
[9948]815                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
[8112]816        except IOError:
817            view.flash('Error in image file.')
818            return view.redirect(view.url(view.context))
[14256]819        except LayoutError, err:
820            view.flash(
821                'PDF file could not be created. Reportlab error message: %s'
822                % escape(err.message),
823                type="danger")
824            return view.redirect(view.url(view.context))
[8112]825        return pdf_stream
[7620]826
[14915]827    def GPABoundaries(self, faccode=None, depcode=None, certcode=None):
[14914]828        return ((1, 'Fail'),
829               (1.5, 'Pass'),
830               (2.4, '3rd Class'),
831               (3.5, '2nd Class Lower'),
832               (4.5, '2nd Class Upper'),
833               (5, '1st Class'))
[10576]834
[14461]835    def getClassFromCGPA(self, gpa, student):
836        """Determine the class of degree. In some custom packages
837        this class depends on e.g. the entry session of the student. In the
838        base package, it does not.
839        """
[14914]840        if gpa < self.GPABoundaries()[0][0]:
841            return 0, self.GPABoundaries()[0][1]
842        if gpa < self.GPABoundaries()[1][0]:
843            return 1, self.GPABoundaries()[1][1]
844        if gpa < self.GPABoundaries()[2][0]:
845            return 2, self.GPABoundaries()[2][1]
846        if gpa < self.GPABoundaries()[3][0]:
847            return 3, self.GPABoundaries()[3][1]
848        if gpa < self.GPABoundaries()[4][0]:
849            return 4, self.GPABoundaries()[4][1]
850        if gpa <= self.GPABoundaries()[5][0]:
851            return 5, self.GPABoundaries()[5][1]
[15102]852        return
[10445]853
[14159]854    def getDegreeClassNumber(self, level_obj):
855        """Get degree class number (used for SessionResultsPresentation
[14157]856        reports).
857        """
[14410]858        if level_obj.gpa_params[1] == 0:
859            # No credits weighted
860            return 6
[14461]861        return self.getClassFromCGPA(
862            level_obj.cumulative_params[0], level_obj.student)[0]
[14157]863
[15163]864    def _saveTranscriptPDF(self, student, transcript):
865        """Create a transcript PDF file and store it in student folder.
866        """
867        file_store = getUtility(IExtFileStore)
868        file_id = IFileStoreNameChooser(student).chooseName(
869            attr="final_transcript.pdf")
870        file_store.createFile(file_id, StringIO(transcript))
871        return
872
[10250]873    def renderPDFTranscript(self, view, filename='transcript.pdf',
874                  student=None,
875                  studentview=None,
[15163]876                  note=None,
877                  signatures=(),
878                  sigs_in_footer=(),
879                  digital_sigs=(),
[10250]880                  show_scans=True, topMargin=1.5,
881                  omit_fields=(),
[14292]882                  tableheader=None,
[15163]883                  no_passport=False,
884                  save_file=False):
[14583]885        """Render pdf slip of a transcripts.
[10250]886        """
[10261]887        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[10250]888        # XXX: tell what the different parameters mean
889        style = getSampleStyleSheet()
890        creator = self.getPDFCreator(student)
891        data = []
892        doc_title = view.label
893        author = '%s (%s)' % (view.request.principal.title,
894                              view.request.principal.id)
895        footer_text = view.label.split('\n')
896        if len(footer_text) > 2:
897            # We can add a department in first line
898            footer_text = footer_text[1]
899        else:
900            # Only the first line is used for the footer
901            footer_text = footer_text[0]
902        if getattr(student, 'student_id', None) is not None:
903            footer_text = "%s - %s - " % (student.student_id, footer_text)
904
905        # Insert student data table
906        if student is not None:
907            #bd_translation = trans(_('Base Data'), portal_language)
908            #data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]909            data.append(render_student_data(
[11550]910                studentview, view.context,
911                omit_fields, lang=portal_language,
[14292]912                slipname=filename,
913                no_passport=no_passport))
[10250]914
915        transcript_data = view.context.getTranscriptData()
916        levels_data = transcript_data[0]
917
918        contextdata = []
[10261]919        f_label = trans(_('Course of Study:'), portal_language)
[10250]920        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10650]921        f_text = formatted_text(view.context.certificate.longtitle)
[10250]922        f_text = Paragraph(f_text, ENTRY1_STYLE)
923        contextdata.append([f_label,f_text])
924
[10261]925        f_label = trans(_('Faculty:'), portal_language)
[10250]926        f_label = Paragraph(f_label, ENTRY1_STYLE)
927        f_text = formatted_text(
[10650]928            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
[10250]929        f_text = Paragraph(f_text, ENTRY1_STYLE)
930        contextdata.append([f_label,f_text])
931
[10261]932        f_label = trans(_('Department:'), portal_language)
[10250]933        f_label = Paragraph(f_label, ENTRY1_STYLE)
934        f_text = formatted_text(
[10650]935            view.context.certificate.__parent__.__parent__.longtitle)
[10250]936        f_text = Paragraph(f_text, ENTRY1_STYLE)
937        contextdata.append([f_label,f_text])
938
[10261]939        f_label = trans(_('Entry Session:'), portal_language)
[10250]940        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]941        f_text = formatted_text(
942            view.session_dict.get(view.context.entry_session))
[10250]943        f_text = Paragraph(f_text, ENTRY1_STYLE)
944        contextdata.append([f_label,f_text])
945
[10261]946        f_label = trans(_('Entry Mode:'), portal_language)
[10250]947        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]948        f_text = formatted_text(view.studymode_dict.get(
949            view.context.entry_mode))
[10250]950        f_text = Paragraph(f_text, ENTRY1_STYLE)
951        contextdata.append([f_label,f_text])
952
[10262]953        f_label = trans(_('Cumulative GPA:'), portal_language)
[10250]954        f_label = Paragraph(f_label, ENTRY1_STYLE)
[14473]955        format_float = getUtility(IKofaUtils).format_float
956        cgpa = format_float(transcript_data[1], 3)
957        f_text = formatted_text('%s (%s)' % (
958            cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
[10250]959        f_text = Paragraph(f_text, ENTRY1_STYLE)
960        contextdata.append([f_label,f_text])
961
962        contexttable = Table(contextdata,style=SLIP_STYLE)
963        data.append(contexttable)
964
965        transcripttables = render_transcript_data(
[10261]966            view, tableheader, levels_data, lang=portal_language)
[10250]967        data.extend(transcripttables)
968
969        # Insert signatures
970        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
971        # do not have a test for the following lines.
972        if signatures and not sigs_in_footer:
973            data.append(Spacer(1, 20))
974            # Render one signature table per signature to
975            # get date and signature in line.
976            for signature in signatures:
977                signaturetables = get_signature_tables(signature)
978                data.append(signaturetables[0])
979
[15163]980        # Insert digital signatures
981        if digital_sigs:
982            data.append(Spacer(1, 20))
983            sigs = digital_sigs.split('\n')
984            for sig in sigs:
985                data.append(Paragraph(sig, NOTE_STYLE))
986
[10250]987        view.response.setHeader(
988            'Content-Type', 'application/pdf')
989        try:
990            pdf_stream = creator.create_pdf(
991                data, None, doc_title, author=author, footer=footer_text,
992                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
993        except IOError:
[10261]994            view.flash(_('Error in image file.'))
[10250]995            return view.redirect(view.url(view.context))
[15163]996        if save_file:
997            self._saveTranscriptPDF(student, pdf_stream)
998            return
[10250]999        return pdf_stream
1000
[13898]1001    def renderPDFCourseticketsOverview(
[15197]1002            self, view, session, data, lecturers, orientation, title_length):
[14583]1003        """Render pdf slip of course tickets for a lecturer.
1004        """
[13898]1005        filename = 'coursetickets_%s_%s_%s.pdf' % (
1006            view.context.code, session, view.request.principal.id)
1007        session = academic_sessions_vocab.getTerm(session).title
[14702]1008        creator = getUtility(IPDFCreator, name=orientation)
[13898]1009        style = getSampleStyleSheet()
[15197]1010        pdf_data = []
1011        pdf_data += [Paragraph(
[14151]1012            translate(_('<b>Lecturer(s): ${a}</b>',
1013                      mapping = {'a':lecturers})), style["Normal"]),]
1014        pdf_data += [Paragraph(
1015            translate(_('<b>Credits: ${a}</b>',
1016                      mapping = {'a':view.context.credits})), style["Normal"]),]
[14314]1017        # Not used in base package.
1018        if data[1]:
1019            pdf_data += [Paragraph(
[14709]1020                translate(_('<b>${a}</b>',
[14314]1021                    mapping = {'a':data[1][0]})), style["Normal"]),]
1022            pdf_data += [Paragraph(
[14709]1023                translate(_('<b>${a}</b>',
[14708]1024                    mapping = {'a':data[1][1]})), style["Normal"]),]
1025
1026            pdf_data += [Paragraph(
1027                translate(_('<b>Total Students: ${a}</b>',
1028                    mapping = {'a':data[1][2]})), style["Normal"]),]
1029            pdf_data += [Paragraph(
[14319]1030                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
[14708]1031                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
[14319]1032            pdf_data += [Paragraph(
1033                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
[14708]1034                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
[13899]1035        pdf_data.append(Spacer(1, 20))
[14314]1036        pdf_data += [Table(data[0], style=CONTENT_STYLE)]
[15234]1037        # Process title if too long
1038        title = " ".join(view.context.title.split())
1039        ct = textwrap.fill(title, title_length)
1040        ft = title
[15235]1041        if len(textwrap.wrap(title, title_length)) > 1:
[15234]1042            ft = textwrap.wrap(title, title_length)[0] + ' ...'
[15197]1043        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1044            mapping = {'a':ct,
[14151]1045                       'b':view.context.code,
1046                       'd':session}))
[14705]1047        footer_title = translate(_('${a} (${b}) - ${d}',
[15197]1048            mapping = {'a':ft,
[14705]1049                       'b':view.context.code,
1050                       'd':session}))
[13898]1051        author = '%s (%s)' % (view.request.principal.title,
1052                              view.request.principal.id)
1053        view.response.setHeader(
1054            'Content-Type', 'application/pdf')
1055        view.response.setHeader(
1056            'Content-Disposition:', 'attachment; filename="%s' % filename)
1057        pdf_stream = creator.create_pdf(
[14705]1058            pdf_data, None, doc_title, author, footer_title + ' -'
[13898]1059            )
1060        return pdf_stream
1061
[14584]1062    def warnCreditsOOR(self, studylevel, course=None):
1063        """Return message if credits are out of range. In the base
1064        package only maximum credits is set.
[9830]1065        """
[14582]1066        if course and studylevel.total_credits + course.credits > 50:
[14584]1067            return _('Maximum credits exceeded.')
[14582]1068        elif studylevel.total_credits > 50:
[14584]1069            return _('Maximum credits exceeded.')
[14596]1070        return
[9830]1071
[9987]1072    def getBedCoordinates(self, bedticket):
[13132]1073        """Return descriptive bed coordinates.
[13124]1074        This method can be used to customize the `display_coordinates`
[13132]1075        property method in order to  display a
1076        customary description of the bed space.
[9987]1077        """
1078        return bedticket.bed_coordinates
1079
[11772]1080    def clearance_disabled_message(self, student):
[14583]1081        """Render message if clearance is disabled.
1082        """
[11772]1083        try:
1084            session_config = grok.getSite()[
1085                'configuration'][str(student.current_session)]
1086        except KeyError:
1087            return _('Session configuration object is not available.')
1088        if not session_config.clearance_enabled:
1089            return _('Clearance is disabled for this session.')
1090        return None
1091
[13132]1092    #: A dictionary which maps widget names to headlines. The headline
1093    #: is rendered in forms and on pdf slips above the respective
1094    #: display or input widget. There are no separating headlines
1095    #: in the base package.
[13129]1096    SEPARATORS_DICT = {}
[8410]1097
[13132]1098    #: A tuple containing names of file upload viewlets which are not shown
1099    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1100    #: in the base package. This attribute makes only sense, if intermediate
1101    #: custom packages are being used, like we do for all Nigerian portals.
[10021]1102    SKIP_UPLOAD_VIEWLETS = ()
1103
[13132]1104    #: A tuple containing the names of registration states in which changing of
1105    #: passport pictures is allowed.
[13129]1106    PORTRAIT_CHANGE_STATES = (ADMITTED,)
[10706]1107
[12104]1108    #: A tuple containing all exporter names referring to students or
1109    #: subobjects thereof.
1110    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
1111            'studentstudylevels', 'coursetickets',
[12971]1112            'studentpayments', 'studentunpaidpayments',
[15051]1113            'bedtickets', 'sfpaymentsoverview', 'sessionpaymentsoverview',
[12104]1114            'studylevelsoverview', 'combocard', 'bursary')
1115
[12971]1116    #: A tuple containing all exporter names needed for backing
1117    #: up student data
1118    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
1119            'studentstudylevels', 'coursetickets',
1120            'studentpayments', 'bedtickets')
1121
[8410]1122    #: A prefix used when generating new student ids. Each student id will
[13129]1123    #: start with this string. The default is 'K' for Kofa.
[8410]1124    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.