source: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/students/utils.py @ 10021

Last change on this file since 10021 was 9211, checked in by uli, 12 years ago

Rollback r9209. Looks like multiple merges from trunk confuse svn when merging back into trunk.

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