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

Last change on this file since 8428 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
RevLine 
[7191]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##
[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
[8307]38from waeup.kofa.interfaces import IExtFileStore, IKofaUtils, RETURNING
[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
[8410]97def generate_student_id(students, letter='?'):
98    students = grok.getSite()['students']
99    new_id = students.unique_student_id
100    return new_id
[6742]101
[7186]102def set_up_widgets(view, ignore_request=False):
[7019]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
[7310]110def render_student_data(studentview):
[7318]111    """Render student table for an existing frame.
112    """
113    width, height = A4
[7186]114    set_up_widgets(studentview, ignore_request=True)
[7318]115    data_left = []
116    data_right = []
[7019]117    style = getSampleStyleSheet()
[7280]118    img = getUtility(IExtFileStore).getFileByContext(
119        studentview.context, attr='passport.jpg')
120    if img is None:
[7811]121        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
[7280]122        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7318]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)])
[7819]126    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7019]127    for widget in studentview.widgets:
128        if widget.name == 'form.adm_code':
129            continue
[7714]130        f_label = formatted_label(size=12) % translate(
[7811]131            widget.label.strip(), 'waeup.kofa',
[7714]132            target_language=portal_language)
[7019]133        f_label = Paragraph(f_label, style["Normal"])
[7714]134        f_text = formatted_text(widget(), size=12)
[7019]135        f_text = Paragraph(f_text, style["Normal"])
[7318]136        data_right.append([f_label,f_text])
137    table_left = Table(data_left,style=SLIP_STYLE)
[8112]138    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
[7318]139    table = Table([[table_left, table_right],],style=SLIP_STYLE)
[7019]140    return table
141
[7304]142def render_table_data(tableheader,tabledata):
[7318]143    """Render children table for an existing frame.
144    """
[7304]145    data = []
[7318]146    #data.append([Spacer(1, 12)])
[7304]147    line = []
148    style = getSampleStyleSheet()
149    for element in tableheader:
[7511]150        field = formatted_text(element[0], color='white')
[7310]151        field = Paragraph(field, style["Normal"])
[7304]152        line.append(field)
153    data.append(line)
154    for ticket in tabledata:
155        line = []
156        for element in tableheader:
[7511]157              field = formatted_text(getattr(ticket,element[1],u' '))
[7318]158              field = Paragraph(field, style["Normal"])
[7304]159              line.append(field)
160        data.append(line)
[7310]161    table = Table(data,colWidths=[
[7318]162        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
[7304]163    return table
164
[8112]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 = []
[7318]174
[8112]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)
[8120]187            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
[8112]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:
[8117]195                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
[8112]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
[7318]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)
[8183]207      tz = getUtility(IKofaUtils).tzinfo
[8234]208      timestamp = now(tz).strftime("%d/%m/%Y %H:%M:%S %Z")
[7318]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)
[7819]214      portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7714]215      right_text = translate(_('<font size=10>${a} Page ${b} of ${c}</font>',
216          mapping = {'a':text, 'b':pdf.getPageNumber(), 'c':number_of_pages}),
[7811]217          'waeup.kofa', target_language=portal_language)
[7318]218      story.append(Paragraph(right_text, style["Right"]))
219      frame_footer.addFromList(story,pdf)
220
[7150]221class StudentsUtils(grok.GlobalUtility):
222    """A collection of methods subject to customization.
223    """
224    grok.implements(IStudentsUtils)
[7019]225
[8268]226    def getReturningData(self, student):
[7841]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        """
[8268]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
[7615]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):
[7841]255        """Get the payment dates of a student for the payment category
256        specified.
257        """
[8307]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'])
[7150]265        try:
266            academic_session = grok.getSite()['configuration'][session]
267        except KeyError:
[8420]268            details['error'] = _(u'Session configuration object is not available.')
[8307]269            return details
[7150]270        if category == 'schoolfee':
[8307]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)
[7150]280        elif category == 'clearance':
[8307]281            details['p_item'] = student['studycourse'].certificate.code
282            details['amount'] = academic_session.clearance_fee
[7150]283        elif category == 'bed_allocation':
[8307]284            details['p_item'] = self.getAccommodationDetails(student)['bt']
285            details['amount'] = academic_session.booking_fee
[8420]286        if details['amount'] in (0.0, None):
287            details['error'] = _(u'Amount could not be determined.')
[8307]288        return details
[7019]289
[7186]290    def getAccommodationDetails(self, student):
[7841]291        """Determine the accommodation dates of a student.
292        """
[7150]293        d = {}
294        d['error'] = u''
[7365]295        site_configuration = grok.getSite()['configuration']
296        d['booking_session'] = site_configuration.accommodation_session
297        d['allowed_states'] = site_configuration.accommodation_states
[7150]298        # Determine bed type
299        studycourse = student['studycourse']
[7369]300        certificate = getattr(studycourse,'certificate',None)
[7150]301        entry_session = studycourse.entry_session
302        current_level = studycourse.current_level
[7369]303        if not (entry_session and current_level and certificate):
304            return
305        end_level = certificate.end_level
[8112]306        if entry_session == grok.getSite()[
307            'configuration'].accommodation_session:
[7150]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
[7019]320
[7186]321    def selectBed(self, available_beds):
[7841]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        """
[7150]327        return available_beds[0]
328
[8257]329    def renderPDF(self, view, filename='slip.pdf', student=None,
330                  studentview=None, tableheader=None, tabledata=None,
331                  note=None):
[7841]332        """Render pdf slips for various pages.
333        """
[8112]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)
[7310]343        footer_text = view.label
[7714]344        if getattr(student, 'student_id', None) is not None:
[7310]345            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]346
[7318]347        # Insert student data table
[7819]348        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7310]349        if student is not None:
[8112]350            bd_translation = trans(_('Base Data'), portal_language)
351            data.append(Paragraph(bd_translation, style["Heading3"]))
352            data.append(render_student_data(studentview))
[7304]353
[7318]354        # Insert widgets
[8112]355        data.append(Paragraph(view.title, style["Heading3"]))
[7819]356        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[8180]357        separators = getattr(self, 'SEPARATORS_DICT', {})
[8112]358        table = creator.getWidgetsTable(
[8180]359            view.form_fields, view.context, None, lang=portal_language,
360            separators=separators)
[8112]361        data.append(table)
[7318]362
[8112]363        # Insert scanned docs
364        data.extend(docs_as_flowables(view, portal_language))
[7318]365
[8141]366        # Insert content table (optionally on second page)
[7318]367        if tabledata and tableheader:
[8141]368            #data.append(PageBreak())
369            data.append(Spacer(1, 20))
[8112]370            data.append(Paragraph(view.content_title, style["Heading3"]))
[7304]371            contenttable = render_table_data(tableheader,tabledata)
[8112]372            data.append(contenttable)
[7318]373
[7150]374        view.response.setHeader(
375            'Content-Type', 'application/pdf')
[8112]376        try:
377            pdf_stream = creator.create_pdf(
[8257]378                data, None, doc_title, author=author, footer=footer_text,
379                note=note)
[8112]380        except IOError:
381            view.flash('Error in image file.')
382            return view.redirect(view.url(view.context))
383        return pdf_stream
[7620]384
[7841]385    VERDICTS_DICT = {
[7993]386        '0': _('(not yet)'),
[7841]387        'A': 'Successful student',
388        'B': 'Student with carryover courses',
389        'C': 'Student on probation',
390        }
[8099]391
392    SEPARATORS_DICT = {
393        }
[8410]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.