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

Last change on this file since 17931 was 17917, checked in by Henrik Bettermann, 2 months ago

Implement ExportPDFFinalClearanceSlip.

  • Property svn:keywords set to Id
File size: 56.1 KB
Line 
1## $Id: utils.py 17917 2024-09-07 15:38:05Z 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 filename == 'clearance_slip.pdf':
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        try:
920            pdf_stream = creator.create_pdf(
921                data, None, doc_title, author=author, footer=footer_text,
922                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
923        except IOError:
924            view.flash('Error in image file.')
925            return view.redirect(view.url(view.context))
926        except LayoutError, err:
927            view.flash(
928                'PDF file could not be created. Reportlab error message: %s'
929                % escape(err.message),
930                type="danger")
931            return view.redirect(view.url(view.context))
932        if mergefiles:
933            return self._mergeFiles(mergefiles, watermark, pdf_stream)
934        return pdf_stream
935
936    def _admissionText(self, student, portal_language):
937        inst_name = grok.getSite()['configuration'].name
938        text = trans(_(
939            'This is to inform you that you have been provisionally admitted into'
940            ' ${a} as follows:', mapping = {'a': inst_name}),
941            portal_language)
942        return text
943
944    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
945                                 pre_text=None, post_text=None,
946                                 topMargin = 1.5,
947                                 letterhead_path=None,
948                                 mergefiles=None, watermark=None):
949        """Render pdf admission letter.
950        """
951        if student is None:
952            return
953        style = getSampleStyleSheet()
954        if letterhead_path:
955            creator = getUtility(IPDFCreator, name='letter')
956        else:
957            creator = self.getPDFCreator(student)
958        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
959        data = []
960        doc_title = view.label
961        author = '%s (%s)' % (view.request.principal.title,
962                              view.request.principal.id)
963        footer_text = view.label.split('\n')
964        if len(footer_text) > 2:
965            # We can add a department in first line
966            footer_text = footer_text[1]
967        else:
968            # Only the first line is used for the footer
969            footer_text = footer_text[0]
970        if getattr(student, 'student_id', None) is not None:
971            footer_text = "%s - %s - " % (student.student_id, footer_text)
972
973        # Text before student data
974        if pre_text is None:
975            html = format_html(self._admissionText(student, portal_language))
976        else:
977            html = format_html(pre_text)
978        if html:
979            data.append(Paragraph(html, NOTE_STYLE))
980            data.append(Spacer(1, 20))
981
982        # Student data
983        data.append(render_student_data(view, student,
984                    omit_fields, lang=portal_language,
985                    slipname='admission_slip.pdf'))
986
987        # Text after student data
988        data.append(Spacer(1, 20))
989        if post_text is None:
990            datelist = student.history.messages[0].split()[0].split('-')
991            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
992            post_text = trans(_(
993                'Your Kofa student record was created on ${a}.',
994                mapping = {'a': creation_date}),
995                portal_language)
996        #html = format_html(post_text)
997        #data.append(Paragraph(html, NOTE_STYLE))
998
999        # Create pdf stream
1000        view.response.setHeader(
1001            'Content-Type', 'application/pdf')
1002        pdf_stream = creator.create_pdf(
1003            data, None, doc_title, author=author, footer=footer_text,
1004            note=post_text, topMargin=topMargin,
1005            letterhead_path=letterhead_path)
1006        if mergefiles:
1007            return self._mergeFiles(mergefiles, watermark, pdf_stream)
1008        return pdf_stream
1009
1010    def renderPDFTranscript(self, view, filename='transcript.pdf',
1011                  student=None,
1012                  studentview=None,
1013                  note=None,
1014                  signatures=(),
1015                  sigs_in_footer=(),
1016                  digital_sigs=(),
1017                  show_scans=True, topMargin=1.5,
1018                  omit_fields=(),
1019                  tableheader=None,
1020                  no_passport=False,
1021                  save_file=False):
1022        """Render pdf slip of a transcripts.
1023        """
1024        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1025        # XXX: tell what the different parameters mean
1026        style = getSampleStyleSheet()
1027        creator = self.getPDFCreator(student)
1028        data = []
1029        doc_title = view.label
1030        author = '%s (%s)' % (view.request.principal.title,
1031                              view.request.principal.id)
1032        footer_text = view.label.split('\n')
1033        if len(footer_text) > 2:
1034            # We can add a department in first line
1035            footer_text = footer_text[1]
1036        else:
1037            # Only the first line is used for the footer
1038            footer_text = footer_text[0]
1039        if getattr(student, 'student_id', None) is not None:
1040            footer_text = "%s - %s - " % (student.student_id, footer_text)
1041
1042        # Insert student data table
1043        if student is not None:
1044            #bd_translation = trans(_('Base Data'), portal_language)
1045            #data.append(Paragraph(bd_translation, HEADING_STYLE))
1046            data.append(render_student_data(
1047                studentview, view.context,
1048                omit_fields, lang=portal_language,
1049                slipname=filename,
1050                no_passport=no_passport))
1051
1052        transcript_data = view.context.getTranscriptData()
1053        levels_data = transcript_data[0]
1054
1055        contextdata = []
1056        f_label = trans(_('Course of Study:'), portal_language)
1057        f_label = Paragraph(f_label, ENTRY1_STYLE)
1058        f_text = formatted_text(view.context.certificate.longtitle)
1059        f_text = Paragraph(f_text, ENTRY1_STYLE)
1060        contextdata.append([f_label,f_text])
1061
1062        f_label = trans(_('Faculty:'), portal_language)
1063        f_label = Paragraph(f_label, ENTRY1_STYLE)
1064        f_text = formatted_text(
1065            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
1066        f_text = Paragraph(f_text, ENTRY1_STYLE)
1067        contextdata.append([f_label,f_text])
1068
1069        f_label = trans(_('Department:'), portal_language)
1070        f_label = Paragraph(f_label, ENTRY1_STYLE)
1071        f_text = formatted_text(
1072            view.context.certificate.__parent__.__parent__.longtitle)
1073        f_text = Paragraph(f_text, ENTRY1_STYLE)
1074        contextdata.append([f_label,f_text])
1075
1076        f_label = trans(_('Entry Session:'), portal_language)
1077        f_label = Paragraph(f_label, ENTRY1_STYLE)
1078        f_text = formatted_text(
1079            view.session_dict.get(view.context.entry_session))
1080        f_text = Paragraph(f_text, ENTRY1_STYLE)
1081        contextdata.append([f_label,f_text])
1082
1083        f_label = trans(_('Entry Mode:'), portal_language)
1084        f_label = Paragraph(f_label, ENTRY1_STYLE)
1085        f_text = formatted_text(view.studymode_dict.get(
1086            view.context.entry_mode))
1087        f_text = Paragraph(f_text, ENTRY1_STYLE)
1088        contextdata.append([f_label,f_text])
1089
1090        f_label = trans(_('Cumulative GPA:'), portal_language)
1091        f_label = Paragraph(f_label, ENTRY1_STYLE)
1092        format_float = getUtility(IKofaUtils).format_float
1093        cgpa = format_float(transcript_data[1], 3)
1094        if student.state == GRADUATED:
1095            f_text = formatted_text('%s (%s)' % (
1096                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
1097        else:
1098            f_text = formatted_text('%s' % cgpa)
1099            if not transcript_data[1]:
1100                f_text = formatted_text('No results yet!')
1101        f_text = Paragraph(f_text, ENTRY1_STYLE)
1102        contextdata.append([f_label,f_text])
1103
1104        contexttable = Table(contextdata,style=SLIP_STYLE)
1105        data.append(contexttable)
1106
1107        if transcript_data[1]:
1108            transcripttables = render_transcript_data(
1109                view, tableheader, levels_data, lang=portal_language)
1110            data.extend(transcripttables)
1111
1112        # Insert signatures
1113        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
1114        # do not have a test for the following lines.
1115        if signatures and not sigs_in_footer:
1116            data.append(Spacer(1, 20))
1117            # Render one signature table per signature to
1118            # get date and signature in line.
1119            for signature in signatures:
1120                signaturetables = get_signature_tables(signature)
1121                data.append(signaturetables[0])
1122
1123        # Insert digital signatures
1124        if digital_sigs:
1125            data.append(Spacer(1, 20))
1126            sigs = digital_sigs.split('\n')
1127            for sig in sigs:
1128                data.append(Paragraph(sig, NOTE_STYLE))
1129
1130        view.response.setHeader(
1131            'Content-Type', 'application/pdf')
1132        try:
1133            pdf_stream = creator.create_pdf(
1134                data, None, doc_title, author=author, footer=footer_text,
1135                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin,
1136                view=view)
1137        except IOError:
1138            view.flash(_('Error in image file.'))
1139            return view.redirect(view.url(view.context))
1140        if save_file:
1141            self._saveTranscriptPDF(student, pdf_stream)
1142            return
1143        return pdf_stream
1144
1145    def renderPDFCourseticketsOverview(
1146            self, view, name, session, data, lecturers, orientation,
1147            title_length, note,
1148            signatures=(),
1149            sigs_in_footer=(),):
1150        """Render pdf slip of course tickets for a lecturer.
1151        """
1152        filename = '%s_%s_%s_%s.pdf' % (
1153            name, view.context.code, session, view.request.principal.id)
1154        try:
1155            session = academic_sessions_vocab.getTerm(session).title
1156        except LookupError:
1157            session = _('void')
1158        creator = getUtility(IPDFCreator, name=orientation)
1159        style = getSampleStyleSheet()
1160        pdf_data = []
1161        pdf_data += [Paragraph(
1162            translate(_('<b>Lecturer(s): ${a}</b>',
1163                      mapping = {'a':lecturers})), style["Normal"]),]
1164        pdf_data += [Paragraph(
1165            translate(_('<b>Credits: ${a}</b>',
1166                      mapping = {'a':view.context.credits})), style["Normal"]),]
1167        # Not used in base package.
1168        if data[1]:
1169            pdf_data += [Paragraph(
1170                translate(_('<b>${a}</b>',
1171                    mapping = {'a':data[1][0]})), style["Normal"]),]
1172            pdf_data += [Paragraph(
1173                translate(_('<b>${a}</b>',
1174                    mapping = {'a':data[1][1]})), style["Normal"]),]
1175            pdf_data += [Paragraph(
1176                translate(_('<b>Total Students: ${a}</b>',
1177                    mapping = {'a':data[1][2]})), style["Normal"]),]
1178            pdf_data += [Paragraph(
1179                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
1180                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
1181            pdf_data += [Paragraph(
1182                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
1183                mapping = {'a':data[1][5],'b':data[1][6]})), style["Normal"]),]
1184            grade_stats = []
1185            for item in sorted(data[1][7].items()):
1186                grade_stats.append(('%s=%s' % (item[0], item[1])))
1187            grade_stats_string = ', '.join(grade_stats)
1188            pdf_data += [Paragraph(
1189                translate(_('<b>${a}</b>',
1190                mapping = {'a':grade_stats_string})), style["Normal"]),]
1191        pdf_data.append(Spacer(1, 20))
1192        colWidths = [None] * len(data[0][0])
1193        pdf_data += [Table(data[0], colWidths=colWidths, style=CONTENT_STYLE,
1194                           repeatRows=1)]
1195        # Process title if too long
1196        title = " ".join(view.context.title.split())
1197        ct = textwrap.fill(title, title_length)
1198        ft = "" # title
1199        #if len(textwrap.wrap(title, title_length)) > 1:
1200        #    ft = textwrap.wrap(title, title_length)[0] + ' ...'
1201        doc_title = translate(_('${a} (${b})\nAcademic Session ${d}',
1202            mapping = {'a':ct,
1203                       'b':view.context.code,
1204                       'd':session}))
1205        if name == 'attendance':
1206            doc_title += '\n' + translate(_('Attendance Sheet'))
1207        if name == 'coursetickets':
1208            doc_title += '\n' + translate(_('Course Tickets Overview'))
1209        #footer_title = translate(_('${a} (${b}) - ${d}',
1210        #    mapping = {'a':ft,
1211        #               'b':view.context.code,
1212        #               'd':session}))
1213        footer_title = translate(_('${b} - ${d}',
1214            mapping = {'b':view.context.code,
1215                       'd':session}))
1216        author = '%s (%s)' % (view.request.principal.title,
1217                              view.request.principal.id)
1218
1219        # Insert signatures
1220        # XXX: Not used in waeup.kofa, so we
1221        # do not have a test for the following lines.
1222        if signatures and not sigs_in_footer:
1223            pdf_data.append(Spacer(1, 20))
1224            # Render one signature table per signature to
1225            # get date and signature in line.
1226            for signature in signatures:
1227                signaturetables = get_signature_tables(signature)
1228                pdf_data.append(signaturetables[0])
1229
1230        view.response.setHeader(
1231            'Content-Type', 'application/pdf')
1232        view.response.setHeader(
1233            'Content-Disposition:', 'attachment; filename="%s' % filename)
1234        pdf_stream = creator.create_pdf(
1235            pdf_data, None, doc_title, author, footer_title + ' -', note,
1236            sigs_in_footer=sigs_in_footer,
1237            )
1238        return pdf_stream
1239
1240    def updateCourseTickets(self, course):
1241        """Udate course tickets if course attributes were changed.
1242        """
1243        current_academic_session = grok.getSite()[
1244            'configuration'].current_academic_session
1245        if not current_academic_session:
1246            return
1247        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1248        coursetickets = cat.searchResults(
1249            code=(course.code, course.code),
1250            session=(current_academic_session,current_academic_session))
1251        number = 0
1252        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1253        for ticket in coursetickets:
1254            if ticket.credits == course.credits:
1255                continue
1256            if ticket.student.current_session != current_academic_session:
1257                continue
1258            if ticket.student.state not in (PAID,):
1259                continue
1260            number += 1
1261            ticket.student.__parent__.logger.info(
1262                '%s - %s %s/%s credits updated (%s->%s)' % (
1263                    ob_class, ticket.student.student_id,
1264                    ticket.level, ticket.code, course.credits,
1265                    ticket.credits))
1266            ticket.credits = course.credits
1267        return number
1268
1269    def final_clearance_enabled(self, student):
1270        # disabled in base package but can be enabled in tests
1271        if getattr(student,'browsertest', None) == True:
1272            return True
1273        return False
1274
1275    #: A dictionary which maps widget names to headlines. The headline
1276    #: is rendered in forms and on pdf slips above the respective
1277    #: display or input widget. There are no separating headlines
1278    #: in the base package.
1279    SEPARATORS_DICT = {}
1280
1281    #: A tuple containing names of file upload viewlets which are not shown
1282    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1283    #: in the base package. This attribute makes only sense, if intermediate
1284    #: custom packages are being used, like we do for all Nigerian portals.
1285    SKIP_UPLOAD_VIEWLETS = ()
1286
1287    #: A tuple containing the names of registration states in which changing of
1288    #: passport pictures is allowed.
1289    PORTRAIT_CHANGE_STATES = (ADMITTED,)
1290
1291    #: A tuple containing the names of registration states in which changing of
1292    #: scanned signatures is allowed.
1293    SIGNATURE_CHANGE_STATES = ()
1294
1295    #: A tuple containing all exporter names referring to students or
1296    #: subobjects thereof.
1297    STUDENT_EXPORTER_NAMES = (
1298            'students',
1299            'studentstudycourses',
1300            'studentstudycourses_1',
1301            #'studentstudycourses_2',
1302            'studentstudylevels',
1303            #'studentstudylevels_1',
1304            #'studentstudylevels_2',
1305            'coursetickets',
1306            #'coursetickets_1',
1307            #'coursetickets_2',
1308            'studentpayments',
1309            'bedtickets',
1310            'trimmed',
1311            'outstandingcourses',
1312            'unpaidpayments',
1313            'sfpaymentsoverview',
1314            'sessionpaymentsoverview',
1315            'studylevelsoverview',
1316            'combocard',
1317            'bursary',
1318            'accommodationpayments',
1319            'transcriptdata',
1320            'trimmedpayments',
1321            )
1322
1323    #: A tuple containing all exporter names needed for backing
1324    #: up student data
1325    STUDENT_BACKUP_EXPORTER_NAMES = (
1326            'students',
1327            'studentstudycourses',
1328            'studentstudycourses_1',
1329            'studentstudycourses_2',
1330            'studentstudylevels',
1331            'studentstudylevels_1',
1332            'studentstudylevels_2',
1333            'coursetickets',
1334            'coursetickets_1',
1335            'coursetickets_2',
1336            'studentpayments',
1337            'bedtickets')
1338
1339    # Maximum size of upload files in kB
1340    MAX_KB = 250
1341
1342    #: A prefix used when generating new student ids. Each student id will
1343    #: start with this string. The default is 'K' for Kofa.
1344    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.