source: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/utils.py @ 10009

Last change on this file since 10009 was 9208, checked in by uli, 12 years ago

Merge changes from trunk r9171:9207.

  • Property svn:keywords set to Id
File size: 21.8 KB
RevLine 
[7191]1## $Id: utils.py 9208 2012-09-20 08:22:52Z 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##
[7358]18"""General helper functions and utilities for the student section.
[6651]19"""
[7150]20import grok
[8595]21from time import time
[7318]22from reportlab.lib import colors
[7019]23from reportlab.lib.units import cm
24from reportlab.lib.pagesizes import A4
[9169]25from reportlab.lib.styles import getSampleStyleSheet
26from reportlab.platypus import Paragraph, Image, Table, Spacer
27from zope.component import getUtility, createObject
[7019]28from zope.formlib.form import setUpEditWidgets
[9169]29from zope.i18n import translate
[8596]30from waeup.kofa.interfaces import (
31    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED)
[7811]32from waeup.kofa.interfaces import MessageFactory as _
33from waeup.kofa.students.interfaces import IStudentsUtils
[6651]34
[7318]35SLIP_STYLE = [
36    ('VALIGN',(0,0),(-1,-1),'TOP'),
37    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
38    ]
[7019]39
[7318]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    ]
[7304]46
[7318]47FONT_SIZE = 10
48FONT_COLOR = 'black'
49
[7319]50def formatted_label(color=FONT_COLOR, size=FONT_SIZE):
[7318]51    tag1 ='<font color=%s size=%d>' % (color, size)
[7319]52    return tag1 + '%s:</font>'
53
[8112]54def trans(text, lang):
55    # shortcut
56    return translate(text, 'waeup.kofa', target_language=lang)
57
[7511]58def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE):
59    """Turn `text`, `color` and `size` into an HTML snippet.
[7318]60
[7511]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
[7804]67    Finally, a br tag is added if widgets contain div tags
68    which are not supported by reportlab.
69
[7511]70    The returned snippet is unicode type.
71    """
[8142]72    try:
73        # In unit tests IKofaUtils has not been registered
74        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
75    except:
76        portal_language = 'en'
[7511]77    if not isinstance(text, unicode):
78        if isinstance(text, basestring):
79            text = text.decode('utf-8')
80        else:
81            text = unicode(text)
[8141]82    # Mainly for boolean values we need our customized
83    # localisation of the zope domain
84    text = translate(text, 'zope', target_language=portal_language)
[7804]85    text = text.replace('</div>', '<br /></div>')
[7511]86    tag1 = u'<font color="%s" size="%d">' % (color, size)
87    return tag1 + u'%s</font>' % text
88
[8481]89def generate_student_id():
[8410]90    students = grok.getSite()['students']
91    new_id = students.unique_student_id
92    return new_id
[6742]93
[7186]94def set_up_widgets(view, ignore_request=False):
[7019]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
[7310]102def render_student_data(studentview):
[7318]103    """Render student table for an existing frame.
104    """
105    width, height = A4
[7186]106    set_up_widgets(studentview, ignore_request=True)
[7318]107    data_left = []
108    data_right = []
[7019]109    style = getSampleStyleSheet()
[7280]110    img = getUtility(IExtFileStore).getFileByContext(
111        studentview.context, attr='passport.jpg')
112    if img is None:
[7811]113        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
[7280]114        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
[7318]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)])
[7819]118    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9169]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
[7019]126    for widget in studentview.widgets:
[9169]127        if 'name' in widget.name:
[7019]128            continue
[7714]129        f_label = formatted_label(size=12) % translate(
[7811]130            widget.label.strip(), 'waeup.kofa',
[7714]131            target_language=portal_language)
[7019]132        f_label = Paragraph(f_label, style["Normal"])
[7714]133        f_text = formatted_text(widget(), size=12)
[7019]134        f_text = Paragraph(f_text, style["Normal"])
[7318]135        data_right.append([f_label,f_text])
[9169]136
137    if hasattr(studentview.context, 'certcode'):
138        f_label = formatted_label(size=12) % _('Study Course')
139        f_label = Paragraph(f_label, style["Normal"])
[9208]140        f_text = formatted_text(
141            studentview.context['studycourse'].certificate.longtitle(), size=12)
[9169]142        f_text = Paragraph(f_text, style["Normal"])
143        data_right.append([f_label,f_text])
144
[9208]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
[7318]163    table_left = Table(data_left,style=SLIP_STYLE)
[8112]164    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
[7318]165    table = Table([[table_left, table_right],],style=SLIP_STYLE)
[7019]166    return table
167
[7304]168def render_table_data(tableheader,tabledata):
[7318]169    """Render children table for an existing frame.
170    """
[7304]171    data = []
[7318]172    #data.append([Spacer(1, 12)])
[7304]173    line = []
174    style = getSampleStyleSheet()
175    for element in tableheader:
[7511]176        field = formatted_text(element[0], color='white')
[7310]177        field = Paragraph(field, style["Normal"])
[7304]178        line.append(field)
179    data.append(line)
180    for ticket in tabledata:
181        line = []
182        for element in tableheader:
[7511]183              field = formatted_text(getattr(ticket,element[1],u' '))
[7318]184              field = Paragraph(field, style["Normal"])
[7304]185              line.append(field)
186        data.append(line)
[7310]187    table = Table(data,colWidths=[
[7318]188        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
[7304]189    return table
190
[9169]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
[8112]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 = []
[7318]243
[8112]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)
[8120]256            f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
[8112]257            if img_path is None:
258                pass
[9169]259            elif not img_path[-4:] in ('.jpg', '.JPG'):
[8112]260                # reportlab requires jpg images, I think.
[9169]261                f_text = Paragraph('%s (not displayable)' % (
[8112]262                    viewlet.title,), ENTRY1_STYLE)
263            else:
[8117]264                f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
[8112]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
[7150]271class StudentsUtils(grok.GlobalUtility):
272    """A collection of methods subject to customization.
273    """
274    grok.implements(IStudentsUtils)
[7019]275
[8268]276    def getReturningData(self, student):
[9169]277        """ Define what happens after school fee payment
[7841]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        """
[8268]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):
[9169]288        """ Define what happens after school fee payment
289        depending on the student's senate verdict.
[8268]290
[9169]291        This method folllows the same algorithm as getReturningData but
292        it also sets the new values.
[8268]293        """
294        new_session, new_level = self.getReturningData(student)
295        student['studycourse'].current_level = new_level
296        student['studycourse'].current_session = new_session
[7615]297        verdict = student['studycourse'].current_verdict
[9169]298        student['studycourse'].current_verdict = '0'
[7615]299        student['studycourse'].previous_verdict = verdict
300        return
301
[9169]302    def setPaymentDetails(self, category, student,
303            previous_session, previous_level):
[8595]304        """Create Payment object and set the payment data of a student for
305        the payment category specified.
306
[7841]307        """
[8595]308        p_item = u''
309        amount = 0.0
[9169]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
[8595]318        session = str(p_session)
[7150]319        try:
320            academic_session = grok.getSite()['configuration'][session]
321        except KeyError:
[8595]322            return _(u'Session configuration object is not available.'), None
[7150]323        if category == 'schoolfee':
[8595]324            try:
[8596]325                certificate = student['studycourse'].certificate
326                p_item = certificate.code
[8595]327            except (AttributeError, TypeError):
328                return _('Study course data are incomplete.'), None
[9169]329            if previous_session:
[9208]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
[9169]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)
[7150]353        elif category == 'clearance':
[9208]354            try:
355                p_item = student['studycourse'].certificate.code
356            except (AttributeError, TypeError):
357                return _('Study course data are incomplete.'), None
[8595]358            amount = academic_session.clearance_fee
[7150]359        elif category == 'bed_allocation':
[8595]360            p_item = self.getAccommodationDetails(student)['bt']
361            amount = academic_session.booking_fee
362        if amount in (0.0, None):
[9169]363            return _('Amount could not be determined.' +
364                     ' Would you like to pay for a previous session?'), None
[8595]365        for key in student['payments'].keys():
366            ticket = student['payments'][key]
367            if ticket.p_state == 'paid' and\
368               ticket.p_category == category and \
369               ticket.p_item == p_item and \
370               ticket.p_session == p_session:
[9169]371                  return _('This type of payment has already been made.' +
372                           ' Would you like to pay for a previous session?'), None
[8708]373        payment = createObject(u'waeup.StudentOnlinePayment')
[9169]374        timestamp = ("%d" % int(time()*10000))[1:]
[8595]375        payment.p_id = "p%s" % timestamp
376        payment.p_category = category
377        payment.p_item = p_item
378        payment.p_session = p_session
379        payment.p_level = p_level
[9169]380        payment.p_current = p_current
[8595]381        payment.amount_auth = amount
382        return None, payment
[7019]383
[7186]384    def getAccommodationDetails(self, student):
[7841]385        """Determine the accommodation dates of a student.
386        """
[7150]387        d = {}
388        d['error'] = u''
[8685]389        hostels = grok.getSite()['hostels']
390        d['booking_session'] = hostels.accommodation_session
391        d['allowed_states'] = hostels.accommodation_states
[8688]392        d['startdate'] = hostels.startdate
393        d['enddate'] = hostels.enddate
394        d['expired'] = hostels.expired
[7150]395        # Determine bed type
396        studycourse = student['studycourse']
[7369]397        certificate = getattr(studycourse,'certificate',None)
[7150]398        entry_session = studycourse.entry_session
399        current_level = studycourse.current_level
[9208]400        if None in (entry_session, current_level, certificate):
401            return d
[7369]402        end_level = certificate.end_level
[9169]403        if current_level == 10:
404            bt = 'pr'
405        elif entry_session == grok.getSite()['hostels'].accommodation_session:
[7150]406            bt = 'fr'
407        elif current_level >= end_level:
408            bt = 'fi'
409        else:
410            bt = 're'
411        if student.sex == 'f':
412            sex = 'female'
413        else:
414            sex = 'male'
415        special_handling = 'regular'
416        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
417        return d
[7019]418
[7186]419    def selectBed(self, available_beds):
[7841]420        """Select a bed from a list of available beds.
421
422        In the base configuration we select the first bed found,
423        but can also randomize the selection if we like.
424        """
[7150]425        return available_beds[0]
426
[9208]427    def renderPDFAdmissionLetter(self, view, student=None):
428        """Render pdf admission letter.
429        """
430        # XXX: we have to fix the import problems here.
431        from waeup.kofa.browser.interfaces import IPDFCreator
432        from waeup.kofa.browser.pdf import format_html, NOTE_STYLE
433        if student is None:
434            return
435        style = getSampleStyleSheet()
436        creator = getUtility(IPDFCreator)
437        data = []
438        doc_title = view.label
439        author = '%s (%s)' % (view.request.principal.title,
440                              view.request.principal.id)
441        footer_text = view.label
442        if getattr(student, 'student_id', None) is not None:
443            footer_text = "%s - %s - " % (student.student_id, footer_text)
444
445        # Admission text
446        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
447        inst_name = grok.getSite()['configuration'].name
448        text = trans(_(
449            'This is to inform you that you have been provisionally'
450            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
451            portal_language)
452        html = format_html(text)
453        data.append(Paragraph(html, NOTE_STYLE))
454        data.append(Spacer(1, 20))
455
456        # Student data
457        data.append(render_student_data(view))
458
459        # Insert history
460        data.append(Spacer(1, 20))
461        datelist = student.history.messages[0].split()[0].split('-')
462        creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
463        text = trans(_(
464            'Your Kofa student record was created on ${a}.',
465            mapping = {'a': creation_date}),
466            portal_language)
467        html = format_html(text)
468        data.append(Paragraph(html, NOTE_STYLE))
469
470        # Create pdf stream
471        view.response.setHeader(
472            'Content-Type', 'application/pdf')
473        pdf_stream = creator.create_pdf(
474            data, None, doc_title, author=author, footer=footer_text,
475            note=None)
476        return pdf_stream
477
[8257]478    def renderPDF(self, view, filename='slip.pdf', student=None,
479                  studentview=None, tableheader=None, tabledata=None,
[9169]480                  note=None, signatures=None):
[7841]481        """Render pdf slips for various pages.
482        """
[8112]483        # XXX: we have to fix the import problems here.
484        from waeup.kofa.browser.interfaces import IPDFCreator
485        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
486        style = getSampleStyleSheet()
487        creator = getUtility(IPDFCreator)
488        data = []
489        doc_title = view.label
490        author = '%s (%s)' % (view.request.principal.title,
491                              view.request.principal.id)
[7310]492        footer_text = view.label
[7714]493        if getattr(student, 'student_id', None) is not None:
[7310]494            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]495
[9169]496        # Insert history
497        if not filename.startswith('payment'):
498            data.extend(creator.fromStringList(student.history.messages))
499
[7318]500        # Insert student data table
[7819]501        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7310]502        if student is not None:
[8112]503            bd_translation = trans(_('Base Data'), portal_language)
504            data.append(Paragraph(bd_translation, style["Heading3"]))
505            data.append(render_student_data(studentview))
[7304]506
[7318]507        # Insert widgets
[9208]508        if view.form_fields:
509            data.append(Paragraph(view.title, style["Heading3"]))
510            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
511            separators = getattr(self, 'SEPARATORS_DICT', {})
512            table = creator.getWidgetsTable(
513                view.form_fields, view.context, None, lang=portal_language,
514                separators=separators)
515            data.append(table)
[7318]516
[8112]517        # Insert scanned docs
518        data.extend(docs_as_flowables(view, portal_language))
[7318]519
[8141]520        # Insert content table (optionally on second page)
[7318]521        if tabledata and tableheader:
[8141]522            #data.append(PageBreak())
523            data.append(Spacer(1, 20))
[8112]524            data.append(Paragraph(view.content_title, style["Heading3"]))
[7304]525            contenttable = render_table_data(tableheader,tabledata)
[8112]526            data.append(contenttable)
[7318]527
[9169]528        # Insert signatures
529        if signatures:
530            data.append(Spacer(1, 20))
531            signaturetable = get_signature_table(signatures)
532            data.append(signaturetable)
533
[7150]534        view.response.setHeader(
535            'Content-Type', 'application/pdf')
[8112]536        try:
537            pdf_stream = creator.create_pdf(
[8257]538                data, None, doc_title, author=author, footer=footer_text,
539                note=note)
[8112]540        except IOError:
541            view.flash('Error in image file.')
542            return view.redirect(view.url(view.context))
543        return pdf_stream
[7620]544
[7841]545    VERDICTS_DICT = {
[9169]546        '0': _('(not yet)'),
[7841]547        'A': 'Successful student',
548        'B': 'Student with carryover courses',
549        'C': 'Student on probation',
550        }
[8099]551
552    SEPARATORS_DICT = {
553        }
[8410]554
555    #: A prefix used when generating new student ids. Each student id will
556    #: start with this string. The default is 'K' for ``Kofa``.
557    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.