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

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

Change button title.

Let students view their application slip.

Fix pagetemplate.

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