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

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

Add remark on transcript pdf slips.

  • Property svn:keywords set to Id
File size: 47.9 KB
RevLine 
[7191]1## $Id: utils.py 15331 2019-02-15 21:12:30Z 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"]))
[15331]297        if getattr(level_obj, 'remark'):
298            remark = '%s: %s' % (
299                trans('Remark', lang), getattr(level_obj, 'remark'))
300            data.append(Paragraph(remark, style["Normal"]))
[10250]301    return data
302
[8112]303def docs_as_flowables(view, lang='en'):
304    """Create reportlab flowables out of scanned docs.
305    """
306    # XXX: fix circular import problem
[12448]307    from waeup.kofa.browser.fileviewlets import FileManager
[8112]308    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
309    style = getSampleStyleSheet()
310    data = []
[7318]311
[8112]312    # Collect viewlets
313    fm = FileManager(view.context, view.request, view)
314    fm.update()
315    if fm.viewlets:
316        sc_translation = trans(_('Scanned Documents'), lang)
[9910]317        data.append(Paragraph(sc_translation, HEADING_STYLE))
[8112]318        # Insert list of scanned documents
319        table_data = []
320        for viewlet in fm.viewlets:
[10020]321            if viewlet.file_exists:
322                # Show viewlet only if file exists
323                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
324                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
325                    view.context, attr=viewlet.download_name), 'name', None)
326                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
327                if img_path is None:
328                    pass
329                elif not img_path[-4:] in ('.jpg', '.JPG'):
330                    # reportlab requires jpg images, I think.
331                    f_text = Paragraph('%s (not displayable)' % (
332                        viewlet.title,), ENTRY1_STYLE)
333                else:
334                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
335                table_data.append([f_label, f_text])
[8112]336        if table_data:
337            # safety belt; empty tables lead to problems.
338            data.append(Table(table_data, style=SLIP_STYLE))
339    return data
340
[7150]341class StudentsUtils(grok.GlobalUtility):
342    """A collection of methods subject to customization.
343    """
344    grok.implements(IStudentsUtils)
[7019]345
[8268]346    def getReturningData(self, student):
[9005]347        """ Define what happens after school fee payment
[7841]348        depending on the student's senate verdict.
349        In the base configuration current level is always increased
350        by 100 no matter which verdict has been assigned.
351        """
[8268]352        new_level = student['studycourse'].current_level + 100
353        new_session = student['studycourse'].current_session + 1
354        return new_session, new_level
355
356    def setReturningData(self, student):
[9005]357        """ Define what happens after school fee payment
358        depending on the student's senate verdict.
[13124]359        This method folllows the same algorithm as `getReturningData` but
[9005]360        it also sets the new values.
[8268]361        """
362        new_session, new_level = self.getReturningData(student)
[9922]363        try:
364            student['studycourse'].current_level = new_level
365        except ConstraintNotSatisfied:
366            # Do not change level if level exceeds the
367            # certificate's end_level.
368            pass
[8268]369        student['studycourse'].current_session = new_session
[7615]370        verdict = student['studycourse'].current_verdict
[8820]371        student['studycourse'].current_verdict = '0'
[7615]372        student['studycourse'].previous_verdict = verdict
373        return
374
[9519]375    def _getSessionConfiguration(self, session):
376        try:
377            return grok.getSite()['configuration'][str(session)]
378        except KeyError:
379            return None
380
[11451]381    def _isPaymentDisabled(self, p_session, category, student):
382        academic_session = self._getSessionConfiguration(p_session)
[11452]383        if category == 'schoolfee' and \
384            'sf_all' in academic_session.payment_disabled:
[11451]385            return True
386        return False
387
[11641]388    def samePaymentMade(self, student, category, p_item, p_session):
389        for key in student['payments'].keys():
390            ticket = student['payments'][key]
391            if ticket.p_state == 'paid' and\
392               ticket.p_category == category and \
393               ticket.p_item == p_item and \
394               ticket.p_session == p_session:
395                  return True
396        return False
397
[9148]398    def setPaymentDetails(self, category, student,
[9151]399            previous_session, previous_level):
[13124]400        """Create a payment ticket and set the payment data of a
[13040]401        student for the payment category specified.
[7841]402        """
[8595]403        p_item = u''
404        amount = 0.0
[9148]405        if previous_session:
[9517]406            if previous_session < student['studycourse'].entry_session:
407                return _('The previous session must not fall below '
408                         'your entry session.'), None
409            if category == 'schoolfee':
410                # School fee is always paid for the following session
411                if previous_session > student['studycourse'].current_session:
412                    return _('This is not a previous session.'), None
413            else:
414                if previous_session > student['studycourse'].current_session - 1:
415                    return _('This is not a previous session.'), None
[9148]416            p_session = previous_session
417            p_level = previous_level
418            p_current = False
419        else:
420            p_session = student['studycourse'].current_session
421            p_level = student['studycourse'].current_level
422            p_current = True
[9519]423        academic_session = self._getSessionConfiguration(p_session)
424        if academic_session == None:
[8595]425            return _(u'Session configuration object is not available.'), None
[9521]426        # Determine fee.
[7150]427        if category == 'schoolfee':
[8595]428            try:
[8596]429                certificate = student['studycourse'].certificate
430                p_item = certificate.code
[8595]431            except (AttributeError, TypeError):
432                return _('Study course data are incomplete.'), None
[9148]433            if previous_session:
[9916]434                # Students can pay for previous sessions in all
435                # workflow states.  Fresh students are excluded by the
436                # update method of the PreviousPaymentAddFormPage.
[9148]437                if previous_level == 100:
438                    amount = getattr(certificate, 'school_fee_1', 0.0)
439                else:
440                    amount = getattr(certificate, 'school_fee_2', 0.0)
441            else:
442                if student.state == CLEARED:
443                    amount = getattr(certificate, 'school_fee_1', 0.0)
444                elif student.state == RETURNING:
[9916]445                    # In case of returning school fee payment the
446                    # payment session and level contain the values of
447                    # the session the student has paid for. Payment
448                    # session is always next session.
[9148]449                    p_session, p_level = self.getReturningData(student)
[9519]450                    academic_session = self._getSessionConfiguration(p_session)
451                    if academic_session == None:
[9916]452                        return _(
453                            u'Session configuration object is not available.'
454                            ), None
[9148]455                    amount = getattr(certificate, 'school_fee_2', 0.0)
456                elif student.is_postgrad and student.state == PAID:
[9916]457                    # Returning postgraduate students also pay for the
458                    # next session but their level always remains the
459                    # same.
[9148]460                    p_session += 1
[9519]461                    academic_session = self._getSessionConfiguration(p_session)
462                    if academic_session == None:
[9916]463                        return _(
464                            u'Session configuration object is not available.'
465                            ), None
[9148]466                    amount = getattr(certificate, 'school_fee_2', 0.0)
[7150]467        elif category == 'clearance':
[9178]468            try:
469                p_item = student['studycourse'].certificate.code
470            except (AttributeError, TypeError):
471                return _('Study course data are incomplete.'), None
[8595]472            amount = academic_session.clearance_fee
[7150]473        elif category == 'bed_allocation':
[8595]474            p_item = self.getAccommodationDetails(student)['bt']
475            amount = academic_session.booking_fee
[9423]476        elif category == 'hostel_maintenance':
[10681]477            amount = 0.0
[9429]478            bedticket = student['accommodation'].get(
479                str(student.current_session), None)
[13501]480            if bedticket is not None and bedticket.bed is not None:
[9429]481                p_item = bedticket.bed_coordinates
[10681]482                if bedticket.bed.__parent__.maint_fee > 0:
483                    amount = bedticket.bed.__parent__.maint_fee
484                else:
485                    # fallback
486                    amount = academic_session.maint_fee
[9429]487            else:
[13505]488                return _(u'No bed allocated.'), None
[10449]489        elif category == 'transcript':
490            amount = academic_session.transcript_fee
[13574]491        elif category == 'transfer':
492            amount = academic_session.transfer_fee
[13031]493        elif category == 'late_registration':
494            amount = academic_session.late_registration_fee
[8595]495        if amount in (0.0, None):
[9517]496            return _('Amount could not be determined.'), None
[11641]497        if self.samePaymentMade(student, category, p_item, p_session):
498            return _('This type of payment has already been made.'), None
[11451]499        if self._isPaymentDisabled(p_session, category, student):
[13797]500            return _('This category of payments has been disabled.'), None
[8708]501        payment = createObject(u'waeup.StudentOnlinePayment')
[8951]502        timestamp = ("%d" % int(time()*10000))[1:]
[8595]503        payment.p_id = "p%s" % timestamp
504        payment.p_category = category
505        payment.p_item = p_item
506        payment.p_session = p_session
507        payment.p_level = p_level
[9148]508        payment.p_current = p_current
[8595]509        payment.amount_auth = amount
510        return None, payment
[7019]511
[9868]512    def setBalanceDetails(self, category, student,
[9864]513            balance_session, balance_level, balance_amount):
[13124]514        """Create a balance payment ticket and set the payment data
515        as selected by the student.
[9864]516        """
[9868]517        p_item = u'Balance'
[9864]518        p_session = balance_session
519        p_level = balance_level
520        p_current = False
521        amount = balance_amount
522        academic_session = self._getSessionConfiguration(p_session)
523        if academic_session == None:
524            return _(u'Session configuration object is not available.'), None
[9874]525        if amount in (0.0, None) or amount < 0:
526            return _('Amount must be greater than 0.'), None
[9864]527        payment = createObject(u'waeup.StudentOnlinePayment')
528        timestamp = ("%d" % int(time()*10000))[1:]
529        payment.p_id = "p%s" % timestamp
[9868]530        payment.p_category = category
[9864]531        payment.p_item = p_item
532        payment.p_session = p_session
533        payment.p_level = p_level
534        payment.p_current = p_current
535        payment.amount_auth = amount
536        return None, payment
537
[12896]538    def increaseMatricInteger(self, student):
539        """Increase counter for matric numbers.
540        This counter can be a centrally stored attribute or an attribute of
541        faculties, departments or certificates. In the base package the counter
[13124]542        is as an attribute of the site configuration container.
[12896]543        """
544        grok.getSite()['configuration'].next_matric_integer += 1
545        return
546
[11595]547    def constructMatricNumber(self, student):
[12896]548        """Fetch the matric number counter which fits the student and
549        construct the new matric number of the student.
[12902]550        In the base package the counter is returned which is as an attribute
[13124]551        of the site configuration container.
[12896]552        """
[11595]553        next_integer = grok.getSite()['configuration'].next_matric_integer
554        if next_integer == 0:
[11619]555            return _('Matriculation number cannot be set.'), None
556        return None, unicode(next_integer)
[11589]557
558    def setMatricNumber(self, student):
[13124]559        """Set matriculation number of student. If the student's matric number
560        is unset a new matric number is
[12896]561        constructed according to the matriculation number construction rules
[13124]562        defined in the `constructMatricNumber` method. The new matric number is
[12896]563        set, the students catalog updated. The corresponding matric number
564        counter is increased by one.
[11589]565
566        This method is tested but not used in the base package. It can
567        be used in custom packages by adding respective views
[13124]568        and by customizing `increaseMatricInteger` and `constructMatricNumber`
[12896]569        according to the university's matriculation number construction rules.
[11589]570
[12896]571        The method can be disabled by setting the counter to zero.
[11589]572        """
573        if student.matric_number is not None:
574            return _('Matriculation number already set.'), None
[11590]575        if student.certcode is None:
576            return _('No certificate assigned.'), None
[11619]577        error, matric_number = self.constructMatricNumber(student)
578        if error:
579            return error, None
[11589]580        try:
[11592]581            student.matric_number = matric_number
[11589]582        except MatNumNotInSource:
[13224]583            return _('Matriculation number %s exists.' % matric_number), None
[11589]584        notify(grok.ObjectModifiedEvent(student))
[12896]585        self.increaseMatricInteger(student)
[11595]586        return None, matric_number
[11589]587
[7186]588    def getAccommodationDetails(self, student):
[9219]589        """Determine the accommodation data of a student.
[7841]590        """
[7150]591        d = {}
592        d['error'] = u''
[8685]593        hostels = grok.getSite()['hostels']
594        d['booking_session'] = hostels.accommodation_session
595        d['allowed_states'] = hostels.accommodation_states
[8688]596        d['startdate'] = hostels.startdate
597        d['enddate'] = hostels.enddate
598        d['expired'] = hostels.expired
[7150]599        # Determine bed type
600        studycourse = student['studycourse']
[7369]601        certificate = getattr(studycourse,'certificate',None)
[7150]602        entry_session = studycourse.entry_session
603        current_level = studycourse.current_level
[9187]604        if None in (entry_session, current_level, certificate):
605            return d
[7369]606        end_level = certificate.end_level
[9148]607        if current_level == 10:
608            bt = 'pr'
609        elif entry_session == grok.getSite()['hostels'].accommodation_session:
[7150]610            bt = 'fr'
611        elif current_level >= end_level:
612            bt = 'fi'
613        else:
614            bt = 're'
615        if student.sex == 'f':
616            sex = 'female'
617        else:
618            sex = 'male'
619        special_handling = 'regular'
620        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
621        return d
[7019]622
[13247]623    def checkAccommodationRequirements(self, student, acc_details):
624        if acc_details.get('expired', False):
625            startdate = acc_details.get('startdate')
626            enddate = acc_details.get('enddate')
627            if startdate and enddate:
628                tz = getUtility(IKofaUtils).tzinfo
629                startdate = to_timezone(
630                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
631                enddate = to_timezone(
632                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
633                return _("Outside booking period: ${a} - ${b}",
634                         mapping = {'a': startdate, 'b': enddate})
635            else:
636                return _("Outside booking period.")
637        if not acc_details.get('bt'):
638            return _("Your data are incomplete.")
639        if not student.state in acc_details['allowed_states']:
640            return _("You are in the wrong registration state.")
641        if student['studycourse'].current_session != acc_details[
642            'booking_session']:
643            return _('Your current session does not '
644                     'match accommodation session.')
[15306]645        bsession = str(acc_details['booking_session'])
646        if bsession in student['accommodation'].keys() \
647            and not 'booking expired' in \
648            student['accommodation'][bsession].bed_coordinates:
[13247]649            return _('You already booked a bed space in '
650                     'current accommodation session.')
651        return
652
[13457]653    def selectBed(self, available_beds, desired_hostel=None):
654        """Select a bed from a filtered list of available beds.
655        In the base configuration beds are sorted by the sort id
656        of the hostel and the bed number. The first bed found in
657        this sorted list is taken.
[7841]658        """
[13457]659        sorted_beds = sorted(available_beds,
660                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
[15312]661        if desired_hostel and desired_hostel != 'no':
[13457]662            # Filter desired hostel beds
663            filtered_beds = [bed for bed in sorted_beds
664                             if bed.bed_id.startswith(desired_hostel)]
665            if not filtered_beds:
666                return
667            return filtered_beds[0]
668        return sorted_beds[0]
[7150]669
[9981]670    def _admissionText(self, student, portal_language):
[9979]671        inst_name = grok.getSite()['configuration'].name
672        text = trans(_(
673            'This is to inform you that you have been provisionally'
674            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
675            portal_language)
676        return text
677
[10686]678    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
679                                 pre_text=None, post_text=None,):
[9191]680        """Render pdf admission letter.
681        """
682        if student is None:
683            return
684        style = getSampleStyleSheet()
[9949]685        creator = self.getPDFCreator(student)
[9979]686        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9191]687        data = []
688        doc_title = view.label
689        author = '%s (%s)' % (view.request.principal.title,
690                              view.request.principal.id)
[9944]691        footer_text = view.label.split('\n')
692        if len(footer_text) > 1:
693            # We can add a department in first line
694            footer_text = footer_text[1]
695        else:
696            # Only the first line is used for the footer
697            footer_text = footer_text[0]
[9191]698        if getattr(student, 'student_id', None) is not None:
699            footer_text = "%s - %s - " % (student.student_id, footer_text)
700
[10702]701        # Text before student data
[10686]702        if pre_text is None:
703            html = format_html(self._admissionText(student, portal_language))
704        else:
705            html = format_html(pre_text)
[11875]706        if html:
707            data.append(Paragraph(html, NOTE_STYLE))
708            data.append(Spacer(1, 20))
[9191]709
710        # Student data
[11550]711        data.append(render_student_data(view, student,
712                    omit_fields, lang=portal_language,
713                    slipname='admission_slip.pdf'))
[9191]714
[10702]715        # Text after student data
[9191]716        data.append(Spacer(1, 20))
[10686]717        if post_text is None:
718            datelist = student.history.messages[0].split()[0].split('-')
719            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
[10702]720            post_text = trans(_(
[10686]721                'Your Kofa student record was created on ${a}.',
722                mapping = {'a': creation_date}),
723                portal_language)
[10702]724        #html = format_html(post_text)
725        #data.append(Paragraph(html, NOTE_STYLE))
[9191]726
727        # Create pdf stream
728        view.response.setHeader(
729            'Content-Type', 'application/pdf')
730        pdf_stream = creator.create_pdf(
731            data, None, doc_title, author=author, footer=footer_text,
[10702]732            note=post_text)
[9191]733        return pdf_stream
734
[9949]735    def getPDFCreator(self, context):
736        """Get a pdf creator suitable for `context`.
737        The default implementation always returns the default creator.
738        """
739        return getUtility(IPDFCreator)
740
[8257]741    def renderPDF(self, view, filename='slip.pdf', student=None,
[9906]742                  studentview=None,
[10439]743                  tableheader=[], tabledata=[],
[9555]744                  note=None, signatures=None, sigs_in_footer=(),
[10250]745                  show_scans=True, topMargin=1.5,
746                  omit_fields=()):
[14151]747        """Render pdf slips for various pages (also some pages
748        in the applicants module).
[7841]749        """
[10261]750        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9916]751        # XXX: tell what the different parameters mean
[8112]752        style = getSampleStyleSheet()
[9949]753        creator = self.getPDFCreator(student)
[8112]754        data = []
755        doc_title = view.label
756        author = '%s (%s)' % (view.request.principal.title,
757                              view.request.principal.id)
[9913]758        footer_text = view.label.split('\n')
[13304]759        if len(footer_text) > 1:
760            # We can add a department in first line, second line is used
[9913]761            footer_text = footer_text[1]
762        else:
[9917]763            # Only the first line is used for the footer
[9913]764            footer_text = footer_text[0]
[7714]765        if getattr(student, 'student_id', None) is not None:
[7310]766            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]767
[7318]768        # Insert student data table
[7310]769        if student is not None:
[8112]770            bd_translation = trans(_('Base Data'), portal_language)
[9910]771            data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]772            data.append(render_student_data(
[11550]773                studentview, view.context, omit_fields, lang=portal_language,
774                slipname=filename))
[7304]775
[7318]776        # Insert widgets
[9191]777        if view.form_fields:
[9910]778            data.append(Paragraph(view.title, HEADING_STYLE))
[9191]779            separators = getattr(self, 'SEPARATORS_DICT', {})
780            table = creator.getWidgetsTable(
781                view.form_fields, view.context, None, lang=portal_language,
782                separators=separators)
783            data.append(table)
[7318]784
[8112]785        # Insert scanned docs
[9550]786        if show_scans:
787            data.extend(docs_as_flowables(view, portal_language))
[7318]788
[9452]789        # Insert history
[9910]790        if filename.startswith('clearance'):
[9452]791            hist_translation = trans(_('Workflow History'), portal_language)
[9910]792            data.append(Paragraph(hist_translation, HEADING_STYLE))
[9452]793            data.extend(creator.fromStringList(student.history.messages))
794
[10438]795        # Insert content tables (optionally on second page)
[10439]796        if hasattr(view, 'tabletitle'):
797            for i in range(len(view.tabletitle)):
798                if tabledata[i] and tableheader[i]:
799                    #data.append(PageBreak())
800                    #data.append(Spacer(1, 20))
801                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
802                    data.append(Spacer(1, 8))
803                    contenttable = render_table_data(tableheader[i],tabledata[i])
804                    data.append(contenttable)
[7318]805
[9010]806        # Insert signatures
[9965]807        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
808        # do not have a test for the following lines.
[9555]809        if signatures and not sigs_in_footer:
[9010]810            data.append(Spacer(1, 20))
[9966]811            # Render one signature table per signature to
812            # get date and signature in line.
813            for signature in signatures:
814                signaturetables = get_signature_tables(signature)
815                data.append(signaturetables[0])
[9010]816
[7150]817        view.response.setHeader(
818            'Content-Type', 'application/pdf')
[8112]819        try:
820            pdf_stream = creator.create_pdf(
[8257]821                data, None, doc_title, author=author, footer=footer_text,
[9948]822                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
[8112]823        except IOError:
824            view.flash('Error in image file.')
825            return view.redirect(view.url(view.context))
[14256]826        except LayoutError, err:
827            view.flash(
828                'PDF file could not be created. Reportlab error message: %s'
829                % escape(err.message),
830                type="danger")
831            return view.redirect(view.url(view.context))
[8112]832        return pdf_stream
[7620]833
[14915]834    def GPABoundaries(self, faccode=None, depcode=None, certcode=None):
[14914]835        return ((1, 'Fail'),
836               (1.5, 'Pass'),
837               (2.4, '3rd Class'),
838               (3.5, '2nd Class Lower'),
839               (4.5, '2nd Class Upper'),
840               (5, '1st Class'))
[10576]841
[14461]842    def getClassFromCGPA(self, gpa, student):
843        """Determine the class of degree. In some custom packages
844        this class depends on e.g. the entry session of the student. In the
845        base package, it does not.
846        """
[14914]847        if gpa < self.GPABoundaries()[0][0]:
848            return 0, self.GPABoundaries()[0][1]
849        if gpa < self.GPABoundaries()[1][0]:
850            return 1, self.GPABoundaries()[1][1]
851        if gpa < self.GPABoundaries()[2][0]:
852            return 2, self.GPABoundaries()[2][1]
853        if gpa < self.GPABoundaries()[3][0]:
854            return 3, self.GPABoundaries()[3][1]
855        if gpa < self.GPABoundaries()[4][0]:
856            return 4, self.GPABoundaries()[4][1]
857        if gpa <= self.GPABoundaries()[5][0]:
858            return 5, self.GPABoundaries()[5][1]
[15102]859        return
[10445]860
[14159]861    def getDegreeClassNumber(self, level_obj):
862        """Get degree class number (used for SessionResultsPresentation
[14157]863        reports).
864        """
[14410]865        if level_obj.gpa_params[1] == 0:
866            # No credits weighted
867            return 6
[14461]868        return self.getClassFromCGPA(
869            level_obj.cumulative_params[0], level_obj.student)[0]
[14157]870
[15163]871    def _saveTranscriptPDF(self, student, transcript):
872        """Create a transcript PDF file and store it in student folder.
873        """
874        file_store = getUtility(IExtFileStore)
875        file_id = IFileStoreNameChooser(student).chooseName(
876            attr="final_transcript.pdf")
877        file_store.createFile(file_id, StringIO(transcript))
878        return
879
[10250]880    def renderPDFTranscript(self, view, filename='transcript.pdf',
881                  student=None,
882                  studentview=None,
[15163]883                  note=None,
884                  signatures=(),
885                  sigs_in_footer=(),
886                  digital_sigs=(),
[10250]887                  show_scans=True, topMargin=1.5,
888                  omit_fields=(),
[14292]889                  tableheader=None,
[15163]890                  no_passport=False,
891                  save_file=False):
[14583]892        """Render pdf slip of a transcripts.
[10250]893        """
[10261]894        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[10250]895        # XXX: tell what the different parameters mean
896        style = getSampleStyleSheet()
897        creator = self.getPDFCreator(student)
898        data = []
899        doc_title = view.label
900        author = '%s (%s)' % (view.request.principal.title,
901                              view.request.principal.id)
902        footer_text = view.label.split('\n')
903        if len(footer_text) > 2:
904            # We can add a department in first line
905            footer_text = footer_text[1]
906        else:
907            # Only the first line is used for the footer
908            footer_text = footer_text[0]
909        if getattr(student, 'student_id', None) is not None:
910            footer_text = "%s - %s - " % (student.student_id, footer_text)
911
912        # Insert student data table
913        if student is not None:
914            #bd_translation = trans(_('Base Data'), portal_language)
915            #data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]916            data.append(render_student_data(
[11550]917                studentview, view.context,
918                omit_fields, lang=portal_language,
[14292]919                slipname=filename,
920                no_passport=no_passport))
[10250]921
922        transcript_data = view.context.getTranscriptData()
923        levels_data = transcript_data[0]
924
925        contextdata = []
[10261]926        f_label = trans(_('Course of Study:'), portal_language)
[10250]927        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10650]928        f_text = formatted_text(view.context.certificate.longtitle)
[10250]929        f_text = Paragraph(f_text, ENTRY1_STYLE)
930        contextdata.append([f_label,f_text])
931
[10261]932        f_label = trans(_('Faculty:'), portal_language)
[10250]933        f_label = Paragraph(f_label, ENTRY1_STYLE)
934        f_text = formatted_text(
[10650]935            view.context.certificate.__parent__.__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(_('Department:'), portal_language)
[10250]940        f_label = Paragraph(f_label, ENTRY1_STYLE)
941        f_text = formatted_text(
[10650]942            view.context.certificate.__parent__.__parent__.longtitle)
[10250]943        f_text = Paragraph(f_text, ENTRY1_STYLE)
944        contextdata.append([f_label,f_text])
945
[10261]946        f_label = trans(_('Entry Session:'), portal_language)
[10250]947        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]948        f_text = formatted_text(
949            view.session_dict.get(view.context.entry_session))
[10250]950        f_text = Paragraph(f_text, ENTRY1_STYLE)
951        contextdata.append([f_label,f_text])
952
[10261]953        f_label = trans(_('Entry Mode:'), portal_language)
[10250]954        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]955        f_text = formatted_text(view.studymode_dict.get(
956            view.context.entry_mode))
[10250]957        f_text = Paragraph(f_text, ENTRY1_STYLE)
958        contextdata.append([f_label,f_text])
959
[10262]960        f_label = trans(_('Cumulative GPA:'), portal_language)
[10250]961        f_label = Paragraph(f_label, ENTRY1_STYLE)
[14473]962        format_float = getUtility(IKofaUtils).format_float
963        cgpa = format_float(transcript_data[1], 3)
964        f_text = formatted_text('%s (%s)' % (
965            cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
[10250]966        f_text = Paragraph(f_text, ENTRY1_STYLE)
967        contextdata.append([f_label,f_text])
968
969        contexttable = Table(contextdata,style=SLIP_STYLE)
970        data.append(contexttable)
971
972        transcripttables = render_transcript_data(
[10261]973            view, tableheader, levels_data, lang=portal_language)
[10250]974        data.extend(transcripttables)
975
976        # Insert signatures
977        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
978        # do not have a test for the following lines.
979        if signatures and not sigs_in_footer:
980            data.append(Spacer(1, 20))
981            # Render one signature table per signature to
982            # get date and signature in line.
983            for signature in signatures:
984                signaturetables = get_signature_tables(signature)
985                data.append(signaturetables[0])
986
[15163]987        # Insert digital signatures
988        if digital_sigs:
989            data.append(Spacer(1, 20))
990            sigs = digital_sigs.split('\n')
991            for sig in sigs:
992                data.append(Paragraph(sig, NOTE_STYLE))
993
[10250]994        view.response.setHeader(
995            'Content-Type', 'application/pdf')
996        try:
997            pdf_stream = creator.create_pdf(
998                data, None, doc_title, author=author, footer=footer_text,
999                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
1000        except IOError:
[10261]1001            view.flash(_('Error in image file.'))
[10250]1002            return view.redirect(view.url(view.context))
[15163]1003        if save_file:
1004            self._saveTranscriptPDF(student, pdf_stream)
1005            return
[10250]1006        return pdf_stream
1007
[13898]1008    def renderPDFCourseticketsOverview(
[15246]1009            self, view, session, data, lecturers, orientation,
1010            title_length, note):
[14583]1011        """Render pdf slip of course tickets for a lecturer.
1012        """
[13898]1013        filename = 'coursetickets_%s_%s_%s.pdf' % (
1014            view.context.code, session, view.request.principal.id)
1015        session = academic_sessions_vocab.getTerm(session).title
[14702]1016        creator = getUtility(IPDFCreator, name=orientation)
[13898]1017        style = getSampleStyleSheet()
[15197]1018        pdf_data = []
1019        pdf_data += [Paragraph(
[14151]1020            translate(_('<b>Lecturer(s): ${a}</b>',
1021                      mapping = {'a':lecturers})), style["Normal"]),]
1022        pdf_data += [Paragraph(
1023            translate(_('<b>Credits: ${a}</b>',
1024                      mapping = {'a':view.context.credits})), style["Normal"]),]
[14314]1025        # Not used in base package.
1026        if data[1]:
1027            pdf_data += [Paragraph(
[14709]1028                translate(_('<b>${a}</b>',
[14314]1029                    mapping = {'a':data[1][0]})), style["Normal"]),]
1030            pdf_data += [Paragraph(
[14709]1031                translate(_('<b>${a}</b>',
[14708]1032                    mapping = {'a':data[1][1]})), style["Normal"]),]
1033
1034            pdf_data += [Paragraph(
1035                translate(_('<b>Total Students: ${a}</b>',
1036                    mapping = {'a':data[1][2]})), style["Normal"]),]
1037            pdf_data += [Paragraph(
[14319]1038                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
[14708]1039                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
[14319]1040            pdf_data += [Paragraph(
1041                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
[14708]1042                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
[13899]1043        pdf_data.append(Spacer(1, 20))
[15246]1044        colWidths = [None] * len(data[0][0])
1045        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE)]
[15234]1046        # Process title if too long
1047        title = " ".join(view.context.title.split())
1048        ct = textwrap.fill(title, title_length)
1049        ft = title
[15235]1050        if len(textwrap.wrap(title, title_length)) > 1:
[15234]1051            ft = textwrap.wrap(title, title_length)[0] + ' ...'
[15197]1052        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1053            mapping = {'a':ct,
[14151]1054                       'b':view.context.code,
1055                       'd':session}))
[14705]1056        footer_title = translate(_('${a} (${b}) - ${d}',
[15197]1057            mapping = {'a':ft,
[14705]1058                       'b':view.context.code,
1059                       'd':session}))
[13898]1060        author = '%s (%s)' % (view.request.principal.title,
1061                              view.request.principal.id)
1062        view.response.setHeader(
1063            'Content-Type', 'application/pdf')
1064        view.response.setHeader(
1065            'Content-Disposition:', 'attachment; filename="%s' % filename)
1066        pdf_stream = creator.create_pdf(
[15246]1067            pdf_data, None, doc_title, author, footer_title + ' -', note
[13898]1068            )
1069        return pdf_stream
1070
[14584]1071    def warnCreditsOOR(self, studylevel, course=None):
1072        """Return message if credits are out of range. In the base
1073        package only maximum credits is set.
[9830]1074        """
[14582]1075        if course and studylevel.total_credits + course.credits > 50:
[14584]1076            return _('Maximum credits exceeded.')
[14582]1077        elif studylevel.total_credits > 50:
[14584]1078            return _('Maximum credits exceeded.')
[14596]1079        return
[9830]1080
[9987]1081    def getBedCoordinates(self, bedticket):
[13132]1082        """Return descriptive bed coordinates.
[13124]1083        This method can be used to customize the `display_coordinates`
[13132]1084        property method in order to  display a
1085        customary description of the bed space.
[9987]1086        """
1087        return bedticket.bed_coordinates
1088
[11772]1089    def clearance_disabled_message(self, student):
[14583]1090        """Render message if clearance is disabled.
1091        """
[11772]1092        try:
1093            session_config = grok.getSite()[
1094                'configuration'][str(student.current_session)]
1095        except KeyError:
1096            return _('Session configuration object is not available.')
1097        if not session_config.clearance_enabled:
1098            return _('Clearance is disabled for this session.')
1099        return None
1100
[13132]1101    #: A dictionary which maps widget names to headlines. The headline
1102    #: is rendered in forms and on pdf slips above the respective
1103    #: display or input widget. There are no separating headlines
1104    #: in the base package.
[13129]1105    SEPARATORS_DICT = {}
[8410]1106
[13132]1107    #: A tuple containing names of file upload viewlets which are not shown
1108    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1109    #: in the base package. This attribute makes only sense, if intermediate
1110    #: custom packages are being used, like we do for all Nigerian portals.
[10021]1111    SKIP_UPLOAD_VIEWLETS = ()
1112
[13132]1113    #: A tuple containing the names of registration states in which changing of
1114    #: passport pictures is allowed.
[13129]1115    PORTRAIT_CHANGE_STATES = (ADMITTED,)
[10706]1116
[12104]1117    #: A tuple containing all exporter names referring to students or
1118    #: subobjects thereof.
1119    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
1120            'studentstudylevels', 'coursetickets',
[12971]1121            'studentpayments', 'studentunpaidpayments',
[15051]1122            'bedtickets', 'sfpaymentsoverview', 'sessionpaymentsoverview',
[15277]1123            'studylevelsoverview', 'combocard', 'bursary',
1124            'accommodationpayments')
[12104]1125
[12971]1126    #: A tuple containing all exporter names needed for backing
1127    #: up student data
1128    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
1129            'studentstudylevels', 'coursetickets',
1130            'studentpayments', 'bedtickets')
1131
[8410]1132    #: A prefix used when generating new student ids. Each student id will
[13129]1133    #: start with this string. The default is 'K' for Kofa.
[8410]1134    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.