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

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

Clean up code.

  • Property svn:keywords set to Id
File size: 15.8 KB
Line 
1## $Id: utils.py 8481 2012-05-21 09:28:46Z 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, PAID
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():
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 student.is_postgrad and student.state == PAID:
281                # Returning postgraduate students also pay for the next session
282                # but their level always remains the same.
283                details['p_session'] += 1
284                details['amount'] = getattr(
285                    student['studycourse'].certificate,'school_fee_2')
286        elif category == 'clearance':
287            details['p_item'] = student['studycourse'].certificate.code
288            details['amount'] = academic_session.clearance_fee
289        elif category == 'bed_allocation':
290            details['p_item'] = self.getAccommodationDetails(student)['bt']
291            details['amount'] = academic_session.booking_fee
292        if details['amount'] in (0.0, None):
293            details['error'] = _(u'Amount could not be determined.')
294        return details
295
296    def getAccommodationDetails(self, student):
297        """Determine the accommodation dates of a student.
298        """
299        d = {}
300        d['error'] = u''
301        site_configuration = grok.getSite()['configuration']
302        d['booking_session'] = site_configuration.accommodation_session
303        d['allowed_states'] = site_configuration.accommodation_states
304        # Determine bed type
305        studycourse = student['studycourse']
306        certificate = getattr(studycourse,'certificate',None)
307        entry_session = studycourse.entry_session
308        current_level = studycourse.current_level
309        if not (entry_session and current_level and certificate):
310            return
311        end_level = certificate.end_level
312        if entry_session == grok.getSite()[
313            'configuration'].accommodation_session:
314            bt = 'fr'
315        elif current_level >= end_level:
316            bt = 'fi'
317        else:
318            bt = 're'
319        if student.sex == 'f':
320            sex = 'female'
321        else:
322            sex = 'male'
323        special_handling = 'regular'
324        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
325        return d
326
327    def selectBed(self, available_beds):
328        """Select a bed from a list of available beds.
329
330        In the base configuration we select the first bed found,
331        but can also randomize the selection if we like.
332        """
333        return available_beds[0]
334
335    def renderPDF(self, view, filename='slip.pdf', student=None,
336                  studentview=None, tableheader=None, tabledata=None,
337                  note=None):
338        """Render pdf slips for various pages.
339        """
340        # XXX: we have to fix the import problems here.
341        from waeup.kofa.browser.interfaces import IPDFCreator
342        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
343        style = getSampleStyleSheet()
344        creator = getUtility(IPDFCreator)
345        data = []
346        doc_title = view.label
347        author = '%s (%s)' % (view.request.principal.title,
348                              view.request.principal.id)
349        footer_text = view.label
350        if getattr(student, 'student_id', None) is not None:
351            footer_text = "%s - %s - " % (student.student_id, footer_text)
352
353        # Insert student data table
354        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
355        if student is not None:
356            bd_translation = trans(_('Base Data'), portal_language)
357            data.append(Paragraph(bd_translation, style["Heading3"]))
358            data.append(render_student_data(studentview))
359
360        # Insert widgets
361        data.append(Paragraph(view.title, style["Heading3"]))
362        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
363        separators = getattr(self, 'SEPARATORS_DICT', {})
364        table = creator.getWidgetsTable(
365            view.form_fields, view.context, None, lang=portal_language,
366            separators=separators)
367        data.append(table)
368
369        # Insert scanned docs
370        data.extend(docs_as_flowables(view, portal_language))
371
372        # Insert content table (optionally on second page)
373        if tabledata and tableheader:
374            #data.append(PageBreak())
375            data.append(Spacer(1, 20))
376            data.append(Paragraph(view.content_title, style["Heading3"]))
377            contenttable = render_table_data(tableheader,tabledata)
378            data.append(contenttable)
379
380        view.response.setHeader(
381            'Content-Type', 'application/pdf')
382        try:
383            pdf_stream = creator.create_pdf(
384                data, None, doc_title, author=author, footer=footer_text,
385                note=note)
386        except IOError:
387            view.flash('Error in image file.')
388            return view.redirect(view.url(view.context))
389        return pdf_stream
390
391    VERDICTS_DICT = {
392        '0': _('(not yet)'),
393        'A': 'Successful student',
394        'B': 'Student with carryover courses',
395        'C': 'Student on probation',
396        }
397
398    SEPARATORS_DICT = {
399        }
400
401    #: A prefix used when generating new student ids. Each student id will
402    #: start with this string. The default is 'K' for ``Kofa``.
403    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.