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

Last change on this file since 9132 was 9069, checked in by Henrik Bettermann, 12 years ago

Do not show history on payment slips.

  • Property svn:keywords set to Id
File size: 17.2 KB
Line 
1## $Id: utils.py 9069 2012-07-26 16:27:20Z 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.component import getUtility, createObject
28from zope.formlib.form import setUpEditWidgets
29from zope.i18n import translate
30from waeup.kofa.interfaces import (
31    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED)
32from waeup.kofa.interfaces import MessageFactory as _
33from waeup.kofa.students.interfaces import IStudentsUtils
34
35SLIP_STYLE = [
36    ('VALIGN',(0,0),(-1,-1),'TOP'),
37    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
38    ]
39
40CONTENT_STYLE = [
41    ('VALIGN',(0,0),(-1,-1),'TOP'),
42    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
43    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
44    ('BACKGROUND',(0,0),(-1,0),colors.black),
45    ]
46
47FONT_SIZE = 10
48FONT_COLOR = 'black'
49
50def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
51    tag1 ='<font color=%s size=%d>' % (color, size)
52    return tag1 + '%s:</font>'
53
54def trans(text, lang):
55    # shortcut
56    return translate(text, 'waeup.kofa', target_language=lang)
57
58def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
59    """Turn `text`, `color` and `size` into an HTML snippet.
60
61    The snippet is suitable for use with reportlab and generating PDFs.
62    Wraps the `text` into a ``<font>`` tag with passed attributes.
63
64    Also non-strings are converted. Raw strings are expected to be
65    utf-8 encoded (usually the case for widgets etc.).
66
67    Finally, a br tag is added if widgets contain div tags
68    which are not supported by reportlab.
69
70    The returned snippet is unicode type.
71    """
72    try:
73        # In unit tests IKofaUtils has not been registered
74        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
75    except:
76        portal_language = 'en'
77    if not isinstance(text, unicode):
78        if isinstance(text, basestring):
79            text = text.decode('utf-8')
80        else:
81            text = unicode(text)
82    # Mainly for boolean values we need our customized
83    # localisation of the zope domain
84    text = translate(text, 'zope', target_language=portal_language)
85    text = text.replace('</div>', '<br /></div>')
86    tag1 = u'<font color="%s" size="%d">' % (color, size)
87    return tag1 + u'%s</font>' % text
88
89def generate_student_id():
90    students = grok.getSite()['students']
91    new_id = students.unique_student_id
92    return new_id
93
94def set_up_widgets(view, ignore_request=False):
95    view.adapters = {}
96    view.widgets = setUpEditWidgets(
97        view.form_fields, view.prefix, view.context, view.request,
98        adapters=view.adapters, for_display=True,
99        ignore_request=ignore_request
100        )
101
102def render_student_data(studentview):
103    """Render student table for an existing frame.
104    """
105    width, height = A4
106    set_up_widgets(studentview, ignore_request=True)
107    data_left = []
108    data_right = []
109    style = getSampleStyleSheet()
110    img = getUtility(IExtFileStore).getFileByContext(
111        studentview.context, attr='passport.jpg')
112    if img is None:
113        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
114        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
115    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
116    data_left.append([doc_img])
117    #data.append([Spacer(1, 12)])
118    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
119    for widget in studentview.widgets:
120        if widget.name == 'form.adm_code':
121            continue
122        f_label = formatted_label(size=12) % translate(
123            widget.label.strip(), 'waeup.kofa',
124            target_language=portal_language)
125        f_label = Paragraph(f_label, style["Normal"])
126        f_text = formatted_text(widget(), size=12)
127        f_text = Paragraph(f_text, style["Normal"])
128        data_right.append([f_label,f_text])
129    table_left = Table(data_left,style=SLIP_STYLE)
130    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
131    table = Table([[table_left, table_right],],style=SLIP_STYLE)
132    return table
133
134def render_table_data(tableheader,tabledata):
135    """Render children table for an existing frame.
136    """
137    data = []
138    #data.append([Spacer(1, 12)])
139    line = []
140    style = getSampleStyleSheet()
141    for element in tableheader:
142        field = formatted_text(element[0], color='white')
143        field = Paragraph(field, style["Normal"])
144        line.append(field)
145    data.append(line)
146    for ticket in tabledata:
147        line = []
148        for element in tableheader:
149              field = formatted_text(getattr(ticket,element[1],u' '))
150              field = Paragraph(field, style["Normal"])
151              line.append(field)
152        data.append(line)
153    table = Table(data,colWidths=[
154        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
155    return table
156
157def get_signature_table(signatures, lang='en'):
158    """Return a reportlab table containing signature fields (with date).
159    """
160    style = getSampleStyleSheet()
161    space_width = 0.4  # width in cm of space between signatures
162    table_width = 16.0 # supposed width of signature table in cms
163    # width of signature cells in cm...
164    sig_col_width = table_width - ((len(signatures) - 1) * space_width)
165    sig_col_width = sig_col_width / len(signatures)
166    data = []
167    col_widths = [] # widths of columns
168
169    sig_style = [
170        ('VALIGN',(0,-1),(-1,-1),'TOP'),
171        ('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
172        ('BOTTOMPADDING', (0,0), (-1,0), 36),
173        ('TOPPADDING', (0,-1), (-1,-1), 0),
174        ]
175    for num, elem in enumerate(signatures):
176        # draw a line above each signature cell (not: empty cells in between)
177        sig_style.append(
178            ('LINEABOVE', (num*2,-1), (num*2, -1), 1, colors.black))
179
180    row = []
181    for signature in signatures:
182        row.append(trans(_('Date:'), lang))
183        row.append('')
184        if len(signatures) > 1:
185            col_widths.extend([sig_col_width*cm, space_width*cm])
186        else:
187            col_widths.extend([sig_col_width/2*cm, sig_col_width/2*cm])
188            row.append('') # empty spaceholder on right
189    data.append(row[:-1])
190    data.extend(([''],)*3) # insert 3 empty rows...
191    row = []
192    for signature in signatures:
193        row.append(Paragraph(trans(signature, lang), style["Normal"]))
194        row.append('')
195    data.append(row[:-1])
196    table = Table(data, style=sig_style, repeatRows=len(data),
197                  colWidths=col_widths)
198    return table
199
200def docs_as_flowables(view, lang='en'):
201    """Create reportlab flowables out of scanned docs.
202    """
203    # XXX: fix circular import problem
204    from waeup.kofa.students.viewlets import FileManager
205    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
206    from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
207    style = getSampleStyleSheet()
208    data = []
209
210    # Collect viewlets
211    fm = FileManager(view.context, view.request, view)
212    fm.update()
213    if fm.viewlets:
214        sc_translation = trans(_('Scanned Documents'), lang)
215        data.append(Paragraph(sc_translation, style["Heading3"]))
216        # Insert list of scanned documents
217        table_data = []
218        for viewlet in fm.viewlets:
219            f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
220            img_path = getattr(getUtility(IExtFileStore).getFileByContext(
221                view.context, attr=viewlet.download_name), 'name', None)
222            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
223            if img_path is None:
224                pass
225            elif not img_path[-4:] in ('.jpg', '.JPG'):
226                # reportlab requires jpg images, I think.
227                f_text = Paragraph('%s (not displayable)' % (
228                    viewlet.title,), ENTRY1_STYLE)
229            else:
230                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
231            table_data.append([f_label, f_text])
232        if table_data:
233            # safety belt; empty tables lead to problems.
234            data.append(Table(table_data, style=SLIP_STYLE))
235    return data
236
237class StudentsUtils(grok.GlobalUtility):
238    """A collection of methods subject to customization.
239    """
240    grok.implements(IStudentsUtils)
241
242    def getReturningData(self, student):
243        """ Define what happens after school fee payment
244        depending on the student's senate verdict.
245
246        In the base configuration current level is always increased
247        by 100 no matter which verdict has been assigned.
248        """
249        new_level = student['studycourse'].current_level + 100
250        new_session = student['studycourse'].current_session + 1
251        return new_session, new_level
252
253    def setReturningData(self, student):
254        """ Define what happens after school fee payment
255        depending on the student's senate verdict.
256
257        This method folllows the same algorithm as getReturningData but
258        it also sets the new values.
259        """
260        new_session, new_level = self.getReturningData(student)
261        student['studycourse'].current_level = new_level
262        student['studycourse'].current_session = new_session
263        verdict = student['studycourse'].current_verdict
264        student['studycourse'].current_verdict = '0'
265        student['studycourse'].previous_verdict = verdict
266        return
267
268    def setPaymentDetails(self, category, student):
269        """Create Payment object and set the payment data of a student for
270        the payment category specified.
271
272        """
273        p_item = u''
274        amount = 0.0
275        p_session = student['studycourse'].current_session
276        p_level = student['studycourse'].current_level
277        session = str(p_session)
278        try:
279            academic_session = grok.getSite()['configuration'][session]
280        except KeyError:
281            return _(u'Session configuration object is not available.'), None
282        if category == 'schoolfee':
283            try:
284                certificate = student['studycourse'].certificate
285                p_item = certificate.code
286            except (AttributeError, TypeError):
287                return _('Study course data are incomplete.'), None
288            if student.state == CLEARED:
289                amount = getattr(certificate, 'school_fee_1', 0.0)
290            elif student.state == RETURNING:
291                # In case of returning school fee payment the payment session
292                # and level contain the values of the session the student
293                # has paid for.
294                p_session, p_level = self.getReturningData(student)
295                amount = getattr(certificate, 'school_fee_2', 0.0)
296            elif student.is_postgrad and student.state == PAID:
297                # Returning postgraduate students also pay for the next session
298                # but their level always remains the same.
299                p_session += 1
300                amount = getattr(certificate, 'school_fee_2', 0.0)
301        elif category == 'clearance':
302            p_item = student['studycourse'].certificate.code
303            amount = academic_session.clearance_fee
304        elif category == 'bed_allocation':
305            p_item = self.getAccommodationDetails(student)['bt']
306            amount = academic_session.booking_fee
307        if amount in (0.0, None):
308            return _(u'Amount could not be determined.'), None
309        for key in student['payments'].keys():
310            ticket = student['payments'][key]
311            if ticket.p_state == 'paid' and\
312               ticket.p_category == category and \
313               ticket.p_item == p_item and \
314               ticket.p_session == p_session:
315                  return _('This type of payment has already been made.'), None
316        payment = createObject(u'waeup.StudentOnlinePayment')
317        timestamp = ("%d" % int(time()*10000))[1:]
318        payment.p_id = "p%s" % timestamp
319        payment.p_category = category
320        payment.p_item = p_item
321        payment.p_session = p_session
322        payment.p_level = p_level
323        payment.amount_auth = amount
324        return None, payment
325
326    def getAccommodationDetails(self, student):
327        """Determine the accommodation dates of a student.
328        """
329        d = {}
330        d['error'] = u''
331        hostels = grok.getSite()['hostels']
332        d['booking_session'] = hostels.accommodation_session
333        d['allowed_states'] = hostels.accommodation_states
334        d['startdate'] = hostels.startdate
335        d['enddate'] = hostels.enddate
336        d['expired'] = hostels.expired
337        # Determine bed type
338        studycourse = student['studycourse']
339        certificate = getattr(studycourse,'certificate',None)
340        entry_session = studycourse.entry_session
341        current_level = studycourse.current_level
342        if not (entry_session and current_level and certificate):
343            return
344        end_level = certificate.end_level
345        if entry_session == grok.getSite()['hostels'].accommodation_session:
346            bt = 'fr'
347        elif current_level >= end_level:
348            bt = 'fi'
349        else:
350            bt = 're'
351        if student.sex == 'f':
352            sex = 'female'
353        else:
354            sex = 'male'
355        special_handling = 'regular'
356        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
357        return d
358
359    def selectBed(self, available_beds):
360        """Select a bed from a list of available beds.
361
362        In the base configuration we select the first bed found,
363        but can also randomize the selection if we like.
364        """
365        return available_beds[0]
366
367    def renderPDF(self, view, filename='slip.pdf', student=None,
368                  studentview=None, tableheader=None, tabledata=None,
369                  note=None, signatures=None):
370        """Render pdf slips for various pages.
371        """
372        # XXX: we have to fix the import problems here.
373        from waeup.kofa.browser.interfaces import IPDFCreator
374        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
375        style = getSampleStyleSheet()
376        creator = getUtility(IPDFCreator)
377        data = []
378        doc_title = view.label
379        author = '%s (%s)' % (view.request.principal.title,
380                              view.request.principal.id)
381        footer_text = view.label
382        if getattr(student, 'student_id', None) is not None:
383            footer_text = "%s - %s - " % (student.student_id, footer_text)
384
385        # Insert history
386        if not filename.startswith('payment'):
387            data.extend(creator.fromStringList(student.history.messages))
388
389        # Insert student data table
390        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
391        if student is not None:
392            bd_translation = trans(_('Base Data'), portal_language)
393            data.append(Paragraph(bd_translation, style["Heading3"]))
394            data.append(render_student_data(studentview))
395
396        # Insert widgets
397        data.append(Paragraph(view.title, style["Heading3"]))
398        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
399        separators = getattr(self, 'SEPARATORS_DICT', {})
400        table = creator.getWidgetsTable(
401            view.form_fields, view.context, None, lang=portal_language,
402            separators=separators)
403        data.append(table)
404
405        # Insert scanned docs
406        data.extend(docs_as_flowables(view, portal_language))
407
408        # Insert content table (optionally on second page)
409        if tabledata and tableheader:
410            #data.append(PageBreak())
411            data.append(Spacer(1, 20))
412            data.append(Paragraph(view.content_title, style["Heading3"]))
413            contenttable = render_table_data(tableheader,tabledata)
414            data.append(contenttable)
415
416        # Insert signatures
417        if signatures:
418            data.append(Spacer(1, 20))
419            signaturetable = get_signature_table(signatures)
420            data.append(signaturetable)
421
422        view.response.setHeader(
423            'Content-Type', 'application/pdf')
424        try:
425            pdf_stream = creator.create_pdf(
426                data, None, doc_title, author=author, footer=footer_text,
427                note=note)
428        except IOError:
429            view.flash('Error in image file.')
430            return view.redirect(view.url(view.context))
431        return pdf_stream
432
433    VERDICTS_DICT = {
434        '0': _('(not yet)'),
435        'A': 'Successful student',
436        'B': 'Student with carryover courses',
437        'C': 'Student on probation',
438        }
439
440    SEPARATORS_DICT = {
441        }
442
443    #: A prefix used when generating new student ids. Each student id will
444    #: start with this string. The default is 'K' for ``Kofa``.
445    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.