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

Last change on this file since 17816 was 17564, checked in by Henrik Bettermann, 16 months ago

Change line breaks.

  • Property svn:keywords set to Id
File size: 54.9 KB
Line 
1## $Id: utils.py 17564 2023-08-30 06:24:19Z 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            acco_details = self.getAccommodationDetails(student)
493            p_session = acco_details['booking_session']
494            p_item = acco_details['bt']
495            amount = academic_session.booking_fee
496        elif category == 'hostel_maintenance':
497            amount = 0.0
498            booking_session = grok.getSite()['hostels'].accommodation_session
499            bedticket = student['accommodation'].get(str(booking_session), None)
500            if bedticket is not None and bedticket.bed is not None:
501                p_session = booking_session
502                p_item = bedticket.bed_coordinates
503                if bedticket.bed.__parent__.maint_fee > 0:
504                    amount = bedticket.bed.__parent__.maint_fee
505                else:
506                    # fallback
507                    amount = academic_session.maint_fee
508            else:
509                return _(u'No bed allocated.'), None
510        elif category == 'combi' and combi:
511            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
512            for cat in combi:
513                fee_name = cat + '_fee'
514                cat_amount = getattr(academic_session, fee_name, 0.0)
515                if not cat_amount:
516                    return _('%s undefined.' % categories[cat]), None
517                amount += cat_amount
518                p_item += u'%s + ' % categories[cat]
519            p_item = p_item.strip(' + ')
520        else:
521            fee_name = category + '_fee'
522            amount = getattr(academic_session, fee_name, 0.0)
523        if amount in (0.0, None):
524            return _('Amount could not be determined.'), None
525        if self.samePaymentMade(student, category, p_item, p_session):
526            return _('This type of payment has already been made.'), None
527        if self._isPaymentDisabled(p_session, category, student):
528            return _('This category of payments has been disabled.'), None
529        payment = createObject(u'waeup.StudentOnlinePayment')
530        timestamp = ("%d" % int(time()*10000))[1:]
531        payment.p_id = "p%s" % timestamp
532        payment.p_category = category
533        payment.p_item = p_item
534        payment.p_session = p_session
535        payment.p_level = p_level
536        payment.p_current = p_current
537        payment.amount_auth = amount
538        payment.p_combi = combi
539        return None, payment
540
541    def setBalanceDetails(self, category, student,
542            balance_session, balance_level, balance_amount):
543        """Create a balance payment ticket and set the payment data
544        as selected by the student.
545        """
546        p_item = u'Balance'
547        p_session = balance_session
548        p_level = balance_level
549        p_current = False
550        amount = balance_amount
551        academic_session = self._getSessionConfiguration(p_session)
552        if academic_session == None:
553            return _(u'Session configuration object is not available.'), None
554        if amount in (0.0, None) or amount < 0:
555            return _('Amount must be greater than 0.'), None
556        payment = createObject(u'waeup.StudentOnlinePayment')
557        timestamp = ("%d" % int(time()*10000))[1:]
558        payment.p_id = "p%s" % timestamp
559        payment.p_category = category
560        payment.p_item = p_item
561        payment.p_session = p_session
562        payment.p_level = p_level
563        payment.p_current = p_current
564        payment.amount_auth = amount
565        return None, payment
566
567    def increaseMatricInteger(self, student):
568        """Increase counter for matric numbers.
569        This counter can be a centrally stored attribute or an attribute of
570        faculties, departments or certificates. In the base package the counter
571        is as an attribute of the site configuration container.
572        """
573        grok.getSite()['configuration'].next_matric_integer += 1
574        return
575
576    def constructMatricNumber(self, student):
577        """Fetch the matric number counter which fits the student and
578        construct the new matric number of the student.
579        In the base package the counter is returned which is as an attribute
580        of the site configuration container.
581        """
582        next_integer = grok.getSite()['configuration'].next_matric_integer
583        if next_integer == 0:
584            return _('Matriculation number cannot be set.'), None
585        return None, unicode(next_integer)
586
587    def setMatricNumber(self, student):
588        """Set matriculation number of student. If the student's matric number
589        is unset a new matric number is
590        constructed according to the matriculation number construction rules
591        defined in the `constructMatricNumber` method. The new matric number is
592        set, the students catalog updated. The corresponding matric number
593        counter is increased by one.
594
595        This method is tested but not used in the base package. It can
596        be used in custom packages by adding respective views
597        and by customizing `increaseMatricInteger` and `constructMatricNumber`
598        according to the university's matriculation number construction rules.
599
600        The method can be disabled by setting the counter to zero.
601        """
602        if student.matric_number is not None:
603            return _('Matriculation number already set.'), None
604        if student.certcode is None:
605            return _('No certificate assigned.'), None
606        error, matric_number = self.constructMatricNumber(student)
607        if error:
608            return error, None
609        try:
610            student.matric_number = matric_number
611        except MatNumNotInSource:
612            return _('Matriculation number %s exists.' % matric_number), None
613        notify(grok.ObjectModifiedEvent(student))
614        self.increaseMatricInteger(student)
615        return None, matric_number
616
617    def allowPortraitChange(self, student):
618        """Check if student is allowed to change the portrait file.
619        """
620        if student.state not in self.PORTRAIT_CHANGE_STATES:
621            return False
622        return True
623
624    def getAccommodationDetails(self, student):
625        """Determine the accommodation data of a student.
626        """
627        d = {}
628        d['error'] = u''
629        hostels = grok.getSite()['hostels']
630        d['booking_session'] = hostels.accommodation_session
631        d['allowed_states'] = hostels.accommodation_states
632        d['startdate'] = hostels.startdate
633        d['enddate'] = hostels.enddate
634        d['expired'] = hostels.expired
635        # Determine bed type
636        studycourse = student['studycourse']
637        certificate = getattr(studycourse,'certificate',None)
638        entry_session = studycourse.entry_session
639        current_level = studycourse.current_level
640        if None in (entry_session, current_level, certificate):
641            return d
642        end_level = certificate.end_level
643        if current_level == 10:
644            bt = 'pr'
645        elif entry_session == grok.getSite()['hostels'].accommodation_session:
646            bt = 'fr'
647        elif current_level >= end_level:
648            bt = 'fi'
649        else:
650            bt = 're'
651        if student.sex == 'f':
652            sex = 'female'
653        else:
654            sex = 'male'
655        special_handling = 'regular'
656        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
657        return d
658
659    ACCOMMODATION_SPAN = 0
660
661    def checkAccommodationRequirements(self, student, acc_details):
662        if acc_details.get('expired', False):
663            startdate = acc_details.get('startdate')
664            enddate = acc_details.get('enddate')
665            if startdate and enddate:
666                tz = getUtility(IKofaUtils).tzinfo
667                startdate = to_timezone(
668                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
669                enddate = to_timezone(
670                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
671                return _("Outside booking period: ${a} - ${b}",
672                         mapping = {'a': startdate, 'b': enddate})
673            else:
674                return _("Outside booking period.")
675        if not acc_details.get('bt'):
676            return _("Your data are incomplete.")
677        if not student.state in acc_details['allowed_states']:
678            return _("You are in the wrong registration state.")
679        if  acc_details['booking_session'] - student[
680            'studycourse'].current_session > self.ACCOMMODATION_SPAN:
681            return _('Your current session does not allow ' + \
682                    'to book accommodation.')
683        bsession = str(acc_details['booking_session'])
684        if bsession in student['accommodation'].keys() \
685            and not 'booking expired' in \
686            student['accommodation'][bsession].bed_coordinates:
687            return _('You already booked a bed space in '
688                     'current accommodation session.')
689        return
690
691    def selectBed(self, available_beds):
692        """Select a bed from a filtered list of available beds.
693        In the base configuration beds are sorted by the sort id
694        of the hostel and the bed number. The first bed found in
695        this sorted list is taken.
696        """
697        sorted_beds = sorted(available_beds,
698                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
699        return sorted_beds[0]
700
701    def GPABoundaries(self, faccode=None, depcode=None, certcode=None):
702        return ((1, 'Fail'),
703               (1.5, 'Pass'),
704               (2.4, '3rd Class'),
705               (3.5, '2nd Class Lower'),
706               (4.5, '2nd Class Upper'),
707               (5, '1st Class'))
708
709    def getClassFromCGPA(self, gpa, student):
710        """Determine the class of degree. In some custom packages
711        this class depends on e.g. the entry session of the student. In the
712        base package, it does not.
713        """
714        if gpa < self.GPABoundaries()[0][0]:
715            return 0, self.GPABoundaries()[0][1]
716        if gpa < self.GPABoundaries()[1][0]:
717            return 1, self.GPABoundaries()[1][1]
718        if gpa < self.GPABoundaries()[2][0]:
719            return 2, self.GPABoundaries()[2][1]
720        if gpa < self.GPABoundaries()[3][0]:
721            return 3, self.GPABoundaries()[3][1]
722        if gpa < self.GPABoundaries()[4][0]:
723            return 4, self.GPABoundaries()[4][1]
724        if gpa <= self.GPABoundaries()[5][0]:
725            return 5, self.GPABoundaries()[5][1]
726        return
727
728    def getDegreeClassNumber(self, level_obj):
729        """Get degree class number (used for SessionResultsPresentation
730        reports).
731        """
732        if level_obj.gpa_params[1] == 0:
733            # No credits weighted
734            return 6
735        return self.getClassFromCGPA(
736            level_obj.cumulative_params[0], level_obj.student)[0]
737
738    def _saveTranscriptPDF(self, student, transcript):
739        """Create a transcript PDF file and store it in student folder.
740        """
741        file_store = getUtility(IExtFileStore)
742        file_id = IFileStoreNameChooser(student).chooseName(
743            attr="final_transcript.pdf")
744        file_store.createFile(file_id, StringIO(transcript))
745        return
746
747    def warnCreditsOOR(self, studylevel, course=None):
748        """Return message if credits are out of range. In the base
749        package only maximum credits is set.
750        """
751        if course and studylevel.total_credits + course.credits > 50:
752            return _('Maximum credits exceeded.')
753        elif studylevel.total_credits > 50:
754            return _('Maximum credits exceeded.')
755        return
756
757    def warnCourseAlreadyPassed(self, studylevel, course):
758        """Return message if course has already been passed at
759        previous levels.
760        """
761        for slevel in studylevel.__parent__.values():
762            for cticket in slevel.values():
763                if cticket.code == course.code \
764                    and cticket.total_score >= cticket.passmark:
765                    return _('Course has already been passed at previous level.')
766        return False
767
768    def getBedCoordinates(self, bedticket):
769        """Return descriptive bed coordinates.
770        This method can be used to customize the `display_coordinates`
771        property method in order to  display a
772        customary description of the bed space.
773        """
774        return bedticket.bed_coordinates
775
776    def clearance_disabled_message(self, student):
777        """Render message if clearance is disabled.
778        """
779        try:
780            session_config = grok.getSite()[
781                'configuration'][str(student.current_session)]
782        except KeyError:
783            return _('Session configuration object is not available.')
784        if not session_config.clearance_enabled:
785            return _('Clearance is disabled for this session.')
786        return None
787
788    def getPDFCreator(self, context):
789        """Get a pdf creator suitable for `context`.
790        The default implementation always returns the default creator.
791        """
792        return getUtility(IPDFCreator)
793
794    def _mergeFiles(self, mergefiles, watermark, pdf_stream):
795        merger = PdfFileMerger()
796        merger.append(StringIO(pdf_stream))
797        for file in mergefiles:
798            if watermark:
799                # Pass through all pages of each file
800                # and merge with watermark page. Paint
801                # watermark first to make it transparent.
802                marked_file = PdfFileWriter()
803                orig_file = PdfFileReader(file)
804                num_pages = orig_file.getNumPages()
805                for num in range(num_pages):
806                    watermark_file = PdfFileReader(watermark)
807                    page = watermark_file.getPage(0)
808                    page.mergePage(orig_file.getPage(num))
809                    marked_file.addPage(page)
810                # Save into a file-like object
811                tmp1 = StringIO()
812                marked_file.write(tmp1)
813                # Append the file-like object
814                merger.append(tmp1)
815            else:
816                # Just append the file object
817                merger.append(file)
818        # Save into a file-like object
819        tmp2 = StringIO()
820        merger.write(tmp2)
821        return tmp2.getvalue()
822
823    def renderPDF(self, view, filename='slip.pdf', student=None,
824                  studentview=None,
825                  tableheader=[], tabledata=[],
826                  note=None, signatures=None, sigs_in_footer=(),
827                  show_scans=True, topMargin=1.5,
828                  omit_fields=(),
829                  mergefiles=None, watermark=None,
830                  pagebreak=False):
831        """Render pdf slips for various pages (also some pages
832        in the applicants module).
833        """
834        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
835        # XXX: tell what the different parameters mean
836        style = getSampleStyleSheet()
837        creator = self.getPDFCreator(student)
838        data = []
839        doc_title = view.label
840        author = '%s (%s)' % (view.request.principal.title,
841                              view.request.principal.id)
842        footer_text = view.label.split('\n')
843        if len(footer_text) > 2:
844            # We can add a department in first line, second line is used
845            footer_text = footer_text[1]
846        else:
847            # Only the first line is used for the footer
848            footer_text = footer_text[0]
849        if getattr(student, 'student_id', None) is not None:
850            footer_text = "%s - %s - " % (student.student_id, footer_text)
851
852        # Insert student data table
853        if student is not None:
854            if view.form_fields:
855                bd_translation = trans(_('Base Data'), portal_language)
856                data.append(Paragraph(bd_translation, HEADING_STYLE))
857            data.append(render_student_data(
858                studentview, view.context, omit_fields, lang=portal_language,
859                slipname=filename))
860
861        # Insert widgets
862        if view.form_fields:
863            data.append(Paragraph(view.title, HEADING_STYLE))
864            separators = getattr(self, 'SEPARATORS_DICT', {})
865            table = creator.getWidgetsTable(
866                view.form_fields, view.context, None, lang=portal_language,
867                separators=separators)
868            data.append(table)
869
870        # Insert scanned docs
871        if show_scans:
872            data.extend(docs_as_flowables(view, portal_language))
873
874        # Insert history
875        if filename == 'clearance_slip.pdf':
876            hist_translation = trans(_('Workflow History'), portal_language)
877            data.append(Paragraph(hist_translation, HEADING_STYLE))
878            data.extend(creator.fromStringList(student.history.messages))
879
880        # Insert content tables (optionally on second page)
881        if hasattr(view, 'tabletitle'):
882            for i in range(len(view.tabletitle)):
883                if tabledata[i] and tableheader[i]:
884                    tabletitle = view.tabletitle[i]
885                    if tabletitle.startswith('_PB_'):
886                        data.append(PageBreak())
887                        tabletitle = view.tabletitle[i].strip('_PB_')
888                    #data.append(Spacer(1, 20))
889                    data.append(Paragraph(tabletitle, HEADING_STYLE))
890                    data.append(Spacer(1, 8))
891                    contenttable = render_table_data(tableheader[i],tabledata[i])
892                    data.append(contenttable)
893
894        # Insert signatures
895        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
896        # do not have a test for the following lines.
897        if signatures and not sigs_in_footer:
898            # Insert page break if necessary, else some space
899            if pagebreak:
900                data.append(PageBreak())
901            else:
902                data.append(Spacer(1, 20))
903            # Render one signature table per signature to
904            # get date and signature in line.
905            for signature in signatures:
906                signaturetables = get_signature_tables(signature)
907                data.append(signaturetables[0])
908
909        view.response.setHeader(
910            'Content-Type', 'application/pdf')
911        try:
912            pdf_stream = creator.create_pdf(
913                data, None, doc_title, author=author, footer=footer_text,
914                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
915        except IOError:
916            view.flash('Error in image file.')
917            return view.redirect(view.url(view.context))
918        except LayoutError, err:
919            view.flash(
920                'PDF file could not be created. Reportlab error message: %s'
921                % escape(err.message),
922                type="danger")
923            return view.redirect(view.url(view.context))
924        if mergefiles:
925            return self._mergeFiles(mergefiles, watermark, pdf_stream)
926        return pdf_stream
927
928    def _admissionText(self, student, portal_language):
929        inst_name = grok.getSite()['configuration'].name
930        text = trans(_(
931            'This is to inform you that you have been provisionally admitted into'
932            ' ${a} as follows:', mapping = {'a': inst_name}),
933            portal_language)
934        return text
935
936    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
937                                 pre_text=None, post_text=None,
938                                 topMargin = 1.5,
939                                 letterhead_path=None,
940                                 mergefiles=None, watermark=None):
941        """Render pdf admission letter.
942        """
943        if student is None:
944            return
945        style = getSampleStyleSheet()
946        if letterhead_path:
947            creator = getUtility(IPDFCreator, name='letter')
948        else:
949            creator = self.getPDFCreator(student)
950        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
951        data = []
952        doc_title = view.label
953        author = '%s (%s)' % (view.request.principal.title,
954                              view.request.principal.id)
955        footer_text = view.label.split('\n')
956        if len(footer_text) > 2:
957            # We can add a department in first line
958            footer_text = footer_text[1]
959        else:
960            # Only the first line is used for the footer
961            footer_text = footer_text[0]
962        if getattr(student, 'student_id', None) is not None:
963            footer_text = "%s - %s - " % (student.student_id, footer_text)
964
965        # Text before student data
966        if pre_text is None:
967            html = format_html(self._admissionText(student, portal_language))
968        else:
969            html = format_html(pre_text)
970        if html:
971            data.append(Paragraph(html, NOTE_STYLE))
972            data.append(Spacer(1, 20))
973
974        # Student data
975        data.append(render_student_data(view, student,
976                    omit_fields, lang=portal_language,
977                    slipname='admission_slip.pdf'))
978
979        # Text after student data
980        data.append(Spacer(1, 20))
981        if post_text is None:
982            datelist = student.history.messages[0].split()[0].split('-')
983            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
984            post_text = trans(_(
985                'Your Kofa student record was created on ${a}.',
986                mapping = {'a': creation_date}),
987                portal_language)
988        #html = format_html(post_text)
989        #data.append(Paragraph(html, NOTE_STYLE))
990
991        # Create pdf stream
992        view.response.setHeader(
993            'Content-Type', 'application/pdf')
994        pdf_stream = creator.create_pdf(
995            data, None, doc_title, author=author, footer=footer_text,
996            note=post_text, topMargin=topMargin,
997            letterhead_path=letterhead_path)
998        if mergefiles:
999            return self._mergeFiles(mergefiles, watermark, pdf_stream)
1000        return pdf_stream
1001
1002    def renderPDFTranscript(self, view, filename='transcript.pdf',
1003                  student=None,
1004                  studentview=None,
1005                  note=None,
1006                  signatures=(),
1007                  sigs_in_footer=(),
1008                  digital_sigs=(),
1009                  show_scans=True, topMargin=1.5,
1010                  omit_fields=(),
1011                  tableheader=None,
1012                  no_passport=False,
1013                  save_file=False):
1014        """Render pdf slip of a transcripts.
1015        """
1016        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1017        # XXX: tell what the different parameters mean
1018        style = getSampleStyleSheet()
1019        creator = self.getPDFCreator(student)
1020        data = []
1021        doc_title = view.label
1022        author = '%s (%s)' % (view.request.principal.title,
1023                              view.request.principal.id)
1024        footer_text = view.label.split('\n')
1025        if len(footer_text) > 2:
1026            # We can add a department in first line
1027            footer_text = footer_text[1]
1028        else:
1029            # Only the first line is used for the footer
1030            footer_text = footer_text[0]
1031        if getattr(student, 'student_id', None) is not None:
1032            footer_text = "%s - %s - " % (student.student_id, footer_text)
1033
1034        # Insert student data table
1035        if student is not None:
1036            #bd_translation = trans(_('Base Data'), portal_language)
1037            #data.append(Paragraph(bd_translation, HEADING_STYLE))
1038            data.append(render_student_data(
1039                studentview, view.context,
1040                omit_fields, lang=portal_language,
1041                slipname=filename,
1042                no_passport=no_passport))
1043
1044        transcript_data = view.context.getTranscriptData()
1045        levels_data = transcript_data[0]
1046
1047        contextdata = []
1048        f_label = trans(_('Course of Study:'), portal_language)
1049        f_label = Paragraph(f_label, ENTRY1_STYLE)
1050        f_text = formatted_text(view.context.certificate.longtitle)
1051        f_text = Paragraph(f_text, ENTRY1_STYLE)
1052        contextdata.append([f_label,f_text])
1053
1054        f_label = trans(_('Faculty:'), portal_language)
1055        f_label = Paragraph(f_label, ENTRY1_STYLE)
1056        f_text = formatted_text(
1057            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
1058        f_text = Paragraph(f_text, ENTRY1_STYLE)
1059        contextdata.append([f_label,f_text])
1060
1061        f_label = trans(_('Department:'), portal_language)
1062        f_label = Paragraph(f_label, ENTRY1_STYLE)
1063        f_text = formatted_text(
1064            view.context.certificate.__parent__.__parent__.longtitle)
1065        f_text = Paragraph(f_text, ENTRY1_STYLE)
1066        contextdata.append([f_label,f_text])
1067
1068        f_label = trans(_('Entry Session:'), portal_language)
1069        f_label = Paragraph(f_label, ENTRY1_STYLE)
1070        f_text = formatted_text(
1071            view.session_dict.get(view.context.entry_session))
1072        f_text = Paragraph(f_text, ENTRY1_STYLE)
1073        contextdata.append([f_label,f_text])
1074
1075        f_label = trans(_('Entry Mode:'), portal_language)
1076        f_label = Paragraph(f_label, ENTRY1_STYLE)
1077        f_text = formatted_text(view.studymode_dict.get(
1078            view.context.entry_mode))
1079        f_text = Paragraph(f_text, ENTRY1_STYLE)
1080        contextdata.append([f_label,f_text])
1081
1082        f_label = trans(_('Cumulative GPA:'), portal_language)
1083        f_label = Paragraph(f_label, ENTRY1_STYLE)
1084        format_float = getUtility(IKofaUtils).format_float
1085        cgpa = format_float(transcript_data[1], 3)
1086        if student.state == GRADUATED:
1087            f_text = formatted_text('%s (%s)' % (
1088                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
1089        else:
1090            f_text = formatted_text('%s' % cgpa)
1091            if not transcript_data[1]:
1092                f_text = formatted_text('No results yet!')
1093        f_text = Paragraph(f_text, ENTRY1_STYLE)
1094        contextdata.append([f_label,f_text])
1095
1096        contexttable = Table(contextdata,style=SLIP_STYLE)
1097        data.append(contexttable)
1098
1099        if transcript_data[1]:
1100            transcripttables = render_transcript_data(
1101                view, tableheader, levels_data, lang=portal_language)
1102            data.extend(transcripttables)
1103
1104        # Insert signatures
1105        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
1106        # do not have a test for the following lines.
1107        if signatures and not sigs_in_footer:
1108            data.append(Spacer(1, 20))
1109            # Render one signature table per signature to
1110            # get date and signature in line.
1111            for signature in signatures:
1112                signaturetables = get_signature_tables(signature)
1113                data.append(signaturetables[0])
1114
1115        # Insert digital signatures
1116        if digital_sigs:
1117            data.append(Spacer(1, 20))
1118            sigs = digital_sigs.split('\n')
1119            for sig in sigs:
1120                data.append(Paragraph(sig, NOTE_STYLE))
1121
1122        view.response.setHeader(
1123            'Content-Type', 'application/pdf')
1124        try:
1125            pdf_stream = creator.create_pdf(
1126                data, None, doc_title, author=author, footer=footer_text,
1127                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin,
1128                view=view)
1129        except IOError:
1130            view.flash(_('Error in image file.'))
1131            return view.redirect(view.url(view.context))
1132        if save_file:
1133            self._saveTranscriptPDF(student, pdf_stream)
1134            return
1135        return pdf_stream
1136
1137    def renderPDFCourseticketsOverview(
1138            self, view, name, session, data, lecturers, orientation,
1139            title_length, note):
1140        """Render pdf slip of course tickets for a lecturer.
1141        """
1142        filename = '%s_%s_%s_%s.pdf' % (
1143            name, view.context.code, session, view.request.principal.id)
1144        try:
1145            session = academic_sessions_vocab.getTerm(session).title
1146        except LookupError:
1147            session = _('void')
1148        creator = getUtility(IPDFCreator, name=orientation)
1149        style = getSampleStyleSheet()
1150        pdf_data = []
1151        pdf_data += [Paragraph(
1152            translate(_('<b>Lecturer(s): ${a}</b>',
1153                      mapping = {'a':lecturers})), style["Normal"]),]
1154        pdf_data += [Paragraph(
1155            translate(_('<b>Credits: ${a}</b>',
1156                      mapping = {'a':view.context.credits})), style["Normal"]),]
1157        # Not used in base package.
1158        if data[1]:
1159            pdf_data += [Paragraph(
1160                translate(_('<b>${a}</b>',
1161                    mapping = {'a':data[1][0]})), style["Normal"]),]
1162            pdf_data += [Paragraph(
1163                translate(_('<b>${a}</b>',
1164                    mapping = {'a':data[1][1]})), style["Normal"]),]
1165            pdf_data += [Paragraph(
1166                translate(_('<b>Total Students: ${a}</b>',
1167                    mapping = {'a':data[1][2]})), style["Normal"]),]
1168            pdf_data += [Paragraph(
1169                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
1170                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
1171            pdf_data += [Paragraph(
1172                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
1173                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
1174            grade_stats = []
1175            for item in sorted(data[1][7].items()):
1176                grade_stats.append(('%s=%s' % (item[0], item[1])))
1177            grade_stats_string = ', '.join(grade_stats)
1178            pdf_data += [Paragraph(
1179                translate(_('<b>${a}</b>',
1180                mapping = {'a':grade_stats_string})), style["Normal"]),]
1181        pdf_data.append(Spacer(1, 20))
1182        colWidths = [None] * len(data[0][0])
1183        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE,
1184                           repeatRows=1)]
1185        # Process title if too long
1186        title = " ".join(view.context.title.split())
1187        ct = textwrap.fill(title, title_length)
1188        ft = "" # title
1189        #if len(textwrap.wrap(title, title_length)) > 1:
1190        #    ft = textwrap.wrap(title, title_length)[0] + ' ...'
1191        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1192            mapping = {'a':ct,
1193                       'b':view.context.code,
1194                       'd':session}))
1195        if name == 'attendance':
1196            doc_title += '\n' + translate(_('Attendance Sheet'))
1197        if name == 'coursetickets':
1198            doc_title += '\n' + translate(_('Course Tickets Overview'))
1199        #footer_title = translate(_('${a} (${b}) - ${d}',
1200        #    mapping = {'a':ft,
1201        #               'b':view.context.code,
1202        #               'd':session}))
1203        footer_title = translate(_('${b} - ${d}',
1204            mapping = {'b':view.context.code,
1205                       'd':session}))
1206        author = '%s (%s)' % (view.request.principal.title,
1207                              view.request.principal.id)
1208        view.response.setHeader(
1209            'Content-Type', 'application/pdf')
1210        view.response.setHeader(
1211            'Content-Disposition:', 'attachment; filename="%s' % filename)
1212        pdf_stream = creator.create_pdf(
1213            pdf_data, None, doc_title, author, footer_title + ' -', note
1214            )
1215        return pdf_stream
1216
1217    def updateCourseTickets(self, course):
1218        """Udate course tickets if course attributes were changed.
1219        """
1220        current_academic_session = grok.getSite()[
1221            'configuration'].current_academic_session
1222        if not current_academic_session:
1223            return
1224        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1225        coursetickets = cat.searchResults(
1226            code=(course.code, course.code),
1227            session=(current_academic_session,current_academic_session))
1228        number = 0
1229        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1230        for ticket in coursetickets:
1231            if ticket.credits == course.credits:
1232                continue
1233            if ticket.student.current_session != current_academic_session:
1234                continue
1235            if ticket.student.state not in (PAID,):
1236                continue
1237            number += 1
1238            ticket.student.__parent__.logger.info(
1239                '%s - %s %s/%s credits updated (%s->%s)' % (
1240                    ob_class, ticket.student.student_id,
1241                    ticket.level, ticket.code, course.credits,
1242                    ticket.credits))
1243            ticket.credits = course.credits
1244        return number
1245
1246    #: A dictionary which maps widget names to headlines. The headline
1247    #: is rendered in forms and on pdf slips above the respective
1248    #: display or input widget. There are no separating headlines
1249    #: in the base package.
1250    SEPARATORS_DICT = {}
1251
1252    #: A tuple containing names of file upload viewlets which are not shown
1253    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1254    #: in the base package. This attribute makes only sense, if intermediate
1255    #: custom packages are being used, like we do for all Nigerian portals.
1256    SKIP_UPLOAD_VIEWLETS = ()
1257
1258    #: A tuple containing the names of registration states in which changing of
1259    #: passport pictures is allowed.
1260    PORTRAIT_CHANGE_STATES = (ADMITTED,)
1261
1262    #: A tuple containing the names of registration states in which changing of
1263    #: scanned signatures is allowed.
1264    SIGNATURE_CHANGE_STATES = ()
1265
1266    #: A tuple containing all exporter names referring to students or
1267    #: subobjects thereof.
1268    STUDENT_EXPORTER_NAMES = (
1269            'students',
1270            'studentstudycourses',
1271            'studentstudycourses_1',
1272            #'studentstudycourses_2',
1273            'studentstudylevels',
1274            #'studentstudylevels_1',
1275            #'studentstudylevels_2',
1276            'coursetickets',
1277            #'coursetickets_1',
1278            #'coursetickets_2',
1279            'studentpayments',
1280            'bedtickets',
1281            'trimmed',
1282            'outstandingcourses',
1283            'unpaidpayments',
1284            'sfpaymentsoverview',
1285            'sessionpaymentsoverview',
1286            'studylevelsoverview',
1287            'combocard',
1288            'bursary',
1289            'accommodationpayments',
1290            'transcriptdata',
1291            'trimmedpayments',
1292            )
1293
1294    #: A tuple containing all exporter names needed for backing
1295    #: up student data
1296    STUDENT_BACKUP_EXPORTER_NAMES = (
1297            'students',
1298            'studentstudycourses',
1299            'studentstudycourses_1',
1300            'studentstudycourses_2',
1301            'studentstudylevels',
1302            'studentstudylevels_1',
1303            'studentstudylevels_2',
1304            'coursetickets',
1305            'coursetickets_1',
1306            'coursetickets_2',
1307            'studentpayments',
1308            'bedtickets')
1309
1310    # Maximum size of upload files in kB
1311    MAX_KB = 250
1312
1313    #: A prefix used when generating new student ids. Each student id will
1314    #: start with this string. The default is 'K' for Kofa.
1315    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.