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

Last change on this file since 8412 was 8410, checked in by uli, 13 years ago

Create unique stud ids in the students container.

  • Property svn:keywords set to Id
File size: 15.4 KB
Line 
1## $Id: utils.py 8410 2012-05-10 16:37:07Z 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##
18"""General helper functions and utilities for the student section.
19"""
20import grok
21from random import SystemRandom as r
22from datetime import datetime
23from zope.i18n import translate
24from zope.component import getUtility
25from reportlab.pdfgen import canvas
26from reportlab.lib import colors
27from reportlab.lib.units import cm
28from reportlab.lib.enums import TA_RIGHT
29from reportlab.lib.pagesizes import A4
30from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
31from reportlab.platypus import (Frame, Paragraph, Image, PageBreak, Table,
32                                Spacer)
33from reportlab.platypus.tables import TableStyle
34from reportlab.platypus.flowables import PageBreak
35from zope.component import getUtility
36from zope.formlib.form import setUpEditWidgets
37
38from waeup.kofa.interfaces import IExtFileStore, IKofaUtils, RETURNING
39from waeup.kofa.interfaces import MessageFactory as _
40from waeup.kofa.students.interfaces import IStudentsUtils
41from waeup.kofa.utils.helpers import now
42
43SLIP_STYLE = [
44    ('VALIGN',(0,0),(-1,-1),'TOP'),
45    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
46    ]
47
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    ]
54
55FONT_SIZE = 10
56FONT_COLOR = 'black'
57
58def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
59    tag1 ='<font color=%s size=%d>' % (color, size)
60    return tag1 + '%s:</font>'
61
62def trans(text, lang):
63    # shortcut
64    return translate(text, 'waeup.kofa', target_language=lang)
65
66def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
67    """Turn `text`, `color` and `size` into an HTML snippet.
68
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
75    Finally, a br tag is added if widgets contain div tags
76    which are not supported by reportlab.
77
78    The returned snippet is unicode type.
79    """
80    try:
81        # In unit tests IKofaUtils has not been registered
82        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
83    except:
84        portal_language = 'en'
85    if not isinstance(text, unicode):
86        if isinstance(text, basestring):
87            text = text.decode('utf-8')
88        else:
89            text = unicode(text)
90    # Mainly for boolean values we need our customized
91    # localisation of the zope domain
92    text = translate(text, 'zope', target_language=portal_language)
93    text = text.replace('</div>', '<br /></div>')
94    tag1 = u'<font color="%s" size="%d">' % (color, size)
95    return tag1 + u'%s</font>' % text
96
97def generate_student_id(students, letter='?'):
98    students = grok.getSite()['students']
99    new_id = students.unique_student_id
100    return new_id
101
102def set_up_widgets(view, ignore_request=False):
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
110def render_student_data(studentview):
111    """Render student table for an existing frame.
112    """
113    width, height = A4
114    set_up_widgets(studentview, ignore_request=True)
115    data_left = []
116    data_right = []
117    style = getSampleStyleSheet()
118    img = getUtility(IExtFileStore).getFileByContext(
119        studentview.context, attr='passport.jpg')
120    if img is None:
121        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
122        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
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)])
126    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
127    for widget in studentview.widgets:
128        if widget.name == 'form.adm_code':
129            continue
130        f_label = formatted_label(size=12) % translate(
131            widget.label.strip(), 'waeup.kofa',
132            target_language=portal_language)
133        f_label = Paragraph(f_label, style["Normal"])
134        f_text = formatted_text(widget(), size=12)
135        f_text = Paragraph(f_text, style["Normal"])
136        data_right.append([f_label,f_text])
137    table_left = Table(data_left,style=SLIP_STYLE)
138    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
139    table = Table([[table_left, table_right],],style=SLIP_STYLE)
140    return table
141
142def render_table_data(tableheader,tabledata):
143    """Render children table for an existing frame.
144    """
145    data = []
146    #data.append([Spacer(1, 12)])
147    line = []
148    style = getSampleStyleSheet()
149    for element in tableheader:
150        field = formatted_text(element[0], color='white')
151        field = Paragraph(field, style["Normal"])
152        line.append(field)
153    data.append(line)
154    for ticket in tabledata:
155        line = []
156        for element in tableheader:
157              field = formatted_text(getattr(ticket,element[1],u' '))
158              field = Paragraph(field, style["Normal"])
159              line.append(field)
160        data.append(line)
161    table = Table(data,colWidths=[
162        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
163    return table
164
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 = []
174
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)
187            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
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:
195                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
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
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)
207      tz = getUtility(IKofaUtils).tzinfo
208      timestamp = now(tz).strftime("%d/%m/%Y %H:%M:%S %Z")
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)
214      portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
215      right_text = translate(_('<font size=10>${a} Page ${b} of ${c}</font>',
216          mapping = {'a':text, 'b':pdf.getPageNumber(), 'c':number_of_pages}),
217          'waeup.kofa', target_language=portal_language)
218      story.append(Paragraph(right_text, style["Right"]))
219      frame_footer.addFromList(story,pdf)
220
221class StudentsUtils(grok.GlobalUtility):
222    """A collection of methods subject to customization.
223    """
224    grok.implements(IStudentsUtils)
225
226    def getReturningData(self, student):
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        """
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
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):
255        """Get the payment dates of a student for the payment category
256        specified.
257        """
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'])
265        try:
266            academic_session = grok.getSite()['configuration'][session]
267        except KeyError:
268            details['error'] = u'Session configuration object is not available.'
269            return details
270        if category == 'schoolfee':
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)
280        elif category == 'clearance':
281            details['p_item'] = student['studycourse'].certificate.code
282            details['amount'] = academic_session.clearance_fee
283        elif category == 'bed_allocation':
284            details['p_item'] = self.getAccommodationDetails(student)['bt']
285            details['amount'] = academic_session.booking_fee
286        return details
287
288    def getAccommodationDetails(self, student):
289        """Determine the accommodation dates of a student.
290        """
291        d = {}
292        d['error'] = u''
293        site_configuration = grok.getSite()['configuration']
294        d['booking_session'] = site_configuration.accommodation_session
295        d['allowed_states'] = site_configuration.accommodation_states
296        # Determine bed type
297        studycourse = student['studycourse']
298        certificate = getattr(studycourse,'certificate',None)
299        entry_session = studycourse.entry_session
300        current_level = studycourse.current_level
301        if not (entry_session and current_level and certificate):
302            return
303        end_level = certificate.end_level
304        if entry_session == grok.getSite()[
305            'configuration'].accommodation_session:
306            bt = 'fr'
307        elif current_level >= end_level:
308            bt = 'fi'
309        else:
310            bt = 're'
311        if student.sex == 'f':
312            sex = 'female'
313        else:
314            sex = 'male'
315        special_handling = 'regular'
316        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
317        return d
318
319    def selectBed(self, available_beds):
320        """Select a bed from a list of available beds.
321
322        In the base configuration we select the first bed found,
323        but can also randomize the selection if we like.
324        """
325        return available_beds[0]
326
327    def renderPDF(self, view, filename='slip.pdf', student=None,
328                  studentview=None, tableheader=None, tabledata=None,
329                  note=None):
330        """Render pdf slips for various pages.
331        """
332        # XXX: we have to fix the import problems here.
333        from waeup.kofa.browser.interfaces import IPDFCreator
334        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
335        style = getSampleStyleSheet()
336        creator = getUtility(IPDFCreator)
337        data = []
338        doc_title = view.label
339        author = '%s (%s)' % (view.request.principal.title,
340                              view.request.principal.id)
341        footer_text = view.label
342        if getattr(student, 'student_id', None) is not None:
343            footer_text = "%s - %s - " % (student.student_id, footer_text)
344
345        # Insert student data table
346        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
347        if student is not None:
348            bd_translation = trans(_('Base Data'), portal_language)
349            data.append(Paragraph(bd_translation, style["Heading3"]))
350            data.append(render_student_data(studentview))
351
352        # Insert widgets
353        data.append(Paragraph(view.title, style["Heading3"]))
354        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
355        separators = getattr(self, 'SEPARATORS_DICT', {})
356        table = creator.getWidgetsTable(
357            view.form_fields, view.context, None, lang=portal_language,
358            separators=separators)
359        data.append(table)
360
361        # Insert scanned docs
362        data.extend(docs_as_flowables(view, portal_language))
363
364        # Insert content table (optionally on second page)
365        if tabledata and tableheader:
366            #data.append(PageBreak())
367            data.append(Spacer(1, 20))
368            data.append(Paragraph(view.content_title, style["Heading3"]))
369            contenttable = render_table_data(tableheader,tabledata)
370            data.append(contenttable)
371
372        view.response.setHeader(
373            'Content-Type', 'application/pdf')
374        try:
375            pdf_stream = creator.create_pdf(
376                data, None, doc_title, author=author, footer=footer_text,
377                note=note)
378        except IOError:
379            view.flash('Error in image file.')
380            return view.redirect(view.url(view.context))
381        return pdf_stream
382
383    VERDICTS_DICT = {
384        '0': _('(not yet)'),
385        'A': 'Successful student',
386        'B': 'Student with carryover courses',
387        'C': 'Student on probation',
388        }
389
390    SEPARATORS_DICT = {
391        }
392
393    #: A prefix used when generating new student ids. Each student id will
394    #: start with this string. The default is 'K' for ``Kofa``.
395    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.