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

Last change on this file since 8233 was 8186, checked in by uli, 12 years ago

Use new helper functions to compute pytz timezones correctly.

  • Property svn:keywords set to Id
File size: 14.4 KB
RevLine 
[7191]1## $Id: utils.py 8186 2012-04-17 00:31:10Z 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##
[7358]18"""General helper functions and utilities for the student section.
[6651]19"""
[7150]20import grok
[6662]21from random import SystemRandom as r
[7256]22from datetime import datetime
[7714]23from zope.i18n import translate
24from zope.component import getUtility
[7019]25from reportlab.pdfgen import canvas
[7318]26from reportlab.lib import colors
[7019]27from reportlab.lib.units import cm
[7310]28from reportlab.lib.enums import TA_RIGHT
[7019]29from reportlab.lib.pagesizes import A4
[7310]30from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
[8112]31from reportlab.platypus import (Frame, Paragraph, Image, PageBreak, Table,
32                                Spacer)
[7019]33from reportlab.platypus.tables import TableStyle
[7310]34from reportlab.platypus.flowables import PageBreak
[7280]35from zope.component import getUtility
[7019]36from zope.formlib.form import setUpEditWidgets
[8112]37
[7819]38from waeup.kofa.interfaces import IExtFileStore, IKofaUtils
[7811]39from waeup.kofa.interfaces import MessageFactory as _
40from waeup.kofa.students.interfaces import IStudentsUtils
[8186]41from waeup.kofa.utils.helpers import now
[6651]42
[7318]43SLIP_STYLE = [
44    ('VALIGN',(0,0),(-1,-1),'TOP'),
45    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
46    ]
[7019]47
[7318]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    ]
[7304]54
[7318]55FONT_SIZE = 10
56FONT_COLOR = 'black'
57
[7319]58def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
[7318]59    tag1 ='<font color=%s size=%d>' % (color, size)
[7319]60    return tag1 + '%s:</font>'
61
[8112]62def trans(text, lang):
63    # shortcut
64    return translate(text, 'waeup.kofa', target_language=lang)
65
[7511]66def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
67    """Turn `text`, `color` and `size` into an HTML snippet.
[7318]68
[7511]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
[7804]75    Finally, a br tag is added if widgets contain div tags
76    which are not supported by reportlab.
77
[7511]78    The returned snippet is unicode type.
79    """
[8142]80    try:
81        # In unit tests IKofaUtils has not been registered
82        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
83    except:
84        portal_language = 'en'
[7511]85    if not isinstance(text, unicode):
86        if isinstance(text, basestring):
87            text = text.decode('utf-8')
88        else:
89            text = unicode(text)
[8141]90    # Mainly for boolean values we need our customized
91    # localisation of the zope domain
92    text = translate(text, 'zope', target_language=portal_language)
[7804]93    text = text.replace('</div>', '<br /></div>')
[7511]94    tag1 = u'<font color="%s" size="%d">' % (color, size)
95    return tag1 + u'%s</font>' % text
96
[6749]97def generate_student_id(students,letter):
[6651]98    if letter == '?':
[6664]99        letter= r().choice('ABCDEFGHKLMNPQRSTUVWXY')
100    sid = u"%c%d" % (letter,r().randint(99999,1000000))
[6749]101    while sid in students.keys():
[6664]102        sid = u"%c%d" % (letter,r().randint(99999,1000000))
[6662]103    return sid
[6742]104
[7186]105def set_up_widgets(view, ignore_request=False):
[7019]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
[7310]113def render_student_data(studentview):
[7318]114    """Render student table for an existing frame.
115    """
116    width, height = A4
[7186]117    set_up_widgets(studentview, ignore_request=True)
[7318]118    data_left = []
119    data_right = []
[7019]120    style = getSampleStyleSheet()
[7280]121    img = getUtility(IExtFileStore).getFileByContext(
122        studentview.context, attr='passport.jpg')
123    if img is None:
[7811]124        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
[7280]125        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7318]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)])
[7819]129    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7019]130    for widget in studentview.widgets:
131        if widget.name == 'form.adm_code':
132            continue
[7714]133        f_label = formatted_label(size=12) % translate(
[7811]134            widget.label.strip(), 'waeup.kofa',
[7714]135            target_language=portal_language)
[7019]136        f_label = Paragraph(f_label, style["Normal"])
[7714]137        f_text = formatted_text(widget(), size=12)
[7019]138        f_text = Paragraph(f_text, style["Normal"])
[7318]139        data_right.append([f_label,f_text])
140    table_left = Table(data_left,style=SLIP_STYLE)
[8112]141    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
[7318]142    table = Table([[table_left, table_right],],style=SLIP_STYLE)
[7019]143    return table
144
[7304]145def render_table_data(tableheader,tabledata):
[7318]146    """Render children table for an existing frame.
147    """
[7304]148    data = []
[7318]149    #data.append([Spacer(1, 12)])
[7304]150    line = []
151    style = getSampleStyleSheet()
152    for element in tableheader:
[7511]153        field = formatted_text(element[0], color='white')
[7310]154        field = Paragraph(field, style["Normal"])
[7304]155        line.append(field)
156    data.append(line)
157    for ticket in tabledata:
158        line = []
159        for element in tableheader:
[7511]160              field = formatted_text(getattr(ticket,element[1],u' '))
[7318]161              field = Paragraph(field, style["Normal"])
[7304]162              line.append(field)
163        data.append(line)
[7310]164    table = Table(data,colWidths=[
[7318]165        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
[7304]166    return table
167
[8112]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 = []
[7318]177
[8112]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)
[8120]190            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
[8112]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:
[8117]198                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
[8112]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
[7318]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)
[8183]210      tz = getUtility(IKofaUtils).tzinfo
[8186]211      timestamp = now(tz).strftime("%d/%m/%Y %H:%M:%S %z %Z")
[7318]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)
[7819]217      portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7714]218      right_text = translate(_('<font size=10>${a} Page ${b} of ${c}</font>',
219          mapping = {'a':text, 'b':pdf.getPageNumber(), 'c':number_of_pages}),
[7811]220          'waeup.kofa', target_language=portal_language)
[7318]221      story.append(Paragraph(right_text, style["Right"]))
222      frame_footer.addFromList(story,pdf)
223
[7150]224class StudentsUtils(grok.GlobalUtility):
225    """A collection of methods subject to customization.
226    """
227    grok.implements(IStudentsUtils)
[7019]228
[7615]229    def setReturningData(self, student):
[7841]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        """
[7615]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):
[7841]244        """Get the payment dates of a student for the payment category
245        specified.
246        """
[7150]247        d = {}
248        d['p_item'] = u''
[7927]249        d['amount'] = 0.0
[7150]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
[7927]267            d['surcharge_1'] = 0.0 # no portal fee
[7150]268        elif category == 'bed_allocation':
[7186]269            d['p_item'] = self.getAccommodationDetails(student)['bt']
[7150]270            d['amount'] = academic_session.booking_fee
[7927]271            d['surcharge_1'] = 0.0 # no portal fee
[7150]272        return d
[7019]273
[7186]274    def getAccommodationDetails(self, student):
[7841]275        """Determine the accommodation dates of a student.
276        """
[7150]277        d = {}
278        d['error'] = u''
[7365]279        site_configuration = grok.getSite()['configuration']
280        d['booking_session'] = site_configuration.accommodation_session
281        d['allowed_states'] = site_configuration.accommodation_states
[7150]282        # Determine bed type
283        studycourse = student['studycourse']
[7369]284        certificate = getattr(studycourse,'certificate',None)
[7150]285        entry_session = studycourse.entry_session
286        current_level = studycourse.current_level
[7369]287        if not (entry_session and current_level and certificate):
288            return
289        end_level = certificate.end_level
[8112]290        if entry_session == grok.getSite()[
291            'configuration'].accommodation_session:
[7150]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
[7019]304
[7186]305    def selectBed(self, available_beds):
[7841]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        """
[7150]311        return available_beds[0]
312
[7318]313    def renderPDF(self, view, filename='slip.pdf',
[7304]314        student=None, studentview=None, tableheader=None, tabledata=None):
[7841]315        """Render pdf slips for various pages.
316        """
[8112]317        # XXX: we have to fix the import problems here.
318        from waeup.kofa.browser.interfaces import IPDFCreator
319        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
320        style = getSampleStyleSheet()
321        creator = getUtility(IPDFCreator)
322        data = []
323        doc_title = view.label
324        author = '%s (%s)' % (view.request.principal.title,
325                              view.request.principal.id)
[7310]326        footer_text = view.label
[7714]327        if getattr(student, 'student_id', None) is not None:
[7310]328            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]329
[7318]330        # Insert student data table
[7819]331        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7310]332        if student is not None:
[8112]333            bd_translation = trans(_('Base Data'), portal_language)
334            data.append(Paragraph(bd_translation, style["Heading3"]))
335            data.append(render_student_data(studentview))
[7304]336
[7318]337        # Insert widgets
[8112]338        data.append(Paragraph(view.title, style["Heading3"]))
[7819]339        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[8180]340        separators = getattr(self, 'SEPARATORS_DICT', {})
[8112]341        table = creator.getWidgetsTable(
[8180]342            view.form_fields, view.context, None, lang=portal_language,
343            separators=separators)
[8112]344        data.append(table)
[7318]345
[8112]346        # Insert scanned docs
347        data.extend(docs_as_flowables(view, portal_language))
[7318]348
[8141]349        # Insert content table (optionally on second page)
[7318]350        if tabledata and tableheader:
[8141]351            #data.append(PageBreak())
352            data.append(Spacer(1, 20))
[8112]353            data.append(Paragraph(view.content_title, style["Heading3"]))
[7304]354            contenttable = render_table_data(tableheader,tabledata)
[8112]355            data.append(contenttable)
[7318]356
[7150]357        view.response.setHeader(
358            'Content-Type', 'application/pdf')
[8112]359        try:
360            pdf_stream = creator.create_pdf(
361                data, None, doc_title, author=author, footer=footer_text)
362        except IOError:
363            view.flash('Error in image file.')
364            return view.redirect(view.url(view.context))
365        return pdf_stream
[7620]366
[7841]367    VERDICTS_DICT = {
[7993]368        '0': _('(not yet)'),
[7841]369        'A': 'Successful student',
370        'B': 'Student with carryover courses',
371        'C': 'Student on probation',
372        }
[8099]373
374    SEPARATORS_DICT = {
375        }
Note: See TracBrowser for help on using the repository browser.