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

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