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

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

Do not display 'None' in score (and any other) column.

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