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

Last change on this file since 8703 was 8688, checked in by Henrik Bettermann, 13 years ago

Check if booking perios is expired before allocating a bed.

To do: The update method of BedTicketAddPage? is too long and code has to be split and moved to data models.

  • Property svn:keywords set to Id
File size: 16.7 KB
Line 
1## $Id: utils.py 8688 2012-06-12 09:58:03Z 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 (
40    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED)
41from waeup.kofa.interfaces import MessageFactory as _
42from waeup.kofa.students.interfaces import IStudentsUtils
43from waeup.kofa.utils.helpers import now
44
45SLIP_STYLE = [
46    ('VALIGN',(0,0),(-1,-1),'TOP'),
47    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
48    ]
49
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    ]
56
57FONT_SIZE = 10
58FONT_COLOR = 'black'
59
60def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
61    tag1 ='<font color=%s size=%d>' % (color, size)
62    return tag1 + '%s:</font>'
63
64def trans(text, lang):
65    # shortcut
66    return translate(text, 'waeup.kofa', target_language=lang)
67
68def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
69    """Turn `text`, `color` and `size` into an HTML snippet.
70
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
77    Finally, a br tag is added if widgets contain div tags
78    which are not supported by reportlab.
79
80    The returned snippet is unicode type.
81    """
82    try:
83        # In unit tests IKofaUtils has not been registered
84        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
85    except:
86        portal_language = 'en'
87    if not isinstance(text, unicode):
88        if isinstance(text, basestring):
89            text = text.decode('utf-8')
90        else:
91            text = unicode(text)
92    # Mainly for boolean values we need our customized
93    # localisation of the zope domain
94    text = translate(text, 'zope', target_language=portal_language)
95    text = text.replace('</div>', '<br /></div>')
96    tag1 = u'<font color="%s" size="%d">' % (color, size)
97    return tag1 + u'%s</font>' % text
98
99def generate_student_id():
100    students = grok.getSite()['students']
101    new_id = students.unique_student_id
102    return new_id
103
104def set_up_widgets(view, ignore_request=False):
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
112def render_student_data(studentview):
113    """Render student table for an existing frame.
114    """
115    width, height = A4
116    set_up_widgets(studentview, ignore_request=True)
117    data_left = []
118    data_right = []
119    style = getSampleStyleSheet()
120    img = getUtility(IExtFileStore).getFileByContext(
121        studentview.context, attr='passport.jpg')
122    if img is None:
123        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
124        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
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)])
128    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
129    for widget in studentview.widgets:
130        if widget.name == 'form.adm_code':
131            continue
132        f_label = formatted_label(size=12) % translate(
133            widget.label.strip(), 'waeup.kofa',
134            target_language=portal_language)
135        f_label = Paragraph(f_label, style["Normal"])
136        f_text = formatted_text(widget(), size=12)
137        f_text = Paragraph(f_text, style["Normal"])
138        data_right.append([f_label,f_text])
139    table_left = Table(data_left,style=SLIP_STYLE)
140    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
141    table = Table([[table_left, table_right],],style=SLIP_STYLE)
142    return table
143
144def render_table_data(tableheader,tabledata):
145    """Render children table for an existing frame.
146    """
147    data = []
148    #data.append([Spacer(1, 12)])
149    line = []
150    style = getSampleStyleSheet()
151    for element in tableheader:
152        field = formatted_text(element[0], color='white')
153        field = Paragraph(field, style["Normal"])
154        line.append(field)
155    data.append(line)
156    for ticket in tabledata:
157        line = []
158        for element in tableheader:
159              field = formatted_text(getattr(ticket,element[1],u' '))
160              field = Paragraph(field, style["Normal"])
161              line.append(field)
162        data.append(line)
163    table = Table(data,colWidths=[
164        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
165    return table
166
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 = []
176
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)
189            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
190            if img_path is None:
191                pass
192            elif not img_path.endswith('.jpg'):
193                # reportlab requires jpg images, I think.
194                f_text = Paragraph('%s (Not displayable)' % (
195                    viewlet.title,), ENTRY1_STYLE)
196            else:
197                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
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
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
223class StudentsUtils(grok.GlobalUtility):
224    """A collection of methods subject to customization.
225    """
226    grok.implements(IStudentsUtils)
227    paymentfactory = u'waeup.StudentOnlinePayment'
228
229    def getReturningData(self, student):
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        """
236        new_level = student['studycourse'].current_level + 100
237        new_session = student['studycourse'].current_session + 1
238        return new_session, new_level
239
240    def setReturningData(self, student):
241        """ This method defines what happens after school fee payment
242        depending on the student's senate verdict. It folllows
243        the same algorithm as getReturningData but it also sets the new
244        values
245
246        In the base configuration current level is always increased
247        by 100 no matter which verdict has been assigned.
248        """
249        new_session, new_level = self.getReturningData(student)
250        student['studycourse'].current_level = new_level
251        student['studycourse'].current_session = new_session
252        verdict = student['studycourse'].current_verdict
253        student['studycourse'].current_verdict = 'NY'
254        student['studycourse'].previous_verdict = verdict
255        return
256
257    def setPaymentDetails(self, category, student):
258        """Create Payment object and set the payment data of a student for
259        the payment category specified.
260
261        """
262        details = {}
263        p_item = u''
264        amount = 0.0
265        error = u''
266        p_session = student['studycourse'].current_session
267        p_level = student['studycourse'].current_level
268        session = str(p_session)
269        try:
270            academic_session = grok.getSite()['configuration'][session]
271        except KeyError:
272            return _(u'Session configuration object is not available.'), None
273        if category == 'schoolfee':
274            try:
275                certificate = student['studycourse'].certificate
276                p_item = certificate.code
277            except (AttributeError, TypeError):
278                return _('Study course data are incomplete.'), None
279            if student.state == CLEARED:
280                amount = getattr(certificate, 'school_fee_1', 0.0)
281            elif student.state == RETURNING:
282                # In case of returning school fee payment the payment session
283                # and level contain the values of the session the student
284                # has paid for.
285                p_session, p_level = self.getReturningData(student)
286                amount = getattr(certificate, 'school_fee_2', 0.0)
287            elif student.is_postgrad and student.state == PAID:
288                # Returning postgraduate students also pay for the next session
289                # but their level always remains the same.
290                p_session += 1
291                amount = getattr(certificate, 'school_fee_2', 0.0)
292        elif category == 'clearance':
293            p_item = student['studycourse'].certificate.code
294            amount = academic_session.clearance_fee
295        elif category == 'bed_allocation':
296            p_item = self.getAccommodationDetails(student)['bt']
297            amount = academic_session.booking_fee
298        if amount in (0.0, None):
299            return _(u'Amount could not be determined.'), None
300        for key in student['payments'].keys():
301            ticket = student['payments'][key]
302            if ticket.p_state == 'paid' and\
303               ticket.p_category == category and \
304               ticket.p_item == p_item and \
305               ticket.p_session == p_session:
306                  return _('This type of payment has already been made.'), None
307        payment = createObject(self.paymentfactory)
308        timestamp = "%d" % int(time()*1000)
309        payment.p_id = "p%s" % timestamp
310        payment.p_category = category
311        payment.p_item = p_item
312        payment.p_session = p_session
313        payment.p_level = p_level
314        payment.amount_auth = amount
315        return None, payment
316
317    def getAccommodationDetails(self, student):
318        """Determine the accommodation dates of a student.
319        """
320        d = {}
321        d['error'] = u''
322        hostels = grok.getSite()['hostels']
323        d['booking_session'] = hostels.accommodation_session
324        d['allowed_states'] = hostels.accommodation_states
325        d['startdate'] = hostels.startdate
326        d['enddate'] = hostels.enddate
327        d['expired'] = hostels.expired
328        # Determine bed type
329        studycourse = student['studycourse']
330        certificate = getattr(studycourse,'certificate',None)
331        entry_session = studycourse.entry_session
332        current_level = studycourse.current_level
333        if not (entry_session and current_level and certificate):
334            return
335        end_level = certificate.end_level
336        if entry_session == grok.getSite()['hostels'].accommodation_session:
337            bt = 'fr'
338        elif current_level >= end_level:
339            bt = 'fi'
340        else:
341            bt = 're'
342        if student.sex == 'f':
343            sex = 'female'
344        else:
345            sex = 'male'
346        special_handling = 'regular'
347        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
348        return d
349
350    def selectBed(self, available_beds):
351        """Select a bed from a list of available beds.
352
353        In the base configuration we select the first bed found,
354        but can also randomize the selection if we like.
355        """
356        return available_beds[0]
357
358    def renderPDF(self, view, filename='slip.pdf', student=None,
359                  studentview=None, tableheader=None, tabledata=None,
360                  note=None):
361        """Render pdf slips for various pages.
362        """
363        # XXX: we have to fix the import problems here.
364        from waeup.kofa.browser.interfaces import IPDFCreator
365        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
366        style = getSampleStyleSheet()
367        creator = getUtility(IPDFCreator)
368        data = []
369        doc_title = view.label
370        author = '%s (%s)' % (view.request.principal.title,
371                              view.request.principal.id)
372        footer_text = view.label
373        if getattr(student, 'student_id', None) is not None:
374            footer_text = "%s - %s - " % (student.student_id, footer_text)
375
376        # Insert student data table
377        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
378        if student is not None:
379            bd_translation = trans(_('Base Data'), portal_language)
380            data.append(Paragraph(bd_translation, style["Heading3"]))
381            data.append(render_student_data(studentview))
382
383        # Insert widgets
384        data.append(Paragraph(view.title, style["Heading3"]))
385        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
386        separators = getattr(self, 'SEPARATORS_DICT', {})
387        table = creator.getWidgetsTable(
388            view.form_fields, view.context, None, lang=portal_language,
389            separators=separators)
390        data.append(table)
391
392        # Insert scanned docs
393        data.extend(docs_as_flowables(view, portal_language))
394
395        # Insert content table (optionally on second page)
396        if tabledata and tableheader:
397            #data.append(PageBreak())
398            data.append(Spacer(1, 20))
399            data.append(Paragraph(view.content_title, style["Heading3"]))
400            contenttable = render_table_data(tableheader,tabledata)
401            data.append(contenttable)
402
403        view.response.setHeader(
404            'Content-Type', 'application/pdf')
405        try:
406            pdf_stream = creator.create_pdf(
407                data, None, doc_title, author=author, footer=footer_text,
408                note=note)
409        except IOError:
410            view.flash('Error in image file.')
411            return view.redirect(view.url(view.context))
412        return pdf_stream
413
414    VERDICTS_DICT = {
415        'NY': _('(not yet)'),
416        'A': 'Successful student',
417        'B': 'Student with carryover courses',
418        'C': 'Student on probation',
419        }
420
421    SEPARATORS_DICT = {
422        }
423
424    #: A prefix used when generating new student ids. Each student id will
425    #: start with this string. The default is 'K' for ``Kofa``.
426    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.