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

Last change on this file since 8257 was 8257, checked in by uli, 13 years ago

Support notes in generated PDFs.

  • Property svn:keywords set to Id
File size: 14.5 KB
Line 
1## $Id: utils.py 8257 2012-04-23 11:56:06Z uli $
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 random import SystemRandom as r
22from datetime import datetime
23from zope.i18n import translate
24from zope.component import getUtility
25from reportlab.pdfgen import canvas
26from reportlab.lib import colors
27from reportlab.lib.units import cm
28from reportlab.lib.enums import TA_RIGHT
29from reportlab.lib.pagesizes import A4
30from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
31from reportlab.platypus import (Frame, Paragraph, Image, PageBreak, Table,
32                                Spacer)
33from reportlab.platypus.tables import TableStyle
34from reportlab.platypus.flowables import PageBreak
35from zope.component import getUtility
36from zope.formlib.form import setUpEditWidgets
37
38from waeup.kofa.interfaces import IExtFileStore, IKofaUtils
39from waeup.kofa.interfaces import MessageFactory as _
40from waeup.kofa.students.interfaces import IStudentsUtils
41from waeup.kofa.utils.helpers import now
42
43SLIP_STYLE = [
44    ('VALIGN',(0,0),(-1,-1),'TOP'),
45    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
46    ]
47
48CONTENT_STYLE = [
49    ('VALIGN',(0,0),(-1,-1),'TOP'),
50    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
51    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
52    ('BACKGROUND',(0,0),(-1,0),colors.black),
53    ]
54
55FONT_SIZE = 10
56FONT_COLOR = 'black'
57
58def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
59    tag1 ='<font color=%s size=%d>' % (color, size)
60    return tag1 + '%s:</font>'
61
62def trans(text, lang):
63    # shortcut
64    return translate(text, 'waeup.kofa', target_language=lang)
65
66def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
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    try:
81        # In unit tests IKofaUtils has not been registered
82        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
83    except:
84        portal_language = 'en'
85    if not isinstance(text, unicode):
86        if isinstance(text, basestring):
87            text = text.decode('utf-8')
88        else:
89            text = unicode(text)
90    # Mainly for boolean values we need our customized
91    # localisation of the zope domain
92    text = translate(text, 'zope', target_language=portal_language)
93    text = text.replace('</div>', '<br /></div>')
94    tag1 = u'<font color="%s" size="%d">' % (color, size)
95    return tag1 + u'%s</font>' % text
96
97def generate_student_id(students,letter):
98    if letter == '?':
99        letter= r().choice('ABCDEFGHKLMNPQRSTUVWXY')
100    sid = u"%c%d" % (letter,r().randint(99999,1000000))
101    while sid in students.keys():
102        sid = u"%c%d" % (letter,r().randint(99999,1000000))
103    return sid
104
105def set_up_widgets(view, ignore_request=False):
106    view.adapters = {}
107    view.widgets = setUpEditWidgets(
108        view.form_fields, view.prefix, view.context, view.request,
109        adapters=view.adapters, for_display=True,
110        ignore_request=ignore_request
111        )
112
113def render_student_data(studentview):
114    """Render student table for an existing frame.
115    """
116    width, height = A4
117    set_up_widgets(studentview, ignore_request=True)
118    data_left = []
119    data_right = []
120    style = getSampleStyleSheet()
121    img = getUtility(IExtFileStore).getFileByContext(
122        studentview.context, attr='passport.jpg')
123    if img is None:
124        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
125        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
126    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
127    data_left.append([doc_img])
128    #data.append([Spacer(1, 12)])
129    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
130    for widget in studentview.widgets:
131        if widget.name == 'form.adm_code':
132            continue
133        f_label = formatted_label(size=12) % translate(
134            widget.label.strip(), 'waeup.kofa',
135            target_language=portal_language)
136        f_label = Paragraph(f_label, style["Normal"])
137        f_text = formatted_text(widget(), size=12)
138        f_text = Paragraph(f_text, style["Normal"])
139        data_right.append([f_label,f_text])
140    table_left = Table(data_left,style=SLIP_STYLE)
141    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
142    table = Table([[table_left, table_right],],style=SLIP_STYLE)
143    return table
144
145def render_table_data(tableheader,tabledata):
146    """Render children table for an existing frame.
147    """
148    data = []
149    #data.append([Spacer(1, 12)])
150    line = []
151    style = getSampleStyleSheet()
152    for element in tableheader:
153        field = formatted_text(element[0], color='white')
154        field = Paragraph(field, style["Normal"])
155        line.append(field)
156    data.append(line)
157    for ticket in tabledata:
158        line = []
159        for element in tableheader:
160              field = formatted_text(getattr(ticket,element[1],u' '))
161              field = Paragraph(field, style["Normal"])
162              line.append(field)
163        data.append(line)
164    table = Table(data,colWidths=[
165        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
166    return table
167
168def docs_as_flowables(view, lang='en'):
169    """Create reportlab flowables out of scanned docs.
170    """
171    # XXX: fix circular import problem
172    from waeup.kofa.students.viewlets import FileManager
173    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
174    from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
175    style = getSampleStyleSheet()
176    data = []
177
178    # Collect viewlets
179    fm = FileManager(view.context, view.request, view)
180    fm.update()
181    if fm.viewlets:
182        sc_translation = trans(_('Scanned Documents'), lang)
183        data.append(Paragraph(sc_translation, style["Heading3"]))
184        # Insert list of scanned documents
185        table_data = []
186        for viewlet in fm.viewlets:
187            f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
188            img_path = getattr(getUtility(IExtFileStore).getFileByContext(
189                view.context, attr=viewlet.download_name), 'name', None)
190            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
191            if img_path is None:
192                pass
193            elif not img_path.endswith('.jpg'):
194                # reportlab requires jpg images, I think.
195                f_text = Paragraph('%s (Not displayable)' % (
196                    viewlet.title,), ENTRY1_STYLE)
197            else:
198                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
199            table_data.append([f_label, f_text])
200        if table_data:
201            # safety belt; empty tables lead to problems.
202            data.append(Table(table_data, style=SLIP_STYLE))
203    return data
204
205def insert_footer(pdf,width,style,text=None, number_of_pages=1):
206      """Render the whole footer frame.
207      """
208      story = []
209      frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
210      tz = getUtility(IKofaUtils).tzinfo
211      timestamp = now(tz).strftime("%d/%m/%Y %H:%M:%S %Z")
212      left_text = '<font size=10>%s</font>' % timestamp
213      story.append(Paragraph(left_text, style["Normal"]))
214      frame_footer.addFromList(story,pdf)
215      story = []
216      frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
217      portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
218      right_text = translate(_('<font size=10>${a} Page ${b} of ${c}</font>',
219          mapping = {'a':text, 'b':pdf.getPageNumber(), 'c':number_of_pages}),
220          'waeup.kofa', target_language=portal_language)
221      story.append(Paragraph(right_text, style["Right"]))
222      frame_footer.addFromList(story,pdf)
223
224class StudentsUtils(grok.GlobalUtility):
225    """A collection of methods subject to customization.
226    """
227    grok.implements(IStudentsUtils)
228
229    def setReturningData(self, student):
230        """ This method defines what happens after school fee payment
231        depending on the student's senate verdict.
232
233        In the base configuration current level is always increased
234        by 100 no matter which verdict has been assigned.
235        """
236        student['studycourse'].current_level += 100
237        student['studycourse'].current_session += 1
238        verdict = student['studycourse'].current_verdict
239        student['studycourse'].current_verdict = '0'
240        student['studycourse'].previous_verdict = verdict
241        return
242
243    def getPaymentDetails(self, category, student):
244        """Get the payment dates of a student for the payment category
245        specified.
246        """
247        d = {}
248        d['p_item'] = u''
249        d['amount'] = 0.0
250        d['error'] = u''
251        d['p_session'] = student['studycourse'].current_session
252        session = str(d['p_session'])
253        try:
254            academic_session = grok.getSite()['configuration'][session]
255        except KeyError:
256            d['error'] = u'Session configuration object is not available.'
257            return d
258        #d['surcharge_1'] = academic_session.surcharge_1
259        #d['surcharge_2'] = academic_session.surcharge_2
260        #d['surcharge_3'] = academic_session.surcharge_3
261        if category == 'schoolfee':
262            d['amount'] = academic_session.school_fee_base
263            d['p_item'] = student['studycourse'].certificate.code
264        elif category == 'clearance':
265            d['p_item'] = student['studycourse'].certificate.code
266            d['amount'] = academic_session.clearance_fee
267            #d['surcharge_1'] = 0.0 # no portal fee
268        elif category == 'bed_allocation':
269            d['p_item'] = self.getAccommodationDetails(student)['bt']
270            d['amount'] = academic_session.booking_fee
271            #d['surcharge_1'] = 0.0 # no portal fee
272        return d
273
274    def getAccommodationDetails(self, student):
275        """Determine the accommodation dates of a student.
276        """
277        d = {}
278        d['error'] = u''
279        site_configuration = grok.getSite()['configuration']
280        d['booking_session'] = site_configuration.accommodation_session
281        d['allowed_states'] = site_configuration.accommodation_states
282        # Determine bed type
283        studycourse = student['studycourse']
284        certificate = getattr(studycourse,'certificate',None)
285        entry_session = studycourse.entry_session
286        current_level = studycourse.current_level
287        if not (entry_session and current_level and certificate):
288            return
289        end_level = certificate.end_level
290        if entry_session == grok.getSite()[
291            'configuration'].accommodation_session:
292            bt = 'fr'
293        elif current_level >= end_level:
294            bt = 'fi'
295        else:
296            bt = 're'
297        if student.sex == 'f':
298            sex = 'female'
299        else:
300            sex = 'male'
301        special_handling = 'regular'
302        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
303        return d
304
305    def selectBed(self, available_beds):
306        """Select a bed from a list of available beds.
307
308        In the base configuration we select the first bed found,
309        but can also randomize the selection if we like.
310        """
311        return available_beds[0]
312
313    def renderPDF(self, view, filename='slip.pdf', student=None,
314                  studentview=None, tableheader=None, tabledata=None,
315                  note=None):
316        """Render pdf slips for various pages.
317        """
318        # XXX: we have to fix the import problems here.
319        from waeup.kofa.browser.interfaces import IPDFCreator
320        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
321        style = getSampleStyleSheet()
322        creator = getUtility(IPDFCreator)
323        data = []
324        doc_title = view.label
325        author = '%s (%s)' % (view.request.principal.title,
326                              view.request.principal.id)
327        footer_text = view.label
328        if getattr(student, 'student_id', None) is not None:
329            footer_text = "%s - %s - " % (student.student_id, footer_text)
330
331        # Insert student data table
332        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
333        if student is not None:
334            bd_translation = trans(_('Base Data'), portal_language)
335            data.append(Paragraph(bd_translation, style["Heading3"]))
336            data.append(render_student_data(studentview))
337
338        # Insert widgets
339        data.append(Paragraph(view.title, style["Heading3"]))
340        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
341        separators = getattr(self, 'SEPARATORS_DICT', {})
342        table = creator.getWidgetsTable(
343            view.form_fields, view.context, None, lang=portal_language,
344            separators=separators)
345        data.append(table)
346
347        # Insert scanned docs
348        data.extend(docs_as_flowables(view, portal_language))
349
350        # Insert content table (optionally on second page)
351        if tabledata and tableheader:
352            #data.append(PageBreak())
353            data.append(Spacer(1, 20))
354            data.append(Paragraph(view.content_title, style["Heading3"]))
355            contenttable = render_table_data(tableheader,tabledata)
356            data.append(contenttable)
357
358        view.response.setHeader(
359            'Content-Type', 'application/pdf')
360        try:
361            pdf_stream = creator.create_pdf(
362                data, None, doc_title, author=author, footer=footer_text,
363                note=note)
364        except IOError:
365            view.flash('Error in image file.')
366            return view.redirect(view.url(view.context))
367        return pdf_stream
368
369    VERDICTS_DICT = {
370        '0': _('(not yet)'),
371        'A': 'Successful student',
372        'B': 'Student with carryover courses',
373        'C': 'Student on probation',
374        }
375
376    SEPARATORS_DICT = {
377        }
Note: See TracBrowser for help on using the repository browser.