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

Last change on this file since 9716 was 9555, checked in by uli, 12 years ago

Henriks wishlist.

  • Property svn:keywords set to Id
File size: 23.9 KB
Line 
1## $Id: utils.py 9555 2012-11-06 10:54:46Z 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 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 getattr(studentview.context, 'certcode', None):
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 _getSessionConfiguration(self, session):
303        try:
304            return grok.getSite()['configuration'][str(session)]
305        except KeyError:
306            return None
307
308    def setPaymentDetails(self, category, student,
309            previous_session, previous_level):
310        """Create Payment object and set the payment data of a student for
311        the payment category specified.
312
313        """
314        p_item = u''
315        amount = 0.0
316        if previous_session:
317            if previous_session < student['studycourse'].entry_session:
318                return _('The previous session must not fall below '
319                         'your entry session.'), None
320            if category == 'schoolfee':
321                # School fee is always paid for the following session
322                if previous_session > student['studycourse'].current_session:
323                    return _('This is not a previous session.'), None
324            else:
325                if previous_session > student['studycourse'].current_session - 1:
326                    return _('This is not a previous session.'), None
327            p_session = previous_session
328            p_level = previous_level
329            p_current = False
330        else:
331            p_session = student['studycourse'].current_session
332            p_level = student['studycourse'].current_level
333            p_current = True
334        academic_session = self._getSessionConfiguration(p_session)
335        if academic_session == None:
336            return _(u'Session configuration object is not available.'), None
337        # Determine fee.
338        if category == 'schoolfee':
339            try:
340                certificate = student['studycourse'].certificate
341                p_item = certificate.code
342            except (AttributeError, TypeError):
343                return _('Study course data are incomplete.'), None
344            if previous_session:
345                # Students can pay for previous sessions in all workflow states.
346                # Fresh students are excluded by the update method of the
347                # PreviousPaymentAddFormPage.
348                if previous_level == 100:
349                    amount = getattr(certificate, 'school_fee_1', 0.0)
350                else:
351                    amount = getattr(certificate, 'school_fee_2', 0.0)
352            else:
353                if student.state == CLEARED:
354                    amount = getattr(certificate, 'school_fee_1', 0.0)
355                elif student.state == RETURNING:
356                    # In case of returning school fee payment the payment session
357                    # and level contain the values of the session the student
358                    # has paid for. Payment session is always next session.
359                    p_session, p_level = self.getReturningData(student)
360                    academic_session = self._getSessionConfiguration(p_session)
361                    if academic_session == None:
362                        return _(u'Session configuration object is not available.'), None
363                    amount = getattr(certificate, 'school_fee_2', 0.0)
364                elif student.is_postgrad and student.state == PAID:
365                    # Returning postgraduate students also pay for the next session
366                    # but their level always remains the same.
367                    p_session += 1
368                    academic_session = self._getSessionConfiguration(p_session)
369                    if academic_session == None:
370                        return _(u'Session configuration object is not available.'), None
371                    amount = getattr(certificate, 'school_fee_2', 0.0)
372        elif category == 'clearance':
373            try:
374                p_item = student['studycourse'].certificate.code
375            except (AttributeError, TypeError):
376                return _('Study course data are incomplete.'), None
377            amount = academic_session.clearance_fee
378        elif category == 'bed_allocation':
379            p_item = self.getAccommodationDetails(student)['bt']
380            amount = academic_session.booking_fee
381        elif category == 'hostel_maintenance':
382            amount = academic_session.maint_fee
383            bedticket = student['accommodation'].get(
384                str(student.current_session), None)
385            if bedticket:
386                p_item = bedticket.bed_coordinates
387            else:
388                # Should not happen because this is already checked
389                # in the browser module, but anyway ...
390                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
391                p_item = trans(_('no bed allocated'), portal_language)
392        if amount in (0.0, None):
393            return _('Amount could not be determined.'), None
394        for key in student['payments'].keys():
395            ticket = student['payments'][key]
396            if ticket.p_state == 'paid' and\
397               ticket.p_category == category and \
398               ticket.p_item == p_item and \
399               ticket.p_session == p_session:
400                  return _('This type of payment has already been made.'), None
401        payment = createObject(u'waeup.StudentOnlinePayment')
402        timestamp = ("%d" % int(time()*10000))[1:]
403        payment.p_id = "p%s" % timestamp
404        payment.p_category = category
405        payment.p_item = p_item
406        payment.p_session = p_session
407        payment.p_level = p_level
408        payment.p_current = p_current
409        payment.amount_auth = amount
410        return None, payment
411
412    def getAccommodationDetails(self, student):
413        """Determine the accommodation data of a student.
414        """
415        d = {}
416        d['error'] = u''
417        hostels = grok.getSite()['hostels']
418        d['booking_session'] = hostels.accommodation_session
419        d['allowed_states'] = hostels.accommodation_states
420        d['startdate'] = hostels.startdate
421        d['enddate'] = hostels.enddate
422        d['expired'] = hostels.expired
423        # Determine bed type
424        studycourse = student['studycourse']
425        certificate = getattr(studycourse,'certificate',None)
426        entry_session = studycourse.entry_session
427        current_level = studycourse.current_level
428        if None in (entry_session, current_level, certificate):
429            return d
430        end_level = certificate.end_level
431        if current_level == 10:
432            bt = 'pr'
433        elif entry_session == grok.getSite()['hostels'].accommodation_session:
434            bt = 'fr'
435        elif current_level >= end_level:
436            bt = 'fi'
437        else:
438            bt = 're'
439        if student.sex == 'f':
440            sex = 'female'
441        else:
442            sex = 'male'
443        special_handling = 'regular'
444        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
445        return d
446
447    def selectBed(self, available_beds):
448        """Select a bed from a list of available beds.
449
450        In the base configuration we select the first bed found,
451        but can also randomize the selection if we like.
452        """
453        return available_beds[0]
454
455    def renderPDFAdmissionLetter(self, view, student=None):
456        """Render pdf admission letter.
457        """
458        # XXX: we have to fix the import problems here.
459        from waeup.kofa.browser.interfaces import IPDFCreator
460        from waeup.kofa.browser.pdf import format_html, NOTE_STYLE
461        if student is None:
462            return
463        style = getSampleStyleSheet()
464        creator = getUtility(IPDFCreator)
465        data = []
466        doc_title = view.label
467        author = '%s (%s)' % (view.request.principal.title,
468                              view.request.principal.id)
469        footer_text = view.label
470        if getattr(student, 'student_id', None) is not None:
471            footer_text = "%s - %s - " % (student.student_id, footer_text)
472
473        # Admission text
474        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
475        inst_name = grok.getSite()['configuration'].name
476        text = trans(_(
477            'This is to inform you that you have been provisionally'
478            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
479            portal_language)
480        html = format_html(text)
481        data.append(Paragraph(html, NOTE_STYLE))
482        data.append(Spacer(1, 20))
483
484        # Student data
485        data.append(render_student_data(view))
486
487        # Insert history
488        data.append(Spacer(1, 20))
489        datelist = student.history.messages[0].split()[0].split('-')
490        creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
491        text = trans(_(
492            'Your Kofa student record was created on ${a}.',
493            mapping = {'a': creation_date}),
494            portal_language)
495        html = format_html(text)
496        data.append(Paragraph(html, NOTE_STYLE))
497
498        # Create pdf stream
499        view.response.setHeader(
500            'Content-Type', 'application/pdf')
501        pdf_stream = creator.create_pdf(
502            data, None, doc_title, author=author, footer=footer_text,
503            note=None)
504        return pdf_stream
505
506    def renderPDF(self, view, filename='slip.pdf', student=None,
507                  studentview=None, tableheader=None, tabledata=None,
508                  note=None, signatures=None, sigs_in_footer=(),
509                  show_scans=True):
510        """Render pdf slips for various pages.
511        """
512        # XXX: we have to fix the import problems here.
513        from waeup.kofa.browser.interfaces import IPDFCreator
514        from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE
515        style = getSampleStyleSheet()
516        creator = getUtility(IPDFCreator)
517        data = []
518        doc_title = view.label
519        author = '%s (%s)' % (view.request.principal.title,
520                              view.request.principal.id)
521        footer_text = view.label
522        if getattr(student, 'student_id', None) is not None:
523            footer_text = "%s - %s - " % (student.student_id, footer_text)
524
525        # Insert student data table
526        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
527        if student is not None:
528            bd_translation = trans(_('Base Data'), portal_language)
529            data.append(Paragraph(bd_translation, style["Heading3"]))
530            data.append(render_student_data(studentview))
531
532        # Insert widgets
533        if view.form_fields:
534            data.append(Paragraph(view.title, style["Heading3"]))
535            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
536            separators = getattr(self, 'SEPARATORS_DICT', {})
537            table = creator.getWidgetsTable(
538                view.form_fields, view.context, None, lang=portal_language,
539                separators=separators)
540            data.append(table)
541
542        # Insert scanned docs
543        if show_scans:
544            data.extend(docs_as_flowables(view, portal_language))
545
546        # Insert history
547        if filename.startswith('clearance') or filename.startswith('course'):
548            hist_translation = trans(_('Workflow History'), portal_language)
549            data.append(Paragraph(hist_translation, style["Heading3"]))
550            data.extend(creator.fromStringList(student.history.messages))
551
552       # Insert content table (optionally on second page)
553        if tabledata and tableheader:
554            #data.append(PageBreak())
555            data.append(Spacer(1, 20))
556            data.append(Paragraph(view.content_title, style["Heading3"]))
557            contenttable = render_table_data(tableheader,tabledata)
558            data.append(contenttable)
559
560        # Insert signatures
561        if signatures and not sigs_in_footer:
562            data.append(Spacer(1, 20))
563            signaturetable = get_signature_table(signatures)
564            data.append(signaturetable)
565
566        view.response.setHeader(
567            'Content-Type', 'application/pdf')
568        try:
569            pdf_stream = creator.create_pdf(
570                data, None, doc_title, author=author, footer=footer_text,
571                note=note, sigs_in_footer=sigs_in_footer)
572        except IOError:
573            view.flash('Error in image file.')
574            return view.redirect(view.url(view.context))
575        return pdf_stream
576
577    TOTAL_CREDITS = 58
578
579    def maxCreditsExceeded(self, studylevel, course):
580        if self.TOTAL_CREDITS and \
581            studylevel.total_credits + course.credits > self.TOTAL_CREDITS:
582            return self.TOTAL_CREDITS
583        return 0
584
585    VERDICTS_DICT = {
586        '0': _('(not yet)'),
587        'A': 'Successful student',
588        'B': 'Student with carryover courses',
589        'C': 'Student on probation',
590        }
591
592    SEPARATORS_DICT = {
593        }
594
595    #: A prefix used when generating new student ids. Each student id will
596    #: start with this string. The default is 'K' for ``Kofa``.
597    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.