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

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

Add methods for approving payments and implement pages for approving payments (work in progress).

  • Property svn:keywords set to Id
File size: 15.5 KB
Line 
1## $Id: utils.py 8420 2012-05-11 14:18:47Z 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    students = grok.getSite()['students']
99    new_id = students.unique_student_id
100    return new_id
101
102def set_up_widgets(view, ignore_request=False):
103    view.adapters = {}
104    view.widgets = setUpEditWidgets(
105        view.form_fields, view.prefix, view.context, view.request,
106        adapters=view.adapters, for_display=True,
107        ignore_request=ignore_request
108        )
109
110def render_student_data(studentview):
111    """Render student table for an existing frame.
112    """
113    width, height = A4
114    set_up_widgets(studentview, ignore_request=True)
115    data_left = []
116    data_right = []
117    style = getSampleStyleSheet()
118    img = getUtility(IExtFileStore).getFileByContext(
119        studentview.context, attr='passport.jpg')
120    if img is None:
121        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
122        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
123    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
124    data_left.append([doc_img])
125    #data.append([Spacer(1, 12)])
126    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
127    for widget in studentview.widgets:
128        if widget.name == 'form.adm_code':
129            continue
130        f_label = formatted_label(size=12) % translate(
131            widget.label.strip(), 'waeup.kofa',
132            target_language=portal_language)
133        f_label = Paragraph(f_label, style["Normal"])
134        f_text = formatted_text(widget(), size=12)
135        f_text = Paragraph(f_text, style["Normal"])
136        data_right.append([f_label,f_text])
137    table_left = Table(data_left,style=SLIP_STYLE)
138    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
139    table = Table([[table_left, table_right],],style=SLIP_STYLE)
140    return table
141
142def render_table_data(tableheader,tabledata):
143    """Render children table for an existing frame.
144    """
145    data = []
146    #data.append([Spacer(1, 12)])
147    line = []
148    style = getSampleStyleSheet()
149    for element in tableheader:
150        field = formatted_text(element[0], color='white')
151        field = Paragraph(field, style["Normal"])
152        line.append(field)
153    data.append(line)
154    for ticket in tabledata:
155        line = []
156        for element in tableheader:
157              field = formatted_text(getattr(ticket,element[1],u' '))
158              field = Paragraph(field, style["Normal"])
159              line.append(field)
160        data.append(line)
161    table = Table(data,colWidths=[
162        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
163    return table
164
165def docs_as_flowables(view, lang='en'):
166    """Create reportlab flowables out of scanned docs.
167    """
168    # XXX: fix circular import problem
169    from waeup.kofa.students.viewlets import FileManager
170    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
171    from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
172    style = getSampleStyleSheet()
173    data = []
174
175    # Collect viewlets
176    fm = FileManager(view.context, view.request, view)
177    fm.update()
178    if fm.viewlets:
179        sc_translation = trans(_('Scanned Documents'), lang)
180        data.append(Paragraph(sc_translation, style["Heading3"]))
181        # Insert list of scanned documents
182        table_data = []
183        for viewlet in fm.viewlets:
184            f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
185            img_path = getattr(getUtility(IExtFileStore).getFileByContext(
186                view.context, attr=viewlet.download_name), 'name', None)
187            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
188            if img_path is None:
189                pass
190            elif not img_path.endswith('.jpg'):
191                # reportlab requires jpg images, I think.
192                f_text = Paragraph('%s (Not displayable)' % (
193                    viewlet.title,), ENTRY1_STYLE)
194            else:
195                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
196            table_data.append([f_label, f_text])
197        if table_data:
198            # safety belt; empty tables lead to problems.
199            data.append(Table(table_data, style=SLIP_STYLE))
200    return data
201
202def insert_footer(pdf,width,style,text=None, number_of_pages=1):
203      """Render the whole footer frame.
204      """
205      story = []
206      frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
207      tz = getUtility(IKofaUtils).tzinfo
208      timestamp = now(tz).strftime("%d/%m/%Y %H:%M:%S %Z")
209      left_text = '<font size=10>%s</font>' % timestamp
210      story.append(Paragraph(left_text, style["Normal"]))
211      frame_footer.addFromList(story,pdf)
212      story = []
213      frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
214      portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
215      right_text = translate(_('<font size=10>${a} Page ${b} of ${c}</font>',
216          mapping = {'a':text, 'b':pdf.getPageNumber(), 'c':number_of_pages}),
217          'waeup.kofa', target_language=portal_language)
218      story.append(Paragraph(right_text, style["Right"]))
219      frame_footer.addFromList(story,pdf)
220
221class StudentsUtils(grok.GlobalUtility):
222    """A collection of methods subject to customization.
223    """
224    grok.implements(IStudentsUtils)
225
226    def getReturningData(self, student):
227        """ This method defines what happens after school fee payment
228        depending on the student's senate verdict.
229
230        In the base configuration current level is always increased
231        by 100 no matter which verdict has been assigned.
232        """
233        new_level = student['studycourse'].current_level + 100
234        new_session = student['studycourse'].current_session + 1
235        return new_session, new_level
236
237    def setReturningData(self, student):
238        """ This method defines what happens after school fee payment
239        depending on the student's senate verdict. It folllows
240        the same algorithm as getReturningData but it also sets the new
241        values
242
243        In the base configuration current level is always increased
244        by 100 no matter which verdict has been assigned.
245        """
246        new_session, new_level = self.getReturningData(student)
247        student['studycourse'].current_level = new_level
248        student['studycourse'].current_session = new_session
249        verdict = student['studycourse'].current_verdict
250        student['studycourse'].current_verdict = '0'
251        student['studycourse'].previous_verdict = verdict
252        return
253
254    def getPaymentDetails(self, category, student):
255        """Get the payment dates of a student for the payment category
256        specified.
257        """
258        details = {}
259        details['p_item'] = u''
260        details['amount'] = 0.0
261        details['error'] = u''
262        details['p_session'] = student['studycourse'].current_session
263        details['p_level'] = student['studycourse'].current_level
264        session = str(details['p_session'])
265        try:
266            academic_session = grok.getSite()['configuration'][session]
267        except KeyError:
268            details['error'] = _(u'Session configuration object is not available.')
269            return details
270        if category == 'schoolfee':
271            details['amount'] = getattr(
272                student['studycourse'].certificate,'school_fee_1')
273            details['p_item'] = student['studycourse'].certificate.code
274            if student.state == RETURNING:
275                # In case of returning school fee payment the payment session
276                # and level contain the values of the session the student
277                # has paid for.
278                details['p_session'], details[
279                    'p_level'] = self.getReturningData(student)
280        elif category == 'clearance':
281            details['p_item'] = student['studycourse'].certificate.code
282            details['amount'] = academic_session.clearance_fee
283        elif category == 'bed_allocation':
284            details['p_item'] = self.getAccommodationDetails(student)['bt']
285            details['amount'] = academic_session.booking_fee
286        if details['amount'] in (0.0, None):
287            details['error'] = _(u'Amount could not be determined.')
288        return details
289
290    def getAccommodationDetails(self, student):
291        """Determine the accommodation dates of a student.
292        """
293        d = {}
294        d['error'] = u''
295        site_configuration = grok.getSite()['configuration']
296        d['booking_session'] = site_configuration.accommodation_session
297        d['allowed_states'] = site_configuration.accommodation_states
298        # Determine bed type
299        studycourse = student['studycourse']
300        certificate = getattr(studycourse,'certificate',None)
301        entry_session = studycourse.entry_session
302        current_level = studycourse.current_level
303        if not (entry_session and current_level and certificate):
304            return
305        end_level = certificate.end_level
306        if entry_session == grok.getSite()[
307            'configuration'].accommodation_session:
308            bt = 'fr'
309        elif current_level >= end_level:
310            bt = 'fi'
311        else:
312            bt = 're'
313        if student.sex == 'f':
314            sex = 'female'
315        else:
316            sex = 'male'
317        special_handling = 'regular'
318        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
319        return d
320
321    def selectBed(self, available_beds):
322        """Select a bed from a list of available beds.
323
324        In the base configuration we select the first bed found,
325        but can also randomize the selection if we like.
326        """
327        return available_beds[0]
328
329    def renderPDF(self, view, filename='slip.pdf', student=None,
330                  studentview=None, tableheader=None, tabledata=None,
331                  note=None):
332        """Render pdf slips for various pages.
333        """
334        # XXX: we have to fix the import problems here.
335        from waeup.kofa.browser.interfaces import IPDFCreator
336        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
337        style = getSampleStyleSheet()
338        creator = getUtility(IPDFCreator)
339        data = []
340        doc_title = view.label
341        author = '%s (%s)' % (view.request.principal.title,
342                              view.request.principal.id)
343        footer_text = view.label
344        if getattr(student, 'student_id', None) is not None:
345            footer_text = "%s - %s - " % (student.student_id, footer_text)
346
347        # Insert student data table
348        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
349        if student is not None:
350            bd_translation = trans(_('Base Data'), portal_language)
351            data.append(Paragraph(bd_translation, style["Heading3"]))
352            data.append(render_student_data(studentview))
353
354        # Insert widgets
355        data.append(Paragraph(view.title, style["Heading3"]))
356        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
357        separators = getattr(self, 'SEPARATORS_DICT', {})
358        table = creator.getWidgetsTable(
359            view.form_fields, view.context, None, lang=portal_language,
360            separators=separators)
361        data.append(table)
362
363        # Insert scanned docs
364        data.extend(docs_as_flowables(view, portal_language))
365
366        # Insert content table (optionally on second page)
367        if tabledata and tableheader:
368            #data.append(PageBreak())
369            data.append(Spacer(1, 20))
370            data.append(Paragraph(view.content_title, style["Heading3"]))
371            contenttable = render_table_data(tableheader,tabledata)
372            data.append(contenttable)
373
374        view.response.setHeader(
375            'Content-Type', 'application/pdf')
376        try:
377            pdf_stream = creator.create_pdf(
378                data, None, doc_title, author=author, footer=footer_text,
379                note=note)
380        except IOError:
381            view.flash('Error in image file.')
382            return view.redirect(view.url(view.context))
383        return pdf_stream
384
385    VERDICTS_DICT = {
386        '0': _('(not yet)'),
387        'A': 'Successful student',
388        'B': 'Student with carryover courses',
389        'C': 'Student on probation',
390        }
391
392    SEPARATORS_DICT = {
393        }
394
395    #: A prefix used when generating new student ids. Each student id will
396    #: start with this string. The default is 'K' for ``Kofa``.
397    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.