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

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

Use bed coordinates as p_item of maintenance payments.

  • Property svn:keywords set to Id
File size: 22.4 KB
Line 
1## $Id: utils.py 9429 2012-10-26 19:57:19Z 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(
141            studentview.context['studycourse'].certificate.longtitle(), size=12)
142        f_text = Paragraph(f_text, style["Normal"])
143        data_right.append([f_label,f_text])
144
145        f_label = formatted_label(size=12) % _('Department')
146        f_label = Paragraph(f_label, style["Normal"])
147        f_text = formatted_text(
148            studentview.context[
149            'studycourse'].certificate.__parent__.__parent__.longtitle(),
150            size=12)
151        f_text = Paragraph(f_text, style["Normal"])
152        data_right.append([f_label,f_text])
153
154        f_label = formatted_label(size=12) % _('Faculty')
155        f_label = Paragraph(f_label, style["Normal"])
156        f_text = formatted_text(
157            studentview.context[
158            'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle(),
159            size=12)
160        f_text = Paragraph(f_text, style["Normal"])
161        data_right.append([f_label,f_text])
162
163    table_left = Table(data_left,style=SLIP_STYLE)
164    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
165    table = Table([[table_left, table_right],],style=SLIP_STYLE)
166    return table
167
168def render_table_data(tableheader,tabledata):
169    """Render children table for an existing frame.
170    """
171    data = []
172    #data.append([Spacer(1, 12)])
173    line = []
174    style = getSampleStyleSheet()
175    for element in tableheader:
176        field = formatted_text(element[0], color='white')
177        field = Paragraph(field, style["Normal"])
178        line.append(field)
179    data.append(line)
180    for ticket in tabledata:
181        line = []
182        for element in tableheader:
183              field = formatted_text(getattr(ticket,element[1],u' '))
184              field = Paragraph(field, style["Normal"])
185              line.append(field)
186        data.append(line)
187    table = Table(data,colWidths=[
188        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
189    return table
190
191def get_signature_table(signatures, lang='en'):
192    """Return a reportlab table containing signature fields (with date).
193    """
194    style = getSampleStyleSheet()
195    space_width = 0.4  # width in cm of space between signatures
196    table_width = 16.0 # supposed width of signature table in cms
197    # width of signature cells in cm...
198    sig_col_width = table_width - ((len(signatures) - 1) * space_width)
199    sig_col_width = sig_col_width / len(signatures)
200    data = []
201    col_widths = [] # widths of columns
202
203    sig_style = [
204        ('VALIGN',(0,-1),(-1,-1),'TOP'),
205        ('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
206        ('BOTTOMPADDING', (0,0), (-1,0), 36),
207        ('TOPPADDING', (0,-1), (-1,-1), 0),
208        ]
209    for num, elem in enumerate(signatures):
210        # draw a line above each signature cell (not: empty cells in between)
211        sig_style.append(
212            ('LINEABOVE', (num*2,-1), (num*2, -1), 1, colors.black))
213
214    row = []
215    for signature in signatures:
216        row.append(trans(_('Date:'), lang))
217        row.append('')
218        if len(signatures) > 1:
219            col_widths.extend([sig_col_width*cm, space_width*cm])
220        else:
221            col_widths.extend([sig_col_width/2*cm, sig_col_width/2*cm])
222            row.append('') # empty spaceholder on right
223    data.append(row[:-1])
224    data.extend(([''],)*3) # insert 3 empty rows...
225    row = []
226    for signature in signatures:
227        row.append(Paragraph(trans(signature, lang), style["Normal"]))
228        row.append('')
229    data.append(row[:-1])
230    table = Table(data, style=sig_style, repeatRows=len(data),
231                  colWidths=col_widths)
232    return table
233
234def docs_as_flowables(view, lang='en'):
235    """Create reportlab flowables out of scanned docs.
236    """
237    # XXX: fix circular import problem
238    from waeup.kofa.students.viewlets import FileManager
239    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
240    from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
241    style = getSampleStyleSheet()
242    data = []
243
244    # Collect viewlets
245    fm = FileManager(view.context, view.request, view)
246    fm.update()
247    if fm.viewlets:
248        sc_translation = trans(_('Scanned Documents'), lang)
249        data.append(Paragraph(sc_translation, style["Heading3"]))
250        # Insert list of scanned documents
251        table_data = []
252        for viewlet in fm.viewlets:
253            f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
254            img_path = getattr(getUtility(IExtFileStore).getFileByContext(
255                view.context, attr=viewlet.download_name), 'name', None)
256            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
257            if img_path is None:
258                pass
259            elif not img_path[-4:] in ('.jpg', '.JPG'):
260                # reportlab requires jpg images, I think.
261                f_text = Paragraph('%s (not displayable)' % (
262                    viewlet.title,), ENTRY1_STYLE)
263            else:
264                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
265            table_data.append([f_label, f_text])
266        if table_data:
267            # safety belt; empty tables lead to problems.
268            data.append(Table(table_data, style=SLIP_STYLE))
269    return data
270
271class StudentsUtils(grok.GlobalUtility):
272    """A collection of methods subject to customization.
273    """
274    grok.implements(IStudentsUtils)
275
276    def getReturningData(self, student):
277        """ Define what happens after school fee payment
278        depending on the student's senate verdict.
279
280        In the base configuration current level is always increased
281        by 100 no matter which verdict has been assigned.
282        """
283        new_level = student['studycourse'].current_level + 100
284        new_session = student['studycourse'].current_session + 1
285        return new_session, new_level
286
287    def setReturningData(self, student):
288        """ Define what happens after school fee payment
289        depending on the student's senate verdict.
290
291        This method folllows the same algorithm as getReturningData but
292        it also sets the new values.
293        """
294        new_session, new_level = self.getReturningData(student)
295        student['studycourse'].current_level = new_level
296        student['studycourse'].current_session = new_session
297        verdict = student['studycourse'].current_verdict
298        student['studycourse'].current_verdict = '0'
299        student['studycourse'].previous_verdict = verdict
300        return
301
302    def setPaymentDetails(self, category, student,
303            previous_session, previous_level):
304        """Create Payment object and set the payment data of a student for
305        the payment category specified.
306
307        """
308        p_item = u''
309        amount = 0.0
310        if previous_session:
311            p_session = previous_session
312            p_level = previous_level
313            p_current = False
314        else:
315            p_session = student['studycourse'].current_session
316            p_level = student['studycourse'].current_level
317            p_current = True
318        session = str(p_session)
319        try:
320            academic_session = grok.getSite()['configuration'][session]
321        except KeyError:
322            return _(u'Session configuration object is not available.'), None
323        if category == 'schoolfee':
324            try:
325                certificate = student['studycourse'].certificate
326                p_item = certificate.code
327            except (AttributeError, TypeError):
328                return _('Study course data are incomplete.'), None
329            if previous_session:
330                if previous_session < student['studycourse'].entry_session:
331                    return _('The previous session must not fall below '
332                             'your entry session.'), None
333                if previous_session > student['studycourse'].current_session - 1:
334                    return _('This is not a previous session.'), None
335                if previous_level == 100:
336                    amount = getattr(certificate, 'school_fee_1', 0.0)
337                else:
338                    amount = getattr(certificate, 'school_fee_2', 0.0)
339            else:
340                if student.state == CLEARED:
341                    amount = getattr(certificate, 'school_fee_1', 0.0)
342                elif student.state == RETURNING:
343                    # In case of returning school fee payment the payment session
344                    # and level contain the values of the session the student
345                    # has paid for.
346                    p_session, p_level = self.getReturningData(student)
347                    amount = getattr(certificate, 'school_fee_2', 0.0)
348                elif student.is_postgrad and student.state == PAID:
349                    # Returning postgraduate students also pay for the next session
350                    # but their level always remains the same.
351                    p_session += 1
352                    amount = getattr(certificate, 'school_fee_2', 0.0)
353        elif category == 'clearance':
354            try:
355                p_item = student['studycourse'].certificate.code
356            except (AttributeError, TypeError):
357                return _('Study course data are incomplete.'), None
358            amount = academic_session.clearance_fee
359        elif category == 'bed_allocation':
360            p_item = self.getAccommodationDetails(student)['bt']
361            amount = academic_session.booking_fee
362        elif category == 'hostel_maintenance':
363            amount = academic_session.maint_fee
364            bedticket = student['accommodation'].get(
365                str(student.current_session), None)
366            if bedticket:
367                p_item = bedticket.bed_coordinates
368            else:
369                # Should not happen because this is already checked
370                # in the browser module, but anyway ...
371                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
372                p_item = trans(_('no bed allocated'), portal_language)
373        if amount in (0.0, None):
374            return _('Amount could not be determined.' +
375                     ' Would you like to pay for a previous session?'), None
376        for key in student['payments'].keys():
377            ticket = student['payments'][key]
378            if ticket.p_state == 'paid' and\
379               ticket.p_category == category and \
380               ticket.p_item == p_item and \
381               ticket.p_session == p_session:
382                  return _('This type of payment has already been made.' +
383                           ' Would you like to pay for a previous session?'), None
384        payment = createObject(u'waeup.StudentOnlinePayment')
385        timestamp = ("%d" % int(time()*10000))[1:]
386        payment.p_id = "p%s" % timestamp
387        payment.p_category = category
388        payment.p_item = p_item
389        payment.p_session = p_session
390        payment.p_level = p_level
391        payment.p_current = p_current
392        payment.amount_auth = amount
393        return None, payment
394
395    def getAccommodationDetails(self, student):
396        """Determine the accommodation data of a student.
397        """
398        d = {}
399        d['error'] = u''
400        hostels = grok.getSite()['hostels']
401        d['booking_session'] = hostels.accommodation_session
402        d['allowed_states'] = hostels.accommodation_states
403        d['startdate'] = hostels.startdate
404        d['enddate'] = hostels.enddate
405        d['expired'] = hostels.expired
406        # Determine bed type
407        studycourse = student['studycourse']
408        certificate = getattr(studycourse,'certificate',None)
409        entry_session = studycourse.entry_session
410        current_level = studycourse.current_level
411        if None in (entry_session, current_level, certificate):
412            return d
413        end_level = certificate.end_level
414        if current_level == 10:
415            bt = 'pr'
416        elif entry_session == grok.getSite()['hostels'].accommodation_session:
417            bt = 'fr'
418        elif current_level >= end_level:
419            bt = 'fi'
420        else:
421            bt = 're'
422        if student.sex == 'f':
423            sex = 'female'
424        else:
425            sex = 'male'
426        special_handling = 'regular'
427        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
428        return d
429
430    def selectBed(self, available_beds):
431        """Select a bed from a list of available beds.
432
433        In the base configuration we select the first bed found,
434        but can also randomize the selection if we like.
435        """
436        return available_beds[0]
437
438    def renderPDFAdmissionLetter(self, view, student=None):
439        """Render pdf admission letter.
440        """
441        # XXX: we have to fix the import problems here.
442        from waeup.kofa.browser.interfaces import IPDFCreator
443        from waeup.kofa.browser.pdf import format_html, NOTE_STYLE
444        if student is None:
445            return
446        style = getSampleStyleSheet()
447        creator = getUtility(IPDFCreator)
448        data = []
449        doc_title = view.label
450        author = '%s (%s)' % (view.request.principal.title,
451                              view.request.principal.id)
452        footer_text = view.label
453        if getattr(student, 'student_id', None) is not None:
454            footer_text = "%s - %s - " % (student.student_id, footer_text)
455
456        # Admission text
457        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
458        inst_name = grok.getSite()['configuration'].name
459        text = trans(_(
460            'This is to inform you that you have been provisionally'
461            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
462            portal_language)
463        html = format_html(text)
464        data.append(Paragraph(html, NOTE_STYLE))
465        data.append(Spacer(1, 20))
466
467        # Student data
468        data.append(render_student_data(view))
469
470        # Insert history
471        data.append(Spacer(1, 20))
472        datelist = student.history.messages[0].split()[0].split('-')
473        creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
474        text = trans(_(
475            'Your Kofa student record was created on ${a}.',
476            mapping = {'a': creation_date}),
477            portal_language)
478        html = format_html(text)
479        data.append(Paragraph(html, NOTE_STYLE))
480
481        # Create pdf stream
482        view.response.setHeader(
483            'Content-Type', 'application/pdf')
484        pdf_stream = creator.create_pdf(
485            data, None, doc_title, author=author, footer=footer_text,
486            note=None)
487        return pdf_stream
488
489    def renderPDF(self, view, filename='slip.pdf', student=None,
490                  studentview=None, tableheader=None, tabledata=None,
491                  note=None, signatures=None):
492        """Render pdf slips for various pages.
493        """
494        # XXX: we have to fix the import problems here.
495        from waeup.kofa.browser.interfaces import IPDFCreator
496        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
497        style = getSampleStyleSheet()
498        creator = getUtility(IPDFCreator)
499        data = []
500        doc_title = view.label
501        author = '%s (%s)' % (view.request.principal.title,
502                              view.request.principal.id)
503        footer_text = view.label
504        if getattr(student, 'student_id', None) is not None:
505            footer_text = "%s - %s - " % (student.student_id, footer_text)
506
507        # Insert history
508        if not filename.startswith('payment'):
509            data.extend(creator.fromStringList(student.history.messages))
510
511        # Insert student data table
512        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
513        if student is not None:
514            bd_translation = trans(_('Base Data'), portal_language)
515            data.append(Paragraph(bd_translation, style["Heading3"]))
516            data.append(render_student_data(studentview))
517
518        # Insert widgets
519        if view.form_fields:
520            data.append(Paragraph(view.title, style["Heading3"]))
521            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
522            separators = getattr(self, 'SEPARATORS_DICT', {})
523            table = creator.getWidgetsTable(
524                view.form_fields, view.context, None, lang=portal_language,
525                separators=separators)
526            data.append(table)
527
528        # Insert scanned docs
529        data.extend(docs_as_flowables(view, portal_language))
530
531        # Insert content table (optionally on second page)
532        if tabledata and tableheader:
533            #data.append(PageBreak())
534            data.append(Spacer(1, 20))
535            data.append(Paragraph(view.content_title, style["Heading3"]))
536            contenttable = render_table_data(tableheader,tabledata)
537            data.append(contenttable)
538
539        # Insert signatures
540        if signatures:
541            data.append(Spacer(1, 20))
542            signaturetable = get_signature_table(signatures)
543            data.append(signaturetable)
544
545        view.response.setHeader(
546            'Content-Type', 'application/pdf')
547        try:
548            pdf_stream = creator.create_pdf(
549                data, None, doc_title, author=author, footer=footer_text,
550                note=note)
551        except IOError:
552            view.flash('Error in image file.')
553            return view.redirect(view.url(view.context))
554        return pdf_stream
555
556    VERDICTS_DICT = {
557        '0': _('(not yet)'),
558        'A': 'Successful student',
559        'B': 'Student with carryover courses',
560        'C': 'Student on probation',
561        }
562
563    SEPARATORS_DICT = {
564        }
565
566    #: A prefix used when generating new student ids. Each student id will
567    #: start with this string. The default is 'K' for ``Kofa``.
568    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.