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

Last change on this file since 17176 was 17176, checked in by Henrik Bettermann, 22 months ago

Allow students to book accommodation also if they are in previous sessions (not activated in base package).

  • Property svn:keywords set to Id
File size: 54.6 KB
Line 
1## $Id: utils.py 17176 2022-11-25 11:54:59Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""General helper functions and utilities for the students section.
19"""
20import grok
21import textwrap
22from copy import deepcopy
23from cgi import escape
24from time import time
25from cStringIO import StringIO
26from PyPDF2 import PdfFileMerger, PdfFileReader, PdfFileWriter
27from reportlab.lib import colors
28from reportlab.lib.units import cm
29from reportlab.lib.pagesizes import A4
30from reportlab.lib.styles import getSampleStyleSheet
31from reportlab.platypus import Paragraph, Image, Table, Spacer
32from reportlab.platypus.doctemplate import LayoutError
33from reportlab.platypus.flowables import PageBreak
34from zope.event import notify
35from zope.schema.interfaces import ConstraintNotSatisfied
36from zope.component import getUtility, createObject, queryUtility
37from zope.catalog.interfaces import ICatalog
38from zope.formlib.form import setUpEditWidgets
39from zope.i18n import translate
40from waeup.kofa.interfaces import (
41    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED, GRADUATED,
42    academic_sessions_vocab, IFileStoreNameChooser)
43from waeup.kofa.interfaces import MessageFactory as _
44from waeup.kofa.students.interfaces import IStudentsUtils
45from waeup.kofa.students.workflow import ADMITTED
46from waeup.kofa.students.vocabularies import StudyLevelSource, MatNumNotInSource
47from waeup.kofa.browser.pdf import (
48    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
49    get_signature_tables, get_qrcode)
50from waeup.kofa.browser.interfaces import IPDFCreator
51from waeup.kofa.utils.helpers import to_timezone
52
53SLIP_STYLE = [
54    ('VALIGN',(0,0),(-1,-1),'TOP'),
55    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
56    ]
57
58CONTENT_STYLE = [
59    ('VALIGN',(0,0),(-1,-1),'TOP'),
60    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
61    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
62    #('BACKGROUND',(0,0),(-1,0),colors.black),
63    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
64    ('BOX', (0,0), (-1,-1), 1, colors.black),
65    ]
66
67FONT_SIZE = 10
68FONT_COLOR = 'black'
69
70def trans(text, lang):
71    # shortcut
72    return translate(text, 'waeup.kofa', target_language=lang)
73
74def formatted_text(text, color=FONT_COLOR, lang='en'):
75    """Turn `text`, `color` and `size` into an HTML snippet.
76
77    The snippet is suitable for use with reportlab and generating PDFs.
78    Wraps the `text` into a ``<font>`` tag with passed attributes.
79
80    Also non-strings are converted. Raw strings are expected to be
81    utf-8 encoded (usually the case for widgets etc.).
82
83    Finally, a br tag is added if widgets contain div tags
84    which are not supported by reportlab.
85
86    The returned snippet is unicode type.
87    """
88    if not isinstance(text, unicode):
89        if isinstance(text, basestring):
90            text = text.decode('utf-8')
91        else:
92            text = unicode(text)
93    if text == 'None':
94        text = ''
95    # Very long matriculation numbers need to be wrapped
96    if text.find(' ') == -1 and len(text.split('/')) > 6:
97        text = '/'.join(text.split('/')[:5]) + \
98            '/ ' + '/'.join(text.split('/')[5:])
99    # Mainly for boolean values we need our customized
100    # localisation of the zope domain
101    text = translate(text, 'zope', target_language=lang)
102    text = text.replace('</div>', '<br /></div>')
103    tag1 = u'<font color="%s">' % (color)
104    return tag1 + u'%s</font>' % text
105
106def generate_student_id():
107    students = grok.getSite()['students']
108    new_id = students.unique_student_id
109    return new_id
110
111def set_up_widgets(view, ignore_request=False):
112    view.adapters = {}
113    view.widgets = setUpEditWidgets(
114        view.form_fields, view.prefix, view.context, view.request,
115        adapters=view.adapters, for_display=True,
116        ignore_request=ignore_request
117        )
118
119def render_student_data(studentview, context, omit_fields=(),
120                        lang='en', slipname=None, no_passport=False):
121    """Render student table for an existing frame.
122    """
123    width, height = A4
124    set_up_widgets(studentview, ignore_request=True)
125    data_left = []
126    data_middle = []
127    style = getSampleStyleSheet()
128    img = getUtility(IExtFileStore).getFileByContext(
129        studentview.context, attr='passport.jpg')
130    if img is None:
131        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
132        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
133    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
134    data_left.append([doc_img])
135    # Uniben wants a second picture. We add this picture here.
136    img2 = getUtility(IExtFileStore).getFileByContext(
137        studentview.context, attr='passport2.jpg')
138    if img2 is not None:
139        doc_img2 = Image(img2.name, width=4*cm, height=4*cm, kind='bound')
140        data_left.append([doc_img2])
141    #data.append([Spacer(1, 12)])
142
143    f_label = trans(_('Name:'), lang)
144    f_label = Paragraph(f_label, ENTRY1_STYLE)
145    f_text = formatted_text(studentview.context.display_fullname)
146    f_text = Paragraph(f_text, ENTRY1_STYLE)
147    data_middle.append([f_label,f_text])
148
149    for widget in studentview.widgets:
150        if 'name' in widget.name:
151            continue
152        f_label = translate(
153            widget.label.strip(), 'waeup.kofa',
154            target_language=lang)
155        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
156        f_text = formatted_text(widget(), lang=lang)
157        f_text = Paragraph(f_text, ENTRY1_STYLE)
158        data_middle.append([f_label,f_text])
159    if not 'date_of_birth' in omit_fields:
160        f_label = trans(_('Date of Birth:'), lang)
161        f_label = Paragraph(f_label, ENTRY1_STYLE)
162        date_of_birth = studentview.context.date_of_birth
163        tz = getUtility(IKofaUtils).tzinfo
164        date_of_birth = to_timezone(date_of_birth, tz)
165        if date_of_birth is not None:
166            date_of_birth = date_of_birth.strftime("%d/%m/%Y")
167        f_text = formatted_text(date_of_birth)
168        f_text = Paragraph(f_text, ENTRY1_STYLE)
169        data_middle.append([f_label,f_text])
170    if getattr(studentview.context, 'certcode', None):
171        if not 'certificate' in omit_fields:
172            f_label = trans(_('Study Course:'), lang)
173            f_label = Paragraph(f_label, ENTRY1_STYLE)
174            f_text = formatted_text(
175                studentview.context['studycourse'].certificate.longtitle)
176            f_text = Paragraph(f_text, ENTRY1_STYLE)
177            data_middle.append([f_label,f_text])
178        if not 'department' in omit_fields:
179            f_label = trans(_('Department:'), lang)
180            f_label = Paragraph(f_label, ENTRY1_STYLE)
181            f_text = formatted_text(
182                studentview.context[
183                'studycourse'].certificate.__parent__.__parent__.longtitle,
184                )
185            f_text = Paragraph(f_text, ENTRY1_STYLE)
186            data_middle.append([f_label,f_text])
187        if not 'faculty' in omit_fields:
188            f_label = trans(_('Faculty:'), lang)
189            f_label = Paragraph(f_label, ENTRY1_STYLE)
190            f_text = formatted_text(
191                studentview.context[
192                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
193                )
194            f_text = Paragraph(f_text, ENTRY1_STYLE)
195            data_middle.append([f_label,f_text])
196        if not 'current_mode' in omit_fields:
197            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
198            sm = studymodes_dict[studentview.context.current_mode]
199            f_label = trans(_('Study Mode:'), lang)
200            f_label = Paragraph(f_label, ENTRY1_STYLE)
201            f_text = formatted_text(sm)
202            f_text = Paragraph(f_text, ENTRY1_STYLE)
203            data_middle.append([f_label,f_text])
204        if not 'entry_session' in omit_fields:
205            f_label = trans(_('Entry Session:'), lang)
206            f_label = Paragraph(f_label, ENTRY1_STYLE)
207            entry_session = studentview.context.entry_session
208            try:
209                entry_session = academic_sessions_vocab.getTerm(
210                    entry_session).title
211            except LookupError:
212                entry_session = _('void')
213            f_text = formatted_text(entry_session)
214            f_text = Paragraph(f_text, ENTRY1_STYLE)
215            data_middle.append([f_label,f_text])
216        # Requested by Uniben, does not really make sense
217        if not 'current_level' in omit_fields:
218            f_label = trans(_('Current Level:'), lang)
219            f_label = Paragraph(f_label, ENTRY1_STYLE)
220            current_level = studentview.context['studycourse'].current_level
221            studylevelsource = StudyLevelSource().factory
222            current_level = studylevelsource.getTitle(
223                studentview.context, current_level)
224            f_text = formatted_text(current_level)
225            f_text = Paragraph(f_text, ENTRY1_STYLE)
226            data_middle.append([f_label,f_text])
227
228    if no_passport:
229        table = Table(data_middle,style=SLIP_STYLE)
230        table.hAlign = 'LEFT'
231        return table
232
233    # append QR code to the right
234    if slipname:
235        url = studentview.url(context, slipname)
236        data_right = [[get_qrcode(url, width=70.0)]]
237        table_right = Table(data_right,style=SLIP_STYLE)
238    else:
239        table_right = None
240
241    table_left = Table(data_left,style=SLIP_STYLE)
242    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
243    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
244    return table
245
246def render_table_data(tableheader, tabledata, lang='en'):
247    """Render children table for an existing frame.
248    """
249    data = []
250    #data.append([Spacer(1, 12)])
251    line = []
252    style = getSampleStyleSheet()
253    for element in tableheader:
254        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
255        field = Paragraph(field, style["Normal"])
256        line.append(field)
257    data.append(line)
258    for ticket in tabledata:
259        line = []
260        for element in tableheader:
261              field = formatted_text(getattr(ticket,element[1],u' '))
262              field = Paragraph(field, style["Normal"])
263              line.append(field)
264        data.append(line)
265    table = Table(data,colWidths=[
266        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
267    return table
268
269def render_transcript_data(view, tableheader, levels_data, lang='en'):
270    """Render children table for an existing frame.
271    """
272    data = []
273    style = getSampleStyleSheet()
274    format_float = getUtility(IKofaUtils).format_float
275    for level in levels_data:
276        level_obj = level['level']
277        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
278        if not tickets:
279            continue
280        headerline = []
281        tabledata = []
282        if 'evel' in view.level_dict.get('ticket.level', str(level_obj.level)):
283            subheader = '%s %s, %s' % (
284                trans(_('Session'), lang),
285                view.session_dict[level_obj.level_session],
286                view.level_dict.get('ticket.level', str(level_obj.level)))
287        else:
288            subheader = '%s %s, %s %s' % (
289                trans(_('Session'), lang),
290                view.session_dict[level_obj.level_session],
291                trans(_('Level'), lang),
292                view.level_dict.get(level_obj.level, str(level_obj.level)))
293        data.append(Paragraph(subheader, HEADING_STYLE))
294        for element in tableheader:
295            field = '<strong>%s</strong>' % formatted_text(element[0])
296            field = Paragraph(field, style["Normal"])
297            headerline.append(field)
298        tabledata.append(headerline)
299        for ticket in tickets:
300            ticketline = []
301            for element in tableheader:
302                  field = formatted_text(getattr(ticket,element[1],u' '))
303                  field = Paragraph(field, style["Normal"])
304                  ticketline.append(field)
305            tabledata.append(ticketline)
306        table = Table(tabledata,colWidths=[
307            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
308        data.append(table)
309        sgpa = format_float(level['sgpa'], 2)
310        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), sgpa)
311        #sgpa = '%s: %.2f' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
312        data.append(Paragraph(sgpa, style["Normal"]))
313        if getattr(level_obj, 'transcript_remark', None):
314            remark = '%s: %s' % (
315                trans('Transcript Remark', lang),
316                getattr(level_obj, 'transcript_remark'))
317            data.append(Paragraph(remark, style["Normal"]))
318    return data
319
320def docs_as_flowables(view, lang='en'):
321    """Create reportlab flowables out of scanned docs.
322    """
323    # XXX: fix circular import problem
324    from waeup.kofa.browser.fileviewlets import FileManager
325    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
326    style = getSampleStyleSheet()
327    data = []
328
329    # Collect viewlets
330    fm = FileManager(view.context, view.request, view)
331    fm.update()
332    if fm.viewlets:
333        sc_translation = trans(_('Scanned Documents'), lang)
334        data.append(Paragraph(sc_translation, HEADING_STYLE))
335        # Insert list of scanned documents
336        table_data = []
337        for viewlet in fm.viewlets:
338            if viewlet.file_exists:
339                # Show viewlet only if file exists
340                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
341                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
342                    view.context, attr=viewlet.download_name), 'name', None)
343                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
344                if img_path is None:
345                    pass
346                elif not img_path[-4:] in ('.jpg', '.JPG'):
347                    # reportlab requires jpg images, I think.
348                    f_text = Paragraph('%s (not displayable)' % (
349                        viewlet.title,), ENTRY1_STYLE)
350                else:
351                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
352                table_data.append([f_label, f_text])
353        if table_data:
354            # safety belt; empty tables lead to problems.
355            data.append(Table(table_data, style=SLIP_STYLE))
356    return data
357
358class StudentsUtils(grok.GlobalUtility):
359    """A collection of methods subject to customization.
360    """
361    grok.implements(IStudentsUtils)
362
363    def getReturningData(self, student):
364        """ Define what happens after school fee payment
365        depending on the student's senate verdict.
366        In the base configuration current level is always increased
367        by 100 no matter which verdict has been assigned.
368        """
369        new_level = student['studycourse'].current_level + 100
370        new_session = student['studycourse'].current_session + 1
371        return new_session, new_level
372
373    def setReturningData(self, student):
374        """ Define what happens after school fee payment
375        depending on the student's senate verdict.
376        This method follows the same algorithm as `getReturningData` but
377        it also sets the new values.
378        """
379        new_session, new_level = self.getReturningData(student)
380        try:
381            student['studycourse'].current_level = new_level
382        except ConstraintNotSatisfied:
383            # Do not change level if level exceeds the
384            # certificate's end_level.
385            pass
386        student['studycourse'].current_session = new_session
387        verdict = student['studycourse'].current_verdict
388        student['studycourse'].current_verdict = '0'
389        student['studycourse'].previous_verdict = verdict
390        return
391
392    def _getSessionConfiguration(self, session):
393        try:
394            return grok.getSite()['configuration'][str(session)]
395        except KeyError:
396            return None
397
398    def _isPaymentDisabled(self, p_session, category, student):
399        academic_session = self._getSessionConfiguration(p_session)
400        if category == 'schoolfee' and \
401            'sf_all' in academic_session.payment_disabled:
402            return True
403        return False
404
405    def samePaymentMade(self, student, category, p_item, p_session):
406        for key in student['payments'].keys():
407            ticket = student['payments'][key]
408            if ticket.p_state == 'paid' and\
409               ticket.p_category == category and \
410               ticket.p_item != 'Balance' and \
411               ticket.p_item == p_item and \
412               ticket.p_session == p_session:
413                  return True
414        return False
415
416    def setPaymentDetails(self, category, student,
417            previous_session=None, previous_level=None, combi=[]):
418        """Create a payment ticket and set the payment data of a
419        student for the payment category specified.
420        """
421        p_item = u''
422        amount = 0.0
423        if previous_session:
424            if previous_session < student['studycourse'].entry_session:
425                return _('The previous session must not fall below '
426                         'your entry session.'), None
427            if category == 'schoolfee':
428                # School fee is always paid for the following session
429                if previous_session > student['studycourse'].current_session:
430                    return _('This is not a previous session.'), None
431            else:
432                if previous_session > student['studycourse'].current_session - 1:
433                    return _('This is not a previous session.'), None
434            p_session = previous_session
435            p_level = previous_level
436            p_current = False
437        else:
438            p_session = student['studycourse'].current_session
439            p_level = student['studycourse'].current_level
440            p_current = True
441        academic_session = self._getSessionConfiguration(p_session)
442        if academic_session == None:
443            return _(u'Session configuration object is not available.'), None
444        # Determine fee.
445        if category == 'schoolfee':
446            try:
447                certificate = student['studycourse'].certificate
448                p_item = certificate.code
449            except (AttributeError, TypeError):
450                return _('Study course data are incomplete.'), None
451            if previous_session:
452                # Students can pay for previous sessions in all
453                # workflow states.  Fresh students are excluded by the
454                # update method of the PreviousPaymentAddFormPage.
455                if previous_level == 100:
456                    amount = getattr(certificate, 'school_fee_1', 0.0)
457                else:
458                    amount = getattr(certificate, 'school_fee_2', 0.0)
459            else:
460                if student.state == CLEARED:
461                    amount = getattr(certificate, 'school_fee_1', 0.0)
462                elif student.state == RETURNING:
463                    # In case of returning school fee payment the
464                    # payment session and level contain the values of
465                    # the session the student has paid for. Payment
466                    # session is always next session.
467                    p_session, p_level = self.getReturningData(student)
468                    academic_session = self._getSessionConfiguration(p_session)
469                    if academic_session == None:
470                        return _(
471                            u'Session configuration object is not available.'
472                            ), None
473                    amount = getattr(certificate, 'school_fee_2', 0.0)
474                elif student.is_postgrad and student.state == PAID:
475                    # Returning postgraduate students also pay for the
476                    # next session but their level always remains the
477                    # same.
478                    p_session += 1
479                    academic_session = self._getSessionConfiguration(p_session)
480                    if academic_session == None:
481                        return _(
482                            u'Session configuration object is not available.'
483                            ), None
484                    amount = getattr(certificate, 'school_fee_2', 0.0)
485        elif category == 'clearance':
486            try:
487                p_item = student['studycourse'].certificate.code
488            except (AttributeError, TypeError):
489                return _('Study course data are incomplete.'), None
490            amount = academic_session.clearance_fee
491        elif category == 'bed_allocation':
492            p_item = self.getAccommodationDetails(student)['bt']
493            amount = academic_session.booking_fee
494        elif category == 'hostel_maintenance':
495            amount = 0.0
496            bedticket = student['accommodation'].get(
497                str(student.current_session), None)
498            if bedticket is not None and bedticket.bed is not None:
499                p_item = bedticket.bed_coordinates
500                if bedticket.bed.__parent__.maint_fee > 0:
501                    amount = bedticket.bed.__parent__.maint_fee
502                else:
503                    # fallback
504                    amount = academic_session.maint_fee
505            else:
506                return _(u'No bed allocated.'), None
507        elif category == 'combi' and combi:
508            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
509            for cat in combi:
510                fee_name = cat + '_fee'
511                cat_amount = getattr(academic_session, fee_name, 0.0)
512                if not cat_amount:
513                    return _('%s undefined.' % categories[cat]), None
514                amount += cat_amount
515                p_item += u'%s + ' % categories[cat]
516            p_item = p_item.strip(' + ')
517        else:
518            fee_name = category + '_fee'
519            amount = getattr(academic_session, fee_name, 0.0)
520        if amount in (0.0, None):
521            return _('Amount could not be determined.'), None
522        if self.samePaymentMade(student, category, p_item, p_session):
523            return _('This type of payment has already been made.'), None
524        if self._isPaymentDisabled(p_session, category, student):
525            return _('This category of payments has been disabled.'), None
526        payment = createObject(u'waeup.StudentOnlinePayment')
527        timestamp = ("%d" % int(time()*10000))[1:]
528        payment.p_id = "p%s" % timestamp
529        payment.p_category = category
530        payment.p_item = p_item
531        payment.p_session = p_session
532        payment.p_level = p_level
533        payment.p_current = p_current
534        payment.amount_auth = amount
535        payment.p_combi = combi
536        return None, payment
537
538    def setBalanceDetails(self, category, student,
539            balance_session, balance_level, balance_amount):
540        """Create a balance payment ticket and set the payment data
541        as selected by the student.
542        """
543        p_item = u'Balance'
544        p_session = balance_session
545        p_level = balance_level
546        p_current = False
547        amount = balance_amount
548        academic_session = self._getSessionConfiguration(p_session)
549        if academic_session == None:
550            return _(u'Session configuration object is not available.'), None
551        if amount in (0.0, None) or amount < 0:
552            return _('Amount must be greater than 0.'), None
553        payment = createObject(u'waeup.StudentOnlinePayment')
554        timestamp = ("%d" % int(time()*10000))[1:]
555        payment.p_id = "p%s" % timestamp
556        payment.p_category = category
557        payment.p_item = p_item
558        payment.p_session = p_session
559        payment.p_level = p_level
560        payment.p_current = p_current
561        payment.amount_auth = amount
562        return None, payment
563
564    def increaseMatricInteger(self, student):
565        """Increase counter for matric numbers.
566        This counter can be a centrally stored attribute or an attribute of
567        faculties, departments or certificates. In the base package the counter
568        is as an attribute of the site configuration container.
569        """
570        grok.getSite()['configuration'].next_matric_integer += 1
571        return
572
573    def constructMatricNumber(self, student):
574        """Fetch the matric number counter which fits the student and
575        construct the new matric number of the student.
576        In the base package the counter is returned which is as an attribute
577        of the site configuration container.
578        """
579        next_integer = grok.getSite()['configuration'].next_matric_integer
580        if next_integer == 0:
581            return _('Matriculation number cannot be set.'), None
582        return None, unicode(next_integer)
583
584    def setMatricNumber(self, student):
585        """Set matriculation number of student. If the student's matric number
586        is unset a new matric number is
587        constructed according to the matriculation number construction rules
588        defined in the `constructMatricNumber` method. The new matric number is
589        set, the students catalog updated. The corresponding matric number
590        counter is increased by one.
591
592        This method is tested but not used in the base package. It can
593        be used in custom packages by adding respective views
594        and by customizing `increaseMatricInteger` and `constructMatricNumber`
595        according to the university's matriculation number construction rules.
596
597        The method can be disabled by setting the counter to zero.
598        """
599        if student.matric_number is not None:
600            return _('Matriculation number already set.'), None
601        if student.certcode is None:
602            return _('No certificate assigned.'), None
603        error, matric_number = self.constructMatricNumber(student)
604        if error:
605            return error, None
606        try:
607            student.matric_number = matric_number
608        except MatNumNotInSource:
609            return _('Matriculation number %s exists.' % matric_number), None
610        notify(grok.ObjectModifiedEvent(student))
611        self.increaseMatricInteger(student)
612        return None, matric_number
613
614    def allowPortraitChange(self, student):
615        """Check if student is allowed to change the portrait file.
616        """
617        if student.state not in self.PORTRAIT_CHANGE_STATES:
618            return False
619        return True
620
621    def getAccommodationDetails(self, student):
622        """Determine the accommodation data of a student.
623        """
624        d = {}
625        d['error'] = u''
626        hostels = grok.getSite()['hostels']
627        d['booking_session'] = hostels.accommodation_session
628        d['allowed_states'] = hostels.accommodation_states
629        d['startdate'] = hostels.startdate
630        d['enddate'] = hostels.enddate
631        d['expired'] = hostels.expired
632        # Determine bed type
633        studycourse = student['studycourse']
634        certificate = getattr(studycourse,'certificate',None)
635        entry_session = studycourse.entry_session
636        current_level = studycourse.current_level
637        if None in (entry_session, current_level, certificate):
638            return d
639        end_level = certificate.end_level
640        if current_level == 10:
641            bt = 'pr'
642        elif entry_session == grok.getSite()['hostels'].accommodation_session:
643            bt = 'fr'
644        elif current_level >= end_level:
645            bt = 'fi'
646        else:
647            bt = 're'
648        if student.sex == 'f':
649            sex = 'female'
650        else:
651            sex = 'male'
652        special_handling = 'regular'
653        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
654        return d
655
656    ACCOMMODATION_SPAN = 0
657
658    def checkAccommodationRequirements(self, student, acc_details):
659        if acc_details.get('expired', False):
660            startdate = acc_details.get('startdate')
661            enddate = acc_details.get('enddate')
662            if startdate and enddate:
663                tz = getUtility(IKofaUtils).tzinfo
664                startdate = to_timezone(
665                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
666                enddate = to_timezone(
667                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
668                return _("Outside booking period: ${a} - ${b}",
669                         mapping = {'a': startdate, 'b': enddate})
670            else:
671                return _("Outside booking period.")
672        if not acc_details.get('bt'):
673            return _("Your data are incomplete.")
674        if not student.state in acc_details['allowed_states']:
675            return _("You are in the wrong registration state.")
676        if  acc_details['booking_session'] - student[
677            'studycourse'].current_session > self.ACCOMMODATION_SPAN:
678            return _('Your current session does not allow ' + \
679                    'to book accommodation.')
680        bsession = str(acc_details['booking_session'])
681        if bsession in student['accommodation'].keys() \
682            and not 'booking expired' in \
683            student['accommodation'][bsession].bed_coordinates:
684            return _('You already booked a bed space in '
685                     'current accommodation session.')
686        return
687
688    def selectBed(self, available_beds):
689        """Select a bed from a filtered list of available beds.
690        In the base configuration beds are sorted by the sort id
691        of the hostel and the bed number. The first bed found in
692        this sorted list is taken.
693        """
694        sorted_beds = sorted(available_beds,
695                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
696        return sorted_beds[0]
697
698    def GPABoundaries(self, faccode=None, depcode=None, certcode=None):
699        return ((1, 'Fail'),
700               (1.5, 'Pass'),
701               (2.4, '3rd Class'),
702               (3.5, '2nd Class Lower'),
703               (4.5, '2nd Class Upper'),
704               (5, '1st Class'))
705
706    def getClassFromCGPA(self, gpa, student):
707        """Determine the class of degree. In some custom packages
708        this class depends on e.g. the entry session of the student. In the
709        base package, it does not.
710        """
711        if gpa < self.GPABoundaries()[0][0]:
712            return 0, self.GPABoundaries()[0][1]
713        if gpa < self.GPABoundaries()[1][0]:
714            return 1, self.GPABoundaries()[1][1]
715        if gpa < self.GPABoundaries()[2][0]:
716            return 2, self.GPABoundaries()[2][1]
717        if gpa < self.GPABoundaries()[3][0]:
718            return 3, self.GPABoundaries()[3][1]
719        if gpa < self.GPABoundaries()[4][0]:
720            return 4, self.GPABoundaries()[4][1]
721        if gpa <= self.GPABoundaries()[5][0]:
722            return 5, self.GPABoundaries()[5][1]
723        return
724
725    def getDegreeClassNumber(self, level_obj):
726        """Get degree class number (used for SessionResultsPresentation
727        reports).
728        """
729        if level_obj.gpa_params[1] == 0:
730            # No credits weighted
731            return 6
732        return self.getClassFromCGPA(
733            level_obj.cumulative_params[0], level_obj.student)[0]
734
735    def _saveTranscriptPDF(self, student, transcript):
736        """Create a transcript PDF file and store it in student folder.
737        """
738        file_store = getUtility(IExtFileStore)
739        file_id = IFileStoreNameChooser(student).chooseName(
740            attr="final_transcript.pdf")
741        file_store.createFile(file_id, StringIO(transcript))
742        return
743
744    def warnCreditsOOR(self, studylevel, course=None):
745        """Return message if credits are out of range. In the base
746        package only maximum credits is set.
747        """
748        if course and studylevel.total_credits + course.credits > 50:
749            return _('Maximum credits exceeded.')
750        elif studylevel.total_credits > 50:
751            return _('Maximum credits exceeded.')
752        return
753
754    def warnCourseAlreadyPassed(self, studylevel, course):
755        """Return message if course has already been passed at
756        previous levels.
757        """
758        for slevel in studylevel.__parent__.values():
759            for cticket in slevel.values():
760                if cticket.code == course.code \
761                    and cticket.total_score >= cticket.passmark:
762                    return _('Course has already been passed at previous level.')
763        return False
764
765    def getBedCoordinates(self, bedticket):
766        """Return descriptive bed coordinates.
767        This method can be used to customize the `display_coordinates`
768        property method in order to  display a
769        customary description of the bed space.
770        """
771        return bedticket.bed_coordinates
772
773    def clearance_disabled_message(self, student):
774        """Render message if clearance is disabled.
775        """
776        try:
777            session_config = grok.getSite()[
778                'configuration'][str(student.current_session)]
779        except KeyError:
780            return _('Session configuration object is not available.')
781        if not session_config.clearance_enabled:
782            return _('Clearance is disabled for this session.')
783        return None
784
785    def getPDFCreator(self, context):
786        """Get a pdf creator suitable for `context`.
787        The default implementation always returns the default creator.
788        """
789        return getUtility(IPDFCreator)
790
791    def _mergeFiles(self, mergefiles, watermark, pdf_stream):
792        merger = PdfFileMerger()
793        merger.append(StringIO(pdf_stream))
794        for file in mergefiles:
795            if watermark:
796                # Pass through all pages of each file
797                # and merge with watermark page. Paint
798                # watermark first to make it transparent.
799                marked_file = PdfFileWriter()
800                orig_file = PdfFileReader(file)
801                num_pages = orig_file.getNumPages()
802                for num in range(num_pages):
803                    watermark_file = PdfFileReader(watermark)
804                    page = watermark_file.getPage(0)
805                    page.mergePage(orig_file.getPage(num))
806                    marked_file.addPage(page)
807                # Save into a file-like object
808                tmp1 = StringIO()
809                marked_file.write(tmp1)
810                # Append the file-like object
811                merger.append(tmp1)
812            else:
813                # Just append the file object
814                merger.append(file)
815        # Save into a file-like object
816        tmp2 = StringIO()
817        merger.write(tmp2)
818        return tmp2.getvalue()
819
820    def renderPDF(self, view, filename='slip.pdf', student=None,
821                  studentview=None,
822                  tableheader=[], tabledata=[],
823                  note=None, signatures=None, sigs_in_footer=(),
824                  show_scans=True, topMargin=1.5,
825                  omit_fields=(),
826                  mergefiles=None, watermark=None,
827                  pagebreak=False):
828        """Render pdf slips for various pages (also some pages
829        in the applicants module).
830        """
831        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
832        # XXX: tell what the different parameters mean
833        style = getSampleStyleSheet()
834        creator = self.getPDFCreator(student)
835        data = []
836        doc_title = view.label
837        author = '%s (%s)' % (view.request.principal.title,
838                              view.request.principal.id)
839        footer_text = view.label.split('\n')
840        if len(footer_text) > 2:
841            # We can add a department in first line, second line is used
842            footer_text = footer_text[1]
843        else:
844            # Only the first line is used for the footer
845            footer_text = footer_text[0]
846        if getattr(student, 'student_id', None) is not None:
847            footer_text = "%s - %s - " % (student.student_id, footer_text)
848
849        # Insert student data table
850        if student is not None:
851            if view.form_fields:
852                bd_translation = trans(_('Base Data'), portal_language)
853                data.append(Paragraph(bd_translation, HEADING_STYLE))
854            data.append(render_student_data(
855                studentview, view.context, omit_fields, lang=portal_language,
856                slipname=filename))
857
858        # Insert widgets
859        if view.form_fields:
860            data.append(Paragraph(view.title, HEADING_STYLE))
861            separators = getattr(self, 'SEPARATORS_DICT', {})
862            table = creator.getWidgetsTable(
863                view.form_fields, view.context, None, lang=portal_language,
864                separators=separators)
865            data.append(table)
866
867        # Insert scanned docs
868        if show_scans:
869            data.extend(docs_as_flowables(view, portal_language))
870
871        # Insert history
872        if filename == 'clearance_slip.pdf':
873            hist_translation = trans(_('Workflow History'), portal_language)
874            data.append(Paragraph(hist_translation, HEADING_STYLE))
875            data.extend(creator.fromStringList(student.history.messages))
876
877        # Insert content tables (optionally on second page)
878        if hasattr(view, 'tabletitle'):
879            for i in range(len(view.tabletitle)):
880                if tabledata[i] and tableheader[i]:
881                    tabletitle = view.tabletitle[i]
882                    if tabletitle.startswith('_PB_'):
883                        data.append(PageBreak())
884                        tabletitle = view.tabletitle[i].strip('_PB_')
885                    #data.append(Spacer(1, 20))
886                    data.append(Paragraph(tabletitle, HEADING_STYLE))
887                    data.append(Spacer(1, 8))
888                    contenttable = render_table_data(tableheader[i],tabledata[i])
889                    data.append(contenttable)
890
891        # Insert signatures
892        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
893        # do not have a test for the following lines.
894        if signatures and not sigs_in_footer:
895            # Insert page break if necessary, else some space
896            if pagebreak:
897                data.append(PageBreak())
898            else:
899                data.append(Spacer(1, 20))
900            # Render one signature table per signature to
901            # get date and signature in line.
902            for signature in signatures:
903                signaturetables = get_signature_tables(signature)
904                data.append(signaturetables[0])
905
906        view.response.setHeader(
907            'Content-Type', 'application/pdf')
908        try:
909            pdf_stream = creator.create_pdf(
910                data, None, doc_title, author=author, footer=footer_text,
911                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
912        except IOError:
913            view.flash('Error in image file.')
914            return view.redirect(view.url(view.context))
915        except LayoutError, err:
916            view.flash(
917                'PDF file could not be created. Reportlab error message: %s'
918                % escape(err.message),
919                type="danger")
920            return view.redirect(view.url(view.context))
921        if mergefiles:
922            return self._mergeFiles(mergefiles, watermark, pdf_stream)
923        return pdf_stream
924
925    def _admissionText(self, student, portal_language):
926        inst_name = grok.getSite()['configuration'].name
927        text = trans(_(
928            'This is to inform you that you have been provisionally'
929            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
930            portal_language)
931        return text
932
933    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
934                                 pre_text=None, post_text=None,
935                                 topMargin = 1.5,
936                                 letterhead_path=None,
937                                 mergefiles=None, watermark=None):
938        """Render pdf admission letter.
939        """
940        if student is None:
941            return
942        style = getSampleStyleSheet()
943        if letterhead_path:
944            creator = getUtility(IPDFCreator, name='letter')
945        else:
946            creator = self.getPDFCreator(student)
947        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
948        data = []
949        doc_title = view.label
950        author = '%s (%s)' % (view.request.principal.title,
951                              view.request.principal.id)
952        footer_text = view.label.split('\n')
953        if len(footer_text) > 2:
954            # We can add a department in first line
955            footer_text = footer_text[1]
956        else:
957            # Only the first line is used for the footer
958            footer_text = footer_text[0]
959        if getattr(student, 'student_id', None) is not None:
960            footer_text = "%s - %s - " % (student.student_id, footer_text)
961
962        # Text before student data
963        if pre_text is None:
964            html = format_html(self._admissionText(student, portal_language))
965        else:
966            html = format_html(pre_text)
967        if html:
968            data.append(Paragraph(html, NOTE_STYLE))
969            data.append(Spacer(1, 20))
970
971        # Student data
972        data.append(render_student_data(view, student,
973                    omit_fields, lang=portal_language,
974                    slipname='admission_slip.pdf'))
975
976        # Text after student data
977        data.append(Spacer(1, 20))
978        if post_text is None:
979            datelist = student.history.messages[0].split()[0].split('-')
980            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
981            post_text = trans(_(
982                'Your Kofa student record was created on ${a}.',
983                mapping = {'a': creation_date}),
984                portal_language)
985        #html = format_html(post_text)
986        #data.append(Paragraph(html, NOTE_STYLE))
987
988        # Create pdf stream
989        view.response.setHeader(
990            'Content-Type', 'application/pdf')
991        pdf_stream = creator.create_pdf(
992            data, None, doc_title, author=author, footer=footer_text,
993            note=post_text, topMargin=topMargin,
994            letterhead_path=letterhead_path)
995        if mergefiles:
996            return self._mergeFiles(mergefiles, watermark, pdf_stream)
997        return pdf_stream
998
999    def renderPDFTranscript(self, view, filename='transcript.pdf',
1000                  student=None,
1001                  studentview=None,
1002                  note=None,
1003                  signatures=(),
1004                  sigs_in_footer=(),
1005                  digital_sigs=(),
1006                  show_scans=True, topMargin=1.5,
1007                  omit_fields=(),
1008                  tableheader=None,
1009                  no_passport=False,
1010                  save_file=False):
1011        """Render pdf slip of a transcripts.
1012        """
1013        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1014        # XXX: tell what the different parameters mean
1015        style = getSampleStyleSheet()
1016        creator = self.getPDFCreator(student)
1017        data = []
1018        doc_title = view.label
1019        author = '%s (%s)' % (view.request.principal.title,
1020                              view.request.principal.id)
1021        footer_text = view.label.split('\n')
1022        if len(footer_text) > 2:
1023            # We can add a department in first line
1024            footer_text = footer_text[1]
1025        else:
1026            # Only the first line is used for the footer
1027            footer_text = footer_text[0]
1028        if getattr(student, 'student_id', None) is not None:
1029            footer_text = "%s - %s - " % (student.student_id, footer_text)
1030
1031        # Insert student data table
1032        if student is not None:
1033            #bd_translation = trans(_('Base Data'), portal_language)
1034            #data.append(Paragraph(bd_translation, HEADING_STYLE))
1035            data.append(render_student_data(
1036                studentview, view.context,
1037                omit_fields, lang=portal_language,
1038                slipname=filename,
1039                no_passport=no_passport))
1040
1041        transcript_data = view.context.getTranscriptData()
1042        levels_data = transcript_data[0]
1043
1044        contextdata = []
1045        f_label = trans(_('Course of Study:'), portal_language)
1046        f_label = Paragraph(f_label, ENTRY1_STYLE)
1047        f_text = formatted_text(view.context.certificate.longtitle)
1048        f_text = Paragraph(f_text, ENTRY1_STYLE)
1049        contextdata.append([f_label,f_text])
1050
1051        f_label = trans(_('Faculty:'), portal_language)
1052        f_label = Paragraph(f_label, ENTRY1_STYLE)
1053        f_text = formatted_text(
1054            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
1055        f_text = Paragraph(f_text, ENTRY1_STYLE)
1056        contextdata.append([f_label,f_text])
1057
1058        f_label = trans(_('Department:'), portal_language)
1059        f_label = Paragraph(f_label, ENTRY1_STYLE)
1060        f_text = formatted_text(
1061            view.context.certificate.__parent__.__parent__.longtitle)
1062        f_text = Paragraph(f_text, ENTRY1_STYLE)
1063        contextdata.append([f_label,f_text])
1064
1065        f_label = trans(_('Entry Session:'), portal_language)
1066        f_label = Paragraph(f_label, ENTRY1_STYLE)
1067        f_text = formatted_text(
1068            view.session_dict.get(view.context.entry_session))
1069        f_text = Paragraph(f_text, ENTRY1_STYLE)
1070        contextdata.append([f_label,f_text])
1071
1072        f_label = trans(_('Entry Mode:'), portal_language)
1073        f_label = Paragraph(f_label, ENTRY1_STYLE)
1074        f_text = formatted_text(view.studymode_dict.get(
1075            view.context.entry_mode))
1076        f_text = Paragraph(f_text, ENTRY1_STYLE)
1077        contextdata.append([f_label,f_text])
1078
1079        f_label = trans(_('Cumulative GPA:'), portal_language)
1080        f_label = Paragraph(f_label, ENTRY1_STYLE)
1081        format_float = getUtility(IKofaUtils).format_float
1082        cgpa = format_float(transcript_data[1], 3)
1083        if student.state == GRADUATED:
1084            f_text = formatted_text('%s (%s)' % (
1085                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
1086        else:
1087            f_text = formatted_text('%s' % cgpa)
1088        f_text = Paragraph(f_text, ENTRY1_STYLE)
1089        contextdata.append([f_label,f_text])
1090
1091        contexttable = Table(contextdata,style=SLIP_STYLE)
1092        data.append(contexttable)
1093
1094        transcripttables = render_transcript_data(
1095            view, tableheader, levels_data, lang=portal_language)
1096        data.extend(transcripttables)
1097
1098        # Insert signatures
1099        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
1100        # do not have a test for the following lines.
1101        if signatures and not sigs_in_footer:
1102            data.append(Spacer(1, 20))
1103            # Render one signature table per signature to
1104            # get date and signature in line.
1105            for signature in signatures:
1106                signaturetables = get_signature_tables(signature)
1107                data.append(signaturetables[0])
1108
1109        # Insert digital signatures
1110        if digital_sigs:
1111            data.append(Spacer(1, 20))
1112            sigs = digital_sigs.split('\n')
1113            for sig in sigs:
1114                data.append(Paragraph(sig, NOTE_STYLE))
1115
1116        view.response.setHeader(
1117            'Content-Type', 'application/pdf')
1118        try:
1119            pdf_stream = creator.create_pdf(
1120                data, None, doc_title, author=author, footer=footer_text,
1121                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin,
1122                view=view)
1123        except IOError:
1124            view.flash(_('Error in image file.'))
1125            return view.redirect(view.url(view.context))
1126        if save_file:
1127            self._saveTranscriptPDF(student, pdf_stream)
1128            return
1129        return pdf_stream
1130
1131    def renderPDFCourseticketsOverview(
1132            self, view, name, session, data, lecturers, orientation,
1133            title_length, note):
1134        """Render pdf slip of course tickets for a lecturer.
1135        """
1136        filename = '%s_%s_%s_%s.pdf' % (
1137            name, view.context.code, session, view.request.principal.id)
1138        try:
1139            session = academic_sessions_vocab.getTerm(session).title
1140        except LookupError:
1141            session = _('void')
1142        creator = getUtility(IPDFCreator, name=orientation)
1143        style = getSampleStyleSheet()
1144        pdf_data = []
1145        pdf_data += [Paragraph(
1146            translate(_('<b>Lecturer(s): ${a}</b>',
1147                      mapping = {'a':lecturers})), style["Normal"]),]
1148        pdf_data += [Paragraph(
1149            translate(_('<b>Credits: ${a}</b>',
1150                      mapping = {'a':view.context.credits})), style["Normal"]),]
1151        # Not used in base package.
1152        if data[1]:
1153            pdf_data += [Paragraph(
1154                translate(_('<b>${a}</b>',
1155                    mapping = {'a':data[1][0]})), style["Normal"]),]
1156            pdf_data += [Paragraph(
1157                translate(_('<b>${a}</b>',
1158                    mapping = {'a':data[1][1]})), style["Normal"]),]
1159            pdf_data += [Paragraph(
1160                translate(_('<b>Total Students: ${a}</b>',
1161                    mapping = {'a':data[1][2]})), style["Normal"]),]
1162            pdf_data += [Paragraph(
1163                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
1164                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
1165            pdf_data += [Paragraph(
1166                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
1167                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
1168            grade_stats = []
1169            for item in sorted(data[1][7].items()):
1170                grade_stats.append(('%s=%s' % (item[0], item[1])))
1171            grade_stats_string = ', '.join(grade_stats)
1172            pdf_data += [Paragraph(
1173                translate(_('<b>${a}</b>',
1174                mapping = {'a':grade_stats_string})), style["Normal"]),]
1175        pdf_data.append(Spacer(1, 20))
1176        colWidths = [None] * len(data[0][0])
1177        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE,
1178                           repeatRows=1)]
1179        # Process title if too long
1180        title = " ".join(view.context.title.split())
1181        ct = textwrap.fill(title, title_length)
1182        ft = "" # title
1183        #if len(textwrap.wrap(title, title_length)) > 1:
1184        #    ft = textwrap.wrap(title, title_length)[0] + ' ...'
1185        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1186            mapping = {'a':ct,
1187                       'b':view.context.code,
1188                       'd':session}))
1189        if name == 'attendance':
1190            doc_title += '\n' + translate(_('Attendance Sheet'))
1191        if name == 'coursetickets':
1192            doc_title += '\n' + translate(_('Course Tickets Overview'))
1193        #footer_title = translate(_('${a} (${b}) - ${d}',
1194        #    mapping = {'a':ft,
1195        #               'b':view.context.code,
1196        #               'd':session}))
1197        footer_title = translate(_('${b} - ${d}',
1198            mapping = {'b':view.context.code,
1199                       'd':session}))
1200        author = '%s (%s)' % (view.request.principal.title,
1201                              view.request.principal.id)
1202        view.response.setHeader(
1203            'Content-Type', 'application/pdf')
1204        view.response.setHeader(
1205            'Content-Disposition:', 'attachment; filename="%s' % filename)
1206        pdf_stream = creator.create_pdf(
1207            pdf_data, None, doc_title, author, footer_title + ' -', note
1208            )
1209        return pdf_stream
1210
1211    def updateCourseTickets(self, course):
1212        """Udate course tickets if course attributes were changed.
1213        """
1214        current_academic_session = grok.getSite()[
1215            'configuration'].current_academic_session
1216        if not current_academic_session:
1217            return
1218        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1219        coursetickets = cat.searchResults(
1220            code=(course.code, course.code),
1221            session=(current_academic_session,current_academic_session))
1222        number = 0
1223        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1224        for ticket in coursetickets:
1225            if ticket.credits == course.credits:
1226                continue
1227            if ticket.student.current_session != current_academic_session:
1228                continue
1229            if ticket.student.state not in (PAID,):
1230                continue
1231            number += 1
1232            ticket.student.__parent__.logger.info(
1233                '%s - %s %s/%s credits updated (%s->%s)' % (
1234                    ob_class, ticket.student.student_id,
1235                    ticket.level, ticket.code, course.credits,
1236                    ticket.credits))
1237            ticket.credits = course.credits
1238        return number
1239
1240    #: A dictionary which maps widget names to headlines. The headline
1241    #: is rendered in forms and on pdf slips above the respective
1242    #: display or input widget. There are no separating headlines
1243    #: in the base package.
1244    SEPARATORS_DICT = {}
1245
1246    #: A tuple containing names of file upload viewlets which are not shown
1247    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1248    #: in the base package. This attribute makes only sense, if intermediate
1249    #: custom packages are being used, like we do for all Nigerian portals.
1250    SKIP_UPLOAD_VIEWLETS = ()
1251
1252    #: A tuple containing the names of registration states in which changing of
1253    #: passport pictures is allowed.
1254    PORTRAIT_CHANGE_STATES = (ADMITTED,)
1255
1256    #: A tuple containing the names of registration states in which changing of
1257    #: scanned signatures is allowed.
1258    SIGNATURE_CHANGE_STATES = ()
1259
1260    #: A tuple containing all exporter names referring to students or
1261    #: subobjects thereof.
1262    STUDENT_EXPORTER_NAMES = (
1263            'students',
1264            'studentstudycourses',
1265            'studentstudycourses_1',
1266            #'studentstudycourses_2',
1267            'studentstudylevels',
1268            #'studentstudylevels_1',
1269            #'studentstudylevels_2',
1270            'coursetickets',
1271            #'coursetickets_1',
1272            #'coursetickets_2',
1273            'studentpayments',
1274            'bedtickets',
1275            'trimmed',
1276            'outstandingcourses',
1277            'unpaidpayments',
1278            'sfpaymentsoverview',
1279            'sessionpaymentsoverview',
1280            'studylevelsoverview',
1281            'combocard',
1282            'bursary',
1283            'accommodationpayments',
1284            'transcriptdata',
1285            'trimmedpayments',
1286            )
1287
1288    #: A tuple containing all exporter names needed for backing
1289    #: up student data
1290    STUDENT_BACKUP_EXPORTER_NAMES = (
1291            'students',
1292            'studentstudycourses',
1293            'studentstudycourses_1',
1294            'studentstudycourses_2',
1295            'studentstudylevels',
1296            'studentstudylevels_1',
1297            'studentstudylevels_2',
1298            'coursetickets',
1299            'coursetickets_1',
1300            'coursetickets_2',
1301            'studentpayments',
1302            'bedtickets')
1303
1304    # Maximum size of upload files in kB
1305    MAX_KB = 250
1306
1307    #: A prefix used when generating new student ids. Each student id will
1308    #: start with this string. The default is 'K' for Kofa.
1309    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.