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

Last change on this file since 11568 was 11550, checked in by Henrik Bettermann, 10 years ago

Print QR codes on all student slips.

  • Property svn:keywords set to Id
File size: 36.1 KB
Line 
1## $Id: utils.py 11550 2014-03-28 08:54:30Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""General helper functions and utilities for the student section.
19"""
20import grok
21from time import time
22from reportlab.lib import colors
23from reportlab.lib.units import cm
24from reportlab.lib.pagesizes import A4
25from reportlab.lib.styles import getSampleStyleSheet
26from reportlab.platypus import Paragraph, Image, Table, Spacer
27from zope.schema.interfaces import ConstraintNotSatisfied
28from zope.component import getUtility, createObject
29from zope.formlib.form import setUpEditWidgets
30from zope.i18n import translate
31from waeup.kofa.interfaces import (
32    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
33    academic_sessions_vocab)
34from waeup.kofa.interfaces import MessageFactory as _
35from waeup.kofa.students.interfaces import IStudentsUtils
36from waeup.kofa.students.workflow import ADMITTED
37from waeup.kofa.students.vocabularies import StudyLevelSource
38from waeup.kofa.browser.pdf import (
39    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
40    get_signature_tables, get_qrcode)
41from waeup.kofa.browser.interfaces import IPDFCreator
42from waeup.kofa.utils.helpers import to_timezone
43
44SLIP_STYLE = [
45    ('VALIGN',(0,0),(-1,-1),'TOP'),
46    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
47    ]
48
49CONTENT_STYLE = [
50    ('VALIGN',(0,0),(-1,-1),'TOP'),
51    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
52    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
53    #('BACKGROUND',(0,0),(-1,0),colors.black),
54    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
55    ('BOX', (0,0), (-1,-1), 1, colors.black),
56
57    ]
58
59FONT_SIZE = 10
60FONT_COLOR = 'black'
61
62def trans(text, lang):
63    # shortcut
64    return translate(text, 'waeup.kofa', target_language=lang)
65
66def formatted_text(text, color=FONT_COLOR, lang='en'):
67    """Turn `text`, `color` and `size` into an HTML snippet.
68
69    The snippet is suitable for use with reportlab and generating PDFs.
70    Wraps the `text` into a ``<font>`` tag with passed attributes.
71
72    Also non-strings are converted. Raw strings are expected to be
73    utf-8 encoded (usually the case for widgets etc.).
74
75    Finally, a br tag is added if widgets contain div tags
76    which are not supported by reportlab.
77
78    The returned snippet is unicode type.
79    """
80    if not isinstance(text, unicode):
81        if isinstance(text, basestring):
82            text = text.decode('utf-8')
83        else:
84            text = unicode(text)
85    if text == 'None':
86        text = ''
87    # Mainly for boolean values we need our customized
88    # localisation of the zope domain
89    text = translate(text, 'zope', target_language=lang)
90    text = text.replace('</div>', '<br /></div>')
91    tag1 = u'<font color="%s">' % (color)
92    return tag1 + u'%s</font>' % text
93
94def generate_student_id():
95    students = grok.getSite()['students']
96    new_id = students.unique_student_id
97    return new_id
98
99def set_up_widgets(view, ignore_request=False):
100    view.adapters = {}
101    view.widgets = setUpEditWidgets(
102        view.form_fields, view.prefix, view.context, view.request,
103        adapters=view.adapters, for_display=True,
104        ignore_request=ignore_request
105        )
106
107def render_student_data(studentview, context, omit_fields=(),
108                        lang='en', slipname=None):
109    """Render student table for an existing frame.
110    """
111    width, height = A4
112    set_up_widgets(studentview, ignore_request=True)
113    data_left = []
114    data_middle = []
115    style = getSampleStyleSheet()
116    img = getUtility(IExtFileStore).getFileByContext(
117        studentview.context, attr='passport.jpg')
118    if img is None:
119        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
120        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
121    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
122    data_left.append([doc_img])
123    #data.append([Spacer(1, 12)])
124
125    f_label = trans(_('Name:'), lang)
126    f_label = Paragraph(f_label, ENTRY1_STYLE)
127    f_text = formatted_text(studentview.context.display_fullname)
128    f_text = Paragraph(f_text, ENTRY1_STYLE)
129    data_middle.append([f_label,f_text])
130
131    for widget in studentview.widgets:
132        if 'name' in widget.name:
133            continue
134        f_label = translate(
135            widget.label.strip(), 'waeup.kofa',
136            target_language=lang)
137        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
138        f_text = formatted_text(widget(), lang=lang)
139        f_text = Paragraph(f_text, ENTRY1_STYLE)
140        data_middle.append([f_label,f_text])
141
142    if getattr(studentview.context, 'certcode', None):
143        if not 'certificate' in omit_fields:
144            f_label = trans(_('Study Course:'), lang)
145            f_label = Paragraph(f_label, ENTRY1_STYLE)
146            f_text = formatted_text(
147                studentview.context['studycourse'].certificate.longtitle)
148            f_text = Paragraph(f_text, ENTRY1_STYLE)
149            data_middle.append([f_label,f_text])
150        if not 'department' in omit_fields:
151            f_label = trans(_('Department:'), lang)
152            f_label = Paragraph(f_label, ENTRY1_STYLE)
153            f_text = formatted_text(
154                studentview.context[
155                'studycourse'].certificate.__parent__.__parent__.longtitle,
156                )
157            f_text = Paragraph(f_text, ENTRY1_STYLE)
158            data_middle.append([f_label,f_text])
159        if not 'faculty' in omit_fields:
160            f_label = trans(_('Faculty:'), lang)
161            f_label = Paragraph(f_label, ENTRY1_STYLE)
162            f_text = formatted_text(
163                studentview.context[
164                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
165                )
166            f_text = Paragraph(f_text, ENTRY1_STYLE)
167            data_middle.append([f_label,f_text])
168        if not 'current_mode' in omit_fields:
169            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
170            sm = studymodes_dict[studentview.context.current_mode]
171            f_label = trans(_('Study Mode:'), lang)
172            f_label = Paragraph(f_label, ENTRY1_STYLE)
173            f_text = formatted_text(sm)
174            f_text = Paragraph(f_text, ENTRY1_STYLE)
175            data_middle.append([f_label,f_text])
176        if not 'entry_session' in omit_fields:
177            f_label = trans(_('Entry Session:'), lang)
178            f_label = Paragraph(f_label, ENTRY1_STYLE)
179            entry_session = studentview.context.entry_session
180            entry_session = academic_sessions_vocab.getTerm(entry_session).title
181            f_text = formatted_text(entry_session)
182            f_text = Paragraph(f_text, ENTRY1_STYLE)
183            data_middle.append([f_label,f_text])
184        # Requested by Uniben, does not really make sense
185        if not 'current_level' in omit_fields:
186            f_label = trans(_('Current Level:'), lang)
187            f_label = Paragraph(f_label, ENTRY1_STYLE)
188            current_level = studentview.context['studycourse'].current_level
189            studylevelsource = StudyLevelSource().factory
190            current_level = studylevelsource.getTitle(
191                studentview.context, current_level)
192            f_text = formatted_text(current_level)
193            f_text = Paragraph(f_text, ENTRY1_STYLE)
194            data_middle.append([f_label,f_text])
195        if not 'date_of_birth' in omit_fields:
196            f_label = trans(_('Date of Birth:'), lang)
197            f_label = Paragraph(f_label, ENTRY1_STYLE)
198            date_of_birth = studentview.context.date_of_birth
199            tz = getUtility(IKofaUtils).tzinfo
200            date_of_birth = to_timezone(date_of_birth, tz)
201            if date_of_birth is not None:
202                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
203            f_text = formatted_text(date_of_birth)
204            f_text = Paragraph(f_text, ENTRY1_STYLE)
205            data_middle.append([f_label,f_text])
206
207    # append QR code to the right
208    if slipname:
209        url = studentview.url(context, slipname)
210        data_right = [[get_qrcode(url, width=70.0)]]
211        table_right = Table(data_right,style=SLIP_STYLE)
212    else:
213        table_right = None
214
215    table_left = Table(data_left,style=SLIP_STYLE)
216    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
217    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
218    return table
219
220def render_table_data(tableheader, tabledata, lang='en'):
221    """Render children table for an existing frame.
222    """
223    data = []
224    #data.append([Spacer(1, 12)])
225    line = []
226    style = getSampleStyleSheet()
227    for element in tableheader:
228        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
229        field = Paragraph(field, style["Normal"])
230        line.append(field)
231    data.append(line)
232    for ticket in tabledata:
233        line = []
234        for element in tableheader:
235              field = formatted_text(getattr(ticket,element[1],u' '))
236              field = Paragraph(field, style["Normal"])
237              line.append(field)
238        data.append(line)
239    table = Table(data,colWidths=[
240        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
241    return table
242
243def render_transcript_data(view, tableheader, levels_data, lang='en'):
244    """Render children table for an existing frame.
245    """
246    data = []
247    style = getSampleStyleSheet()
248    for level in levels_data:
249        level_obj = level['level']
250        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
251        headerline = []
252        tabledata = []
253        subheader = '%s %s, %s %s' % (
254            trans(_('Session'), lang),
255            view.session_dict[level_obj.level_session],
256            trans(_('Level'), lang),
257            view.level_dict[level_obj.level])
258        data.append(Paragraph(subheader, HEADING_STYLE))
259        for element in tableheader:
260            field = '<strong>%s</strong>' % formatted_text(element[0])
261            field = Paragraph(field, style["Normal"])
262            headerline.append(field)
263        tabledata.append(headerline)
264        for ticket in tickets:
265            ticketline = []
266            for element in tableheader:
267                  field = formatted_text(getattr(ticket,element[1],u' '))
268                  field = Paragraph(field, style["Normal"])
269                  ticketline.append(field)
270            tabledata.append(ticketline)
271        table = Table(tabledata,colWidths=[
272            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
273        data.append(table)
274        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
275        data.append(Paragraph(sgpa, style["Normal"]))
276    return data
277
278def docs_as_flowables(view, lang='en'):
279    """Create reportlab flowables out of scanned docs.
280    """
281    # XXX: fix circular import problem
282    from waeup.kofa.students.viewlets import FileManager
283    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
284    style = getSampleStyleSheet()
285    data = []
286
287    # Collect viewlets
288    fm = FileManager(view.context, view.request, view)
289    fm.update()
290    if fm.viewlets:
291        sc_translation = trans(_('Scanned Documents'), lang)
292        data.append(Paragraph(sc_translation, HEADING_STYLE))
293        # Insert list of scanned documents
294        table_data = []
295        for viewlet in fm.viewlets:
296            if viewlet.file_exists:
297                # Show viewlet only if file exists
298                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
299                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
300                    view.context, attr=viewlet.download_name), 'name', None)
301                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
302                if img_path is None:
303                    pass
304                elif not img_path[-4:] in ('.jpg', '.JPG'):
305                    # reportlab requires jpg images, I think.
306                    f_text = Paragraph('%s (not displayable)' % (
307                        viewlet.title,), ENTRY1_STYLE)
308                else:
309                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
310                table_data.append([f_label, f_text])
311        if table_data:
312            # safety belt; empty tables lead to problems.
313            data.append(Table(table_data, style=SLIP_STYLE))
314    return data
315
316class StudentsUtils(grok.GlobalUtility):
317    """A collection of methods subject to customization.
318    """
319    grok.implements(IStudentsUtils)
320
321    def getReturningData(self, student):
322        """ Define what happens after school fee payment
323        depending on the student's senate verdict.
324
325        In the base configuration current level is always increased
326        by 100 no matter which verdict has been assigned.
327        """
328        new_level = student['studycourse'].current_level + 100
329        new_session = student['studycourse'].current_session + 1
330        return new_session, new_level
331
332    def setReturningData(self, student):
333        """ Define what happens after school fee payment
334        depending on the student's senate verdict.
335
336        This method folllows the same algorithm as getReturningData but
337        it also sets the new values.
338        """
339        new_session, new_level = self.getReturningData(student)
340        try:
341            student['studycourse'].current_level = new_level
342        except ConstraintNotSatisfied:
343            # Do not change level if level exceeds the
344            # certificate's end_level.
345            pass
346        student['studycourse'].current_session = new_session
347        verdict = student['studycourse'].current_verdict
348        student['studycourse'].current_verdict = '0'
349        student['studycourse'].previous_verdict = verdict
350        return
351
352    def _getSessionConfiguration(self, session):
353        try:
354            return grok.getSite()['configuration'][str(session)]
355        except KeyError:
356            return None
357
358    def _isPaymentDisabled(self, p_session, category, student):
359        academic_session = self._getSessionConfiguration(p_session)
360        if category == 'schoolfee' and \
361            'sf_all' in academic_session.payment_disabled:
362            return True
363        return False
364
365    def setPaymentDetails(self, category, student,
366            previous_session, previous_level):
367        """Create Payment object and set the payment data of a student for
368        the payment category specified.
369
370        """
371        p_item = u''
372        amount = 0.0
373        if previous_session:
374            if previous_session < student['studycourse'].entry_session:
375                return _('The previous session must not fall below '
376                         'your entry session.'), None
377            if category == 'schoolfee':
378                # School fee is always paid for the following session
379                if previous_session > student['studycourse'].current_session:
380                    return _('This is not a previous session.'), None
381            else:
382                if previous_session > student['studycourse'].current_session - 1:
383                    return _('This is not a previous session.'), None
384            p_session = previous_session
385            p_level = previous_level
386            p_current = False
387        else:
388            p_session = student['studycourse'].current_session
389            p_level = student['studycourse'].current_level
390            p_current = True
391        academic_session = self._getSessionConfiguration(p_session)
392        if academic_session == None:
393            return _(u'Session configuration object is not available.'), None
394        # Determine fee.
395        if category == 'schoolfee':
396            try:
397                certificate = student['studycourse'].certificate
398                p_item = certificate.code
399            except (AttributeError, TypeError):
400                return _('Study course data are incomplete.'), None
401            if previous_session:
402                # Students can pay for previous sessions in all
403                # workflow states.  Fresh students are excluded by the
404                # update method of the PreviousPaymentAddFormPage.
405                if previous_level == 100:
406                    amount = getattr(certificate, 'school_fee_1', 0.0)
407                else:
408                    amount = getattr(certificate, 'school_fee_2', 0.0)
409            else:
410                if student.state == CLEARED:
411                    amount = getattr(certificate, 'school_fee_1', 0.0)
412                elif student.state == RETURNING:
413                    # In case of returning school fee payment the
414                    # payment session and level contain the values of
415                    # the session the student has paid for. Payment
416                    # session is always next session.
417                    p_session, p_level = self.getReturningData(student)
418                    academic_session = self._getSessionConfiguration(p_session)
419                    if academic_session == None:
420                        return _(
421                            u'Session configuration object is not available.'
422                            ), None
423                    amount = getattr(certificate, 'school_fee_2', 0.0)
424                elif student.is_postgrad and student.state == PAID:
425                    # Returning postgraduate students also pay for the
426                    # next session but their level always remains the
427                    # same.
428                    p_session += 1
429                    academic_session = self._getSessionConfiguration(p_session)
430                    if academic_session == None:
431                        return _(
432                            u'Session configuration object is not available.'
433                            ), None
434                    amount = getattr(certificate, 'school_fee_2', 0.0)
435        elif category == 'clearance':
436            try:
437                p_item = student['studycourse'].certificate.code
438            except (AttributeError, TypeError):
439                return _('Study course data are incomplete.'), None
440            amount = academic_session.clearance_fee
441        elif category == 'bed_allocation':
442            p_item = self.getAccommodationDetails(student)['bt']
443            amount = academic_session.booking_fee
444        elif category == 'hostel_maintenance':
445            amount = 0.0
446            bedticket = student['accommodation'].get(
447                str(student.current_session), None)
448            if bedticket:
449                p_item = bedticket.bed_coordinates
450                if bedticket.bed.__parent__.maint_fee > 0:
451                    amount = bedticket.bed.__parent__.maint_fee
452                else:
453                    # fallback
454                    amount = academic_session.maint_fee
455            else:
456                # Should not happen because this is already checked
457                # in the browser module, but anyway ...
458                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
459                p_item = trans(_('no bed allocated'), portal_language)
460        elif category == 'transcript':
461            amount = academic_session.transcript_fee
462        if amount in (0.0, None):
463            return _('Amount could not be determined.'), None
464        for key in student['payments'].keys():
465            ticket = student['payments'][key]
466            if ticket.p_state == 'paid' and\
467               ticket.p_category == category and \
468               ticket.p_item == p_item and \
469               ticket.p_session == p_session:
470                  return _('This type of payment has already been made.'), None
471        if self._isPaymentDisabled(p_session, category, student):
472            return _('Payment temporarily disabled.'), None
473        payment = createObject(u'waeup.StudentOnlinePayment')
474        timestamp = ("%d" % int(time()*10000))[1:]
475        payment.p_id = "p%s" % timestamp
476        payment.p_category = category
477        payment.p_item = p_item
478        payment.p_session = p_session
479        payment.p_level = p_level
480        payment.p_current = p_current
481        payment.amount_auth = amount
482        return None, payment
483
484    def setBalanceDetails(self, category, student,
485            balance_session, balance_level, balance_amount):
486        """Create Payment object and set the payment data of a student for.
487
488        """
489        p_item = u'Balance'
490        p_session = balance_session
491        p_level = balance_level
492        p_current = False
493        amount = balance_amount
494        academic_session = self._getSessionConfiguration(p_session)
495        if academic_session == None:
496            return _(u'Session configuration object is not available.'), None
497        if amount in (0.0, None) or amount < 0:
498            return _('Amount must be greater than 0.'), None
499        for key in student['payments'].keys():
500            ticket = student['payments'][key]
501            if ticket.p_state == 'paid' and\
502               ticket.p_category == 'balance' and \
503               ticket.p_item == p_item and \
504               ticket.p_session == p_session:
505                  return _('This type of payment has already been made.'), None
506        payment = createObject(u'waeup.StudentOnlinePayment')
507        timestamp = ("%d" % int(time()*10000))[1:]
508        payment.p_id = "p%s" % timestamp
509        payment.p_category = category
510        payment.p_item = p_item
511        payment.p_session = p_session
512        payment.p_level = p_level
513        payment.p_current = p_current
514        payment.amount_auth = amount
515        return None, payment
516
517    def getAccommodationDetails(self, student):
518        """Determine the accommodation data of a student.
519        """
520        d = {}
521        d['error'] = u''
522        hostels = grok.getSite()['hostels']
523        d['booking_session'] = hostels.accommodation_session
524        d['allowed_states'] = hostels.accommodation_states
525        d['startdate'] = hostels.startdate
526        d['enddate'] = hostels.enddate
527        d['expired'] = hostels.expired
528        # Determine bed type
529        studycourse = student['studycourse']
530        certificate = getattr(studycourse,'certificate',None)
531        entry_session = studycourse.entry_session
532        current_level = studycourse.current_level
533        if None in (entry_session, current_level, certificate):
534            return d
535        end_level = certificate.end_level
536        if current_level == 10:
537            bt = 'pr'
538        elif entry_session == grok.getSite()['hostels'].accommodation_session:
539            bt = 'fr'
540        elif current_level >= end_level:
541            bt = 'fi'
542        else:
543            bt = 're'
544        if student.sex == 'f':
545            sex = 'female'
546        else:
547            sex = 'male'
548        special_handling = 'regular'
549        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
550        return d
551
552    def selectBed(self, available_beds):
553        """Select a bed from a list of available beds.
554
555        In the base configuration we select the first bed found,
556        but can also randomize the selection if we like.
557        """
558        return available_beds[0]
559
560    def _admissionText(self, student, portal_language):
561        inst_name = grok.getSite()['configuration'].name
562        text = trans(_(
563            'This is to inform you that you have been provisionally'
564            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
565            portal_language)
566        return text
567
568    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
569                                 pre_text=None, post_text=None,):
570        """Render pdf admission letter.
571        """
572        if student is None:
573            return
574        style = getSampleStyleSheet()
575        creator = self.getPDFCreator(student)
576        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
577        data = []
578        doc_title = view.label
579        author = '%s (%s)' % (view.request.principal.title,
580                              view.request.principal.id)
581        footer_text = view.label.split('\n')
582        if len(footer_text) > 1:
583            # We can add a department in first line
584            footer_text = footer_text[1]
585        else:
586            # Only the first line is used for the footer
587            footer_text = footer_text[0]
588        if getattr(student, 'student_id', None) is not None:
589            footer_text = "%s - %s - " % (student.student_id, footer_text)
590
591        # Text before student data
592        if pre_text is None:
593            html = format_html(self._admissionText(student, portal_language))
594        else:
595            html = format_html(pre_text)
596        data.append(Paragraph(html, NOTE_STYLE))
597        data.append(Spacer(1, 20))
598
599        # Student data
600        data.append(render_student_data(view, student,
601                    omit_fields, lang=portal_language,
602                    slipname='admission_slip.pdf'))
603
604        # Text after student data
605        data.append(Spacer(1, 20))
606        if post_text is None:
607            datelist = student.history.messages[0].split()[0].split('-')
608            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
609            post_text = trans(_(
610                'Your Kofa student record was created on ${a}.',
611                mapping = {'a': creation_date}),
612                portal_language)
613        #html = format_html(post_text)
614        #data.append(Paragraph(html, NOTE_STYLE))
615
616        # Create pdf stream
617        view.response.setHeader(
618            'Content-Type', 'application/pdf')
619        pdf_stream = creator.create_pdf(
620            data, None, doc_title, author=author, footer=footer_text,
621            note=post_text)
622        return pdf_stream
623
624    def getPDFCreator(self, context):
625        """Get a pdf creator suitable for `context`.
626
627        The default implementation always returns the default creator.
628        """
629        return getUtility(IPDFCreator)
630
631    def renderPDF(self, view, filename='slip.pdf', student=None,
632                  studentview=None,
633                  tableheader=[], tabledata=[],
634                  note=None, signatures=None, sigs_in_footer=(),
635                  show_scans=True, topMargin=1.5,
636                  omit_fields=()):
637        """Render pdf slips for various pages.
638        """
639        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
640        # XXX: tell what the different parameters mean
641        style = getSampleStyleSheet()
642        creator = self.getPDFCreator(student)
643        data = []
644        doc_title = view.label
645        author = '%s (%s)' % (view.request.principal.title,
646                              view.request.principal.id)
647        footer_text = view.label.split('\n')
648        if len(footer_text) > 2:
649            # We can add a department in first line
650            footer_text = footer_text[1]
651        else:
652            # Only the first line is used for the footer
653            footer_text = footer_text[0]
654        if getattr(student, 'student_id', None) is not None:
655            footer_text = "%s - %s - " % (student.student_id, footer_text)
656
657        # Insert student data table
658        if student is not None:
659            bd_translation = trans(_('Base Data'), portal_language)
660            data.append(Paragraph(bd_translation, HEADING_STYLE))
661            data.append(render_student_data(
662                studentview, view.context, omit_fields, lang=portal_language,
663                slipname=filename))
664
665        # Insert widgets
666        if view.form_fields:
667            data.append(Paragraph(view.title, HEADING_STYLE))
668            separators = getattr(self, 'SEPARATORS_DICT', {})
669            table = creator.getWidgetsTable(
670                view.form_fields, view.context, None, lang=portal_language,
671                separators=separators)
672            data.append(table)
673
674        # Insert scanned docs
675        if show_scans:
676            data.extend(docs_as_flowables(view, portal_language))
677
678        # Insert history
679        if filename.startswith('clearance'):
680            hist_translation = trans(_('Workflow History'), portal_language)
681            data.append(Paragraph(hist_translation, HEADING_STYLE))
682            data.extend(creator.fromStringList(student.history.messages))
683
684        # Insert content tables (optionally on second page)
685        if hasattr(view, 'tabletitle'):
686            for i in range(len(view.tabletitle)):
687                if tabledata[i] and tableheader[i]:
688                    #data.append(PageBreak())
689                    #data.append(Spacer(1, 20))
690                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
691                    data.append(Spacer(1, 8))
692                    contenttable = render_table_data(tableheader[i],tabledata[i])
693                    data.append(contenttable)
694
695        # Insert signatures
696        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
697        # do not have a test for the following lines.
698        if signatures and not sigs_in_footer:
699            data.append(Spacer(1, 20))
700            # Render one signature table per signature to
701            # get date and signature in line.
702            for signature in signatures:
703                signaturetables = get_signature_tables(signature)
704                data.append(signaturetables[0])
705
706        view.response.setHeader(
707            'Content-Type', 'application/pdf')
708        try:
709            pdf_stream = creator.create_pdf(
710                data, None, doc_title, author=author, footer=footer_text,
711                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
712        except IOError:
713            view.flash('Error in image file.')
714            return view.redirect(view.url(view.context))
715        return pdf_stream
716
717    gpa_boundaries = ((1, 'Fail'),
718                      (1.5, 'Pass'),
719                      (2.4, '3rd Class'),
720                      (3.5, '2nd Class Lower'),
721                      (4.5, '2nd Class Upper'),
722                      (5, '1st Class'))
723
724    def getClassFromCGPA(self, gpa):
725        if gpa < self.gpa_boundaries[0][0]:
726            return 0, self.gpa_boundaries[0][1]
727        if gpa < self.gpa_boundaries[1][0]:
728            return 1, self.gpa_boundaries[1][1]
729        if gpa < self.gpa_boundaries[2][0]:
730            return 2, self.gpa_boundaries[2][1]
731        if gpa < self.gpa_boundaries[3][0]:
732            return 3, self.gpa_boundaries[3][1]
733        if gpa < self.gpa_boundaries[4][0]:
734            return 4, self.gpa_boundaries[4][1]
735        if gpa <= self.gpa_boundaries[5][0]:
736            return 5, self.gpa_boundaries[5][1]
737        return 'N/A'
738
739    def renderPDFTranscript(self, view, filename='transcript.pdf',
740                  student=None,
741                  studentview=None,
742                  note=None, signatures=None, sigs_in_footer=(),
743                  show_scans=True, topMargin=1.5,
744                  omit_fields=(),
745                  tableheader=None):
746        """Render pdf slips for transcript.
747        """
748        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
749        # XXX: tell what the different parameters mean
750        style = getSampleStyleSheet()
751        creator = self.getPDFCreator(student)
752        data = []
753        doc_title = view.label
754        author = '%s (%s)' % (view.request.principal.title,
755                              view.request.principal.id)
756        footer_text = view.label.split('\n')
757        if len(footer_text) > 2:
758            # We can add a department in first line
759            footer_text = footer_text[1]
760        else:
761            # Only the first line is used for the footer
762            footer_text = footer_text[0]
763        if getattr(student, 'student_id', None) is not None:
764            footer_text = "%s - %s - " % (student.student_id, footer_text)
765
766        # Insert student data table
767        if student is not None:
768            #bd_translation = trans(_('Base Data'), portal_language)
769            #data.append(Paragraph(bd_translation, HEADING_STYLE))
770            data.append(render_student_data(
771                studentview, view.context,
772                omit_fields, lang=portal_language,
773                slipname=filename))
774
775        transcript_data = view.context.getTranscriptData()
776        levels_data = transcript_data[0]
777        gpa = transcript_data[1]
778
779        contextdata = []
780        f_label = trans(_('Course of Study:'), portal_language)
781        f_label = Paragraph(f_label, ENTRY1_STYLE)
782        f_text = formatted_text(view.context.certificate.longtitle)
783        f_text = Paragraph(f_text, ENTRY1_STYLE)
784        contextdata.append([f_label,f_text])
785
786        f_label = trans(_('Faculty:'), portal_language)
787        f_label = Paragraph(f_label, ENTRY1_STYLE)
788        f_text = formatted_text(
789            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
790        f_text = Paragraph(f_text, ENTRY1_STYLE)
791        contextdata.append([f_label,f_text])
792
793        f_label = trans(_('Department:'), portal_language)
794        f_label = Paragraph(f_label, ENTRY1_STYLE)
795        f_text = formatted_text(
796            view.context.certificate.__parent__.__parent__.longtitle)
797        f_text = Paragraph(f_text, ENTRY1_STYLE)
798        contextdata.append([f_label,f_text])
799
800        f_label = trans(_('Entry Session:'), portal_language)
801        f_label = Paragraph(f_label, ENTRY1_STYLE)
802        f_text = formatted_text(
803            view.session_dict.get(view.context.entry_session))
804        f_text = Paragraph(f_text, ENTRY1_STYLE)
805        contextdata.append([f_label,f_text])
806
807        f_label = trans(_('Entry Mode:'), portal_language)
808        f_label = Paragraph(f_label, ENTRY1_STYLE)
809        f_text = formatted_text(view.studymode_dict.get(
810            view.context.entry_mode))
811        f_text = Paragraph(f_text, ENTRY1_STYLE)
812        contextdata.append([f_label,f_text])
813
814        f_label = trans(_('Cumulative GPA:'), portal_language)
815        f_label = Paragraph(f_label, ENTRY1_STYLE)
816        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
817        f_text = Paragraph(f_text, ENTRY1_STYLE)
818        contextdata.append([f_label,f_text])
819
820        contexttable = Table(contextdata,style=SLIP_STYLE)
821        data.append(contexttable)
822
823        transcripttables = render_transcript_data(
824            view, tableheader, levels_data, lang=portal_language)
825        data.extend(transcripttables)
826
827        # Insert signatures
828        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
829        # do not have a test for the following lines.
830        if signatures and not sigs_in_footer:
831            data.append(Spacer(1, 20))
832            # Render one signature table per signature to
833            # get date and signature in line.
834            for signature in signatures:
835                signaturetables = get_signature_tables(signature)
836                data.append(signaturetables[0])
837
838        view.response.setHeader(
839            'Content-Type', 'application/pdf')
840        try:
841            pdf_stream = creator.create_pdf(
842                data, None, doc_title, author=author, footer=footer_text,
843                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
844        except IOError:
845            view.flash(_('Error in image file.'))
846            return view.redirect(view.url(view.context))
847        return pdf_stream
848
849    def maxCredits(self, studylevel):
850        """Return maximum credits.
851
852        In some universities maximum credits is not constant, it
853        depends on the student's study level.
854        """
855        return 50
856
857    def maxCreditsExceeded(self, studylevel, course):
858        max_credits = self.maxCredits(studylevel)
859        if max_credits and \
860            studylevel.total_credits + course.credits > max_credits:
861            return max_credits
862        return 0
863
864    def getBedCoordinates(self, bedticket):
865        """Return bed coordinates.
866
867        This method can be used to customize the display_coordinates
868        property method.
869        """
870        return bedticket.bed_coordinates
871
872    VERDICTS_DICT = {
873        '0': _('(not yet)'),
874        'A': 'Successful student',
875        'B': 'Student with carryover courses',
876        'C': 'Student on probation',
877        }
878
879    SEPARATORS_DICT = {
880        }
881
882    SKIP_UPLOAD_VIEWLETS = ()
883
884    PWCHANGE_STATES = (ADMITTED,)
885
886    #: A prefix used when generating new student ids. Each student id will
887    #: start with this string. The default is 'K' for ``Kofa``.
888    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.