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

Last change on this file since 8351 was 8307, checked in by Henrik Bettermann, 13 years ago

Remove school fee from session configuration. School fees are now attributes of the certificates.

Adjust and improve tests.

Fix getPaymentDetails.

  • Property svn:keywords set to Id
File size: 15.3 KB
Line 
1## $Id: utils.py 8307 2012-04-29 06:51:19Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""General helper functions and utilities for the 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, RETURNING
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 getReturningData(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        new_level = student['studycourse'].current_level + 100
237        new_session = student['studycourse'].current_session + 1
238        return new_session, new_level
239
240    def setReturningData(self, student):
241        """ This method defines what happens after school fee payment
242        depending on the student's senate verdict. It folllows
243        the same algorithm as getReturningData but it also sets the new
244        values
245
246        In the base configuration current level is always increased
247        by 100 no matter which verdict has been assigned.
248        """
249        new_session, new_level = self.getReturningData(student)
250        student['studycourse'].current_level = new_level
251        student['studycourse'].current_session = new_session
252        verdict = student['studycourse'].current_verdict
253        student['studycourse'].current_verdict = '0'
254        student['studycourse'].previous_verdict = verdict
255        return
256
257    def getPaymentDetails(self, category, student):
258        """Get the payment dates of a student for the payment category
259        specified.
260        """
261        details = {}
262        details['p_item'] = u''
263        details['amount'] = 0.0
264        details['error'] = u''
265        details['p_session'] = student['studycourse'].current_session
266        details['p_level'] = student['studycourse'].current_level
267        session = str(details['p_session'])
268        try:
269            academic_session = grok.getSite()['configuration'][session]
270        except KeyError:
271            details['error'] = u'Session configuration object is not available.'
272            return details
273        if category == 'schoolfee':
274            details['amount'] = getattr(
275                student['studycourse'].certificate,'school_fee_1')
276            details['p_item'] = student['studycourse'].certificate.code
277            if student.state == RETURNING:
278                # In case of returning school fee payment the payment session
279                # and level contain the values of the session the student
280                # has paid for.
281                details['p_session'], details[
282                    'p_level'] = self.getReturningData(student)
283        elif category == 'clearance':
284            details['p_item'] = student['studycourse'].certificate.code
285            details['amount'] = academic_session.clearance_fee
286        elif category == 'bed_allocation':
287            details['p_item'] = self.getAccommodationDetails(student)['bt']
288            details['amount'] = academic_session.booking_fee
289        return details
290
291    def getAccommodationDetails(self, student):
292        """Determine the accommodation dates of a student.
293        """
294        d = {}
295        d['error'] = u''
296        site_configuration = grok.getSite()['configuration']
297        d['booking_session'] = site_configuration.accommodation_session
298        d['allowed_states'] = site_configuration.accommodation_states
299        # Determine bed type
300        studycourse = student['studycourse']
301        certificate = getattr(studycourse,'certificate',None)
302        entry_session = studycourse.entry_session
303        current_level = studycourse.current_level
304        if not (entry_session and current_level and certificate):
305            return
306        end_level = certificate.end_level
307        if entry_session == grok.getSite()[
308            'configuration'].accommodation_session:
309            bt = 'fr'
310        elif current_level >= end_level:
311            bt = 'fi'
312        else:
313            bt = 're'
314        if student.sex == 'f':
315            sex = 'female'
316        else:
317            sex = 'male'
318        special_handling = 'regular'
319        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
320        return d
321
322    def selectBed(self, available_beds):
323        """Select a bed from a list of available beds.
324
325        In the base configuration we select the first bed found,
326        but can also randomize the selection if we like.
327        """
328        return available_beds[0]
329
330    def renderPDF(self, view, filename='slip.pdf', student=None,
331                  studentview=None, tableheader=None, tabledata=None,
332                  note=None):
333        """Render pdf slips for various pages.
334        """
335        # XXX: we have to fix the import problems here.
336        from waeup.kofa.browser.interfaces import IPDFCreator
337        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
338        style = getSampleStyleSheet()
339        creator = getUtility(IPDFCreator)
340        data = []
341        doc_title = view.label
342        author = '%s (%s)' % (view.request.principal.title,
343                              view.request.principal.id)
344        footer_text = view.label
345        if getattr(student, 'student_id', None) is not None:
346            footer_text = "%s - %s - " % (student.student_id, footer_text)
347
348        # Insert student data table
349        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
350        if student is not None:
351            bd_translation = trans(_('Base Data'), portal_language)
352            data.append(Paragraph(bd_translation, style["Heading3"]))
353            data.append(render_student_data(studentview))
354
355        # Insert widgets
356        data.append(Paragraph(view.title, style["Heading3"]))
357        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
358        separators = getattr(self, 'SEPARATORS_DICT', {})
359        table = creator.getWidgetsTable(
360            view.form_fields, view.context, None, lang=portal_language,
361            separators=separators)
362        data.append(table)
363
364        # Insert scanned docs
365        data.extend(docs_as_flowables(view, portal_language))
366
367        # Insert content table (optionally on second page)
368        if tabledata and tableheader:
369            #data.append(PageBreak())
370            data.append(Spacer(1, 20))
371            data.append(Paragraph(view.content_title, style["Heading3"]))
372            contenttable = render_table_data(tableheader,tabledata)
373            data.append(contenttable)
374
375        view.response.setHeader(
376            'Content-Type', 'application/pdf')
377        try:
378            pdf_stream = creator.create_pdf(
379                data, None, doc_title, author=author, footer=footer_text,
380                note=note)
381        except IOError:
382            view.flash('Error in image file.')
383            return view.redirect(view.url(view.context))
384        return pdf_stream
385
386    VERDICTS_DICT = {
387        '0': _('(not yet)'),
388        'A': 'Successful student',
389        'B': 'Student with carryover courses',
390        'C': 'Student on probation',
391        }
392
393    SEPARATORS_DICT = {
394        }
Note: See TracBrowser for help on using the repository browser.