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

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

Move logic for payment ticket creation to setPaymentDetails in StudentsUtils?.

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