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

Last change on this file since 11483 was 11452, checked in by Henrik Bettermann, 11 years ago

Only school fee payments are not allowed.

  • Property svn:keywords set to Id
File size: 35.0 KB
Line 
1## $Id: utils.py 11452 2014-02-27 10:05:57Z 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.schema.interfaces import ConstraintNotSatisfied
28from zope.component import getUtility, createObject
29from zope.formlib.form import setUpEditWidgets
30from zope.i18n import translate
31from waeup.kofa.interfaces import (
32    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
33    academic_sessions_vocab)
34from waeup.kofa.interfaces import MessageFactory as _
35from waeup.kofa.students.interfaces import IStudentsUtils
36from waeup.kofa.students.workflow import ADMITTED
37from waeup.kofa.browser.pdf import (
38    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
39    get_signature_tables)
40from waeup.kofa.browser.interfaces import IPDFCreator
41from waeup.kofa.utils.helpers import to_timezone
42
43SLIP_STYLE = [
44    ('VALIGN',(0,0),(-1,-1),'TOP'),
45    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
46    ]
47
48CONTENT_STYLE = [
49    ('VALIGN',(0,0),(-1,-1),'TOP'),
50    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
51    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
52    #('BACKGROUND',(0,0),(-1,0),colors.black),
53    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
54    ('BOX', (0,0), (-1,-1), 1, colors.black),
55
56    ]
57
58FONT_SIZE = 10
59FONT_COLOR = 'black'
60
61def trans(text, lang):
62    # shortcut
63    return translate(text, 'waeup.kofa', target_language=lang)
64
65def formatted_text(text, color=FONT_COLOR, lang='en'):
66    """Turn `text`, `color` and `size` into an HTML snippet.
67
68    The snippet is suitable for use with reportlab and generating PDFs.
69    Wraps the `text` into a ``<font>`` tag with passed attributes.
70
71    Also non-strings are converted. Raw strings are expected to be
72    utf-8 encoded (usually the case for widgets etc.).
73
74    Finally, a br tag is added if widgets contain div tags
75    which are not supported by reportlab.
76
77    The returned snippet is unicode type.
78    """
79    if not isinstance(text, unicode):
80        if isinstance(text, basestring):
81            text = text.decode('utf-8')
82        else:
83            text = unicode(text)
84    if text == 'None':
85        text = ''
86    # Mainly for boolean values we need our customized
87    # localisation of the zope domain
88    text = translate(text, 'zope', target_language=lang)
89    text = text.replace('</div>', '<br /></div>')
90    tag1 = u'<font color="%s">' % (color)
91    return tag1 + u'%s</font>' % text
92
93def generate_student_id():
94    students = grok.getSite()['students']
95    new_id = students.unique_student_id
96    return new_id
97
98def set_up_widgets(view, ignore_request=False):
99    view.adapters = {}
100    view.widgets = setUpEditWidgets(
101        view.form_fields, view.prefix, view.context, view.request,
102        adapters=view.adapters, for_display=True,
103        ignore_request=ignore_request
104        )
105
106def render_student_data(studentview, omit_fields=(), lang='en'):
107    """Render student table for an existing frame.
108    """
109    width, height = A4
110    set_up_widgets(studentview, ignore_request=True)
111    data_left = []
112    data_right = []
113    style = getSampleStyleSheet()
114    img = getUtility(IExtFileStore).getFileByContext(
115        studentview.context, attr='passport.jpg')
116    if img is None:
117        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
118        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
119    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
120    data_left.append([doc_img])
121    #data.append([Spacer(1, 12)])
122
123    f_label = trans(_('Name:'), lang)
124    f_label = Paragraph(f_label, ENTRY1_STYLE)
125    f_text = formatted_text(studentview.context.display_fullname)
126    f_text = Paragraph(f_text, ENTRY1_STYLE)
127    data_right.append([f_label,f_text])
128
129    for widget in studentview.widgets:
130        if 'name' in widget.name:
131            continue
132        f_label = translate(
133            widget.label.strip(), 'waeup.kofa',
134            target_language=lang)
135        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
136        f_text = formatted_text(widget(), lang=lang)
137        f_text = Paragraph(f_text, ENTRY1_STYLE)
138        data_right.append([f_label,f_text])
139
140    if getattr(studentview.context, 'certcode', None):
141        if not 'certificate' in omit_fields:
142            f_label = trans(_('Study Course:'), lang)
143            f_label = Paragraph(f_label, ENTRY1_STYLE)
144            f_text = formatted_text(
145                studentview.context['studycourse'].certificate.longtitle)
146            f_text = Paragraph(f_text, ENTRY1_STYLE)
147            data_right.append([f_label,f_text])
148        if not 'department' in omit_fields:
149            f_label = trans(_('Department:'), lang)
150            f_label = Paragraph(f_label, ENTRY1_STYLE)
151            f_text = formatted_text(
152                studentview.context[
153                'studycourse'].certificate.__parent__.__parent__.longtitle,
154                )
155            f_text = Paragraph(f_text, ENTRY1_STYLE)
156            data_right.append([f_label,f_text])
157        if not 'faculty' in omit_fields:
158            f_label = trans(_('Faculty:'), lang)
159            f_label = Paragraph(f_label, ENTRY1_STYLE)
160            f_text = formatted_text(
161                studentview.context[
162                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
163                )
164            f_text = Paragraph(f_text, ENTRY1_STYLE)
165            data_right.append([f_label,f_text])
166        if not 'current_mode' in omit_fields:
167            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
168            sm = studymodes_dict[studentview.context[
169                'studycourse'].certificate.study_mode]
170            f_label = trans(_('Study Mode:'), lang)
171            f_label = Paragraph(f_label, ENTRY1_STYLE)
172            f_text = formatted_text(sm)
173            f_text = Paragraph(f_text, ENTRY1_STYLE)
174            data_right.append([f_label,f_text])
175        if not 'entry_session' in omit_fields:
176            f_label = trans(_('Entry Session:'), lang)
177            f_label = Paragraph(f_label, ENTRY1_STYLE)
178            entry_session = studentview.context['studycourse'].entry_session
179            entry_session = academic_sessions_vocab.getTerm(entry_session).title
180            f_text = formatted_text(entry_session)
181            f_text = Paragraph(f_text, ENTRY1_STYLE)
182            data_right.append([f_label,f_text])
183        if not 'date_of_birth' in omit_fields:
184            f_label = trans(_('Date of Birth:'), lang)
185            f_label = Paragraph(f_label, ENTRY1_STYLE)
186            date_of_birth = studentview.context.date_of_birth
187            tz = getUtility(IKofaUtils).tzinfo
188            date_of_birth = to_timezone(date_of_birth, tz)
189            if date_of_birth is not None:
190                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
191            f_text = formatted_text(date_of_birth)
192            f_text = Paragraph(f_text, ENTRY1_STYLE)
193            data_right.append([f_label,f_text])
194
195    table_left = Table(data_left,style=SLIP_STYLE)
196    table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
197    table = Table([[table_left, table_right],],style=SLIP_STYLE)
198    return table
199
200def render_table_data(tableheader, tabledata, lang='en'):
201    """Render children table for an existing frame.
202    """
203    data = []
204    #data.append([Spacer(1, 12)])
205    line = []
206    style = getSampleStyleSheet()
207    for element in tableheader:
208        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
209        field = Paragraph(field, style["Normal"])
210        line.append(field)
211    data.append(line)
212    for ticket in tabledata:
213        line = []
214        for element in tableheader:
215              field = formatted_text(getattr(ticket,element[1],u' '))
216              field = Paragraph(field, style["Normal"])
217              line.append(field)
218        data.append(line)
219    table = Table(data,colWidths=[
220        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
221    return table
222
223def render_transcript_data(view, tableheader, levels_data, lang='en'):
224    """Render children table for an existing frame.
225    """
226    data = []
227    style = getSampleStyleSheet()
228    for level in levels_data:
229        level_obj = level['level']
230        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
231        headerline = []
232        tabledata = []
233        subheader = '%s %s, %s %s' % (
234            trans(_('Session'), lang),
235            view.session_dict[level_obj.level_session],
236            trans(_('Level'), lang),
237            view.level_dict[level_obj.level])
238        data.append(Paragraph(subheader, HEADING_STYLE))
239        for element in tableheader:
240            field = '<strong>%s</strong>' % formatted_text(element[0])
241            field = Paragraph(field, style["Normal"])
242            headerline.append(field)
243        tabledata.append(headerline)
244        for ticket in tickets:
245            ticketline = []
246            for element in tableheader:
247                  field = formatted_text(getattr(ticket,element[1],u' '))
248                  field = Paragraph(field, style["Normal"])
249                  ticketline.append(field)
250            tabledata.append(ticketline)
251        table = Table(tabledata,colWidths=[
252            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
253        data.append(table)
254        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
255        data.append(Paragraph(sgpa, style["Normal"]))
256    return data
257
258def docs_as_flowables(view, lang='en'):
259    """Create reportlab flowables out of scanned docs.
260    """
261    # XXX: fix circular import problem
262    from waeup.kofa.students.viewlets import FileManager
263    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
264    style = getSampleStyleSheet()
265    data = []
266
267    # Collect viewlets
268    fm = FileManager(view.context, view.request, view)
269    fm.update()
270    if fm.viewlets:
271        sc_translation = trans(_('Scanned Documents'), lang)
272        data.append(Paragraph(sc_translation, HEADING_STYLE))
273        # Insert list of scanned documents
274        table_data = []
275        for viewlet in fm.viewlets:
276            if viewlet.file_exists:
277                # Show viewlet only if file exists
278                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
279                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
280                    view.context, attr=viewlet.download_name), 'name', None)
281                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
282                if img_path is None:
283                    pass
284                elif not img_path[-4:] in ('.jpg', '.JPG'):
285                    # reportlab requires jpg images, I think.
286                    f_text = Paragraph('%s (not displayable)' % (
287                        viewlet.title,), ENTRY1_STYLE)
288                else:
289                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
290                table_data.append([f_label, f_text])
291        if table_data:
292            # safety belt; empty tables lead to problems.
293            data.append(Table(table_data, style=SLIP_STYLE))
294    return data
295
296class StudentsUtils(grok.GlobalUtility):
297    """A collection of methods subject to customization.
298    """
299    grok.implements(IStudentsUtils)
300
301    def getReturningData(self, student):
302        """ Define what happens after school fee payment
303        depending on the student's senate verdict.
304
305        In the base configuration current level is always increased
306        by 100 no matter which verdict has been assigned.
307        """
308        new_level = student['studycourse'].current_level + 100
309        new_session = student['studycourse'].current_session + 1
310        return new_session, new_level
311
312    def setReturningData(self, student):
313        """ Define what happens after school fee payment
314        depending on the student's senate verdict.
315
316        This method folllows the same algorithm as getReturningData but
317        it also sets the new values.
318        """
319        new_session, new_level = self.getReturningData(student)
320        try:
321            student['studycourse'].current_level = new_level
322        except ConstraintNotSatisfied:
323            # Do not change level if level exceeds the
324            # certificate's end_level.
325            pass
326        student['studycourse'].current_session = new_session
327        verdict = student['studycourse'].current_verdict
328        student['studycourse'].current_verdict = '0'
329        student['studycourse'].previous_verdict = verdict
330        return
331
332    def _getSessionConfiguration(self, session):
333        try:
334            return grok.getSite()['configuration'][str(session)]
335        except KeyError:
336            return None
337
338    def _isPaymentDisabled(self, p_session, category, student):
339        academic_session = self._getSessionConfiguration(p_session)
340        if category == 'schoolfee' and \
341            'sf_all' in academic_session.payment_disabled:
342            return True
343        return False
344
345    def setPaymentDetails(self, category, student,
346            previous_session, previous_level):
347        """Create Payment object and set the payment data of a student for
348        the payment category specified.
349
350        """
351        p_item = u''
352        amount = 0.0
353        if previous_session:
354            if previous_session < student['studycourse'].entry_session:
355                return _('The previous session must not fall below '
356                         'your entry session.'), None
357            if category == 'schoolfee':
358                # School fee is always paid for the following session
359                if previous_session > student['studycourse'].current_session:
360                    return _('This is not a previous session.'), None
361            else:
362                if previous_session > student['studycourse'].current_session - 1:
363                    return _('This is not a previous session.'), None
364            p_session = previous_session
365            p_level = previous_level
366            p_current = False
367        else:
368            p_session = student['studycourse'].current_session
369            p_level = student['studycourse'].current_level
370            p_current = True
371        academic_session = self._getSessionConfiguration(p_session)
372        if academic_session == None:
373            return _(u'Session configuration object is not available.'), None
374        # Determine fee.
375        if category == 'schoolfee':
376            try:
377                certificate = student['studycourse'].certificate
378                p_item = certificate.code
379            except (AttributeError, TypeError):
380                return _('Study course data are incomplete.'), None
381            if previous_session:
382                # Students can pay for previous sessions in all
383                # workflow states.  Fresh students are excluded by the
384                # update method of the PreviousPaymentAddFormPage.
385                if previous_level == 100:
386                    amount = getattr(certificate, 'school_fee_1', 0.0)
387                else:
388                    amount = getattr(certificate, 'school_fee_2', 0.0)
389            else:
390                if student.state == CLEARED:
391                    amount = getattr(certificate, 'school_fee_1', 0.0)
392                elif student.state == RETURNING:
393                    # In case of returning school fee payment the
394                    # payment session and level contain the values of
395                    # the session the student has paid for. Payment
396                    # session is always next session.
397                    p_session, p_level = self.getReturningData(student)
398                    academic_session = self._getSessionConfiguration(p_session)
399                    if academic_session == None:
400                        return _(
401                            u'Session configuration object is not available.'
402                            ), None
403                    amount = getattr(certificate, 'school_fee_2', 0.0)
404                elif student.is_postgrad and student.state == PAID:
405                    # Returning postgraduate students also pay for the
406                    # next session but their level always remains the
407                    # same.
408                    p_session += 1
409                    academic_session = self._getSessionConfiguration(p_session)
410                    if academic_session == None:
411                        return _(
412                            u'Session configuration object is not available.'
413                            ), None
414                    amount = getattr(certificate, 'school_fee_2', 0.0)
415        elif category == 'clearance':
416            try:
417                p_item = student['studycourse'].certificate.code
418            except (AttributeError, TypeError):
419                return _('Study course data are incomplete.'), None
420            amount = academic_session.clearance_fee
421        elif category == 'bed_allocation':
422            p_item = self.getAccommodationDetails(student)['bt']
423            amount = academic_session.booking_fee
424        elif category == 'hostel_maintenance':
425            amount = 0.0
426            bedticket = student['accommodation'].get(
427                str(student.current_session), None)
428            if bedticket:
429                p_item = bedticket.bed_coordinates
430                if bedticket.bed.__parent__.maint_fee > 0:
431                    amount = bedticket.bed.__parent__.maint_fee
432                else:
433                    # fallback
434                    amount = academic_session.maint_fee
435            else:
436                # Should not happen because this is already checked
437                # in the browser module, but anyway ...
438                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
439                p_item = trans(_('no bed allocated'), portal_language)
440        elif category == 'transcript':
441            amount = academic_session.transcript_fee
442        if amount in (0.0, None):
443            return _('Amount could not be determined.'), None
444        for key in student['payments'].keys():
445            ticket = student['payments'][key]
446            if ticket.p_state == 'paid' and\
447               ticket.p_category == category and \
448               ticket.p_item == p_item and \
449               ticket.p_session == p_session:
450                  return _('This type of payment has already been made.'), None
451        if self._isPaymentDisabled(p_session, category, student):
452            return _('Payment temporarily disabled.'), None
453        payment = createObject(u'waeup.StudentOnlinePayment')
454        timestamp = ("%d" % int(time()*10000))[1:]
455        payment.p_id = "p%s" % timestamp
456        payment.p_category = category
457        payment.p_item = p_item
458        payment.p_session = p_session
459        payment.p_level = p_level
460        payment.p_current = p_current
461        payment.amount_auth = amount
462        return None, payment
463
464    def setBalanceDetails(self, category, student,
465            balance_session, balance_level, balance_amount):
466        """Create Payment object and set the payment data of a student for.
467
468        """
469        p_item = u'Balance'
470        p_session = balance_session
471        p_level = balance_level
472        p_current = False
473        amount = balance_amount
474        academic_session = self._getSessionConfiguration(p_session)
475        if academic_session == None:
476            return _(u'Session configuration object is not available.'), None
477        if amount in (0.0, None) or amount < 0:
478            return _('Amount must be greater than 0.'), None
479        for key in student['payments'].keys():
480            ticket = student['payments'][key]
481            if ticket.p_state == 'paid' and\
482               ticket.p_category == 'balance' and \
483               ticket.p_item == p_item and \
484               ticket.p_session == p_session:
485                  return _('This type of payment has already been made.'), None
486        payment = createObject(u'waeup.StudentOnlinePayment')
487        timestamp = ("%d" % int(time()*10000))[1:]
488        payment.p_id = "p%s" % timestamp
489        payment.p_category = category
490        payment.p_item = p_item
491        payment.p_session = p_session
492        payment.p_level = p_level
493        payment.p_current = p_current
494        payment.amount_auth = amount
495        return None, payment
496
497    def getAccommodationDetails(self, student):
498        """Determine the accommodation data of a student.
499        """
500        d = {}
501        d['error'] = u''
502        hostels = grok.getSite()['hostels']
503        d['booking_session'] = hostels.accommodation_session
504        d['allowed_states'] = hostels.accommodation_states
505        d['startdate'] = hostels.startdate
506        d['enddate'] = hostels.enddate
507        d['expired'] = hostels.expired
508        # Determine bed type
509        studycourse = student['studycourse']
510        certificate = getattr(studycourse,'certificate',None)
511        entry_session = studycourse.entry_session
512        current_level = studycourse.current_level
513        if None in (entry_session, current_level, certificate):
514            return d
515        end_level = certificate.end_level
516        if current_level == 10:
517            bt = 'pr'
518        elif entry_session == grok.getSite()['hostels'].accommodation_session:
519            bt = 'fr'
520        elif current_level >= end_level:
521            bt = 'fi'
522        else:
523            bt = 're'
524        if student.sex == 'f':
525            sex = 'female'
526        else:
527            sex = 'male'
528        special_handling = 'regular'
529        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
530        return d
531
532    def selectBed(self, available_beds):
533        """Select a bed from a list of available beds.
534
535        In the base configuration we select the first bed found,
536        but can also randomize the selection if we like.
537        """
538        return available_beds[0]
539
540    def _admissionText(self, student, portal_language):
541        inst_name = grok.getSite()['configuration'].name
542        text = trans(_(
543            'This is to inform you that you have been provisionally'
544            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
545            portal_language)
546        return text
547
548    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
549                                 pre_text=None, post_text=None,):
550        """Render pdf admission letter.
551        """
552        if student is None:
553            return
554        style = getSampleStyleSheet()
555        creator = self.getPDFCreator(student)
556        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
557        data = []
558        doc_title = view.label
559        author = '%s (%s)' % (view.request.principal.title,
560                              view.request.principal.id)
561        footer_text = view.label.split('\n')
562        if len(footer_text) > 1:
563            # We can add a department in first line
564            footer_text = footer_text[1]
565        else:
566            # Only the first line is used for the footer
567            footer_text = footer_text[0]
568        if getattr(student, 'student_id', None) is not None:
569            footer_text = "%s - %s - " % (student.student_id, footer_text)
570
571        # Text before student data
572        if pre_text is None:
573            html = format_html(self._admissionText(student, portal_language))
574        else:
575            html = format_html(pre_text)
576        data.append(Paragraph(html, NOTE_STYLE))
577        data.append(Spacer(1, 20))
578
579        # Student data
580        data.append(render_student_data(view, omit_fields, lang=portal_language))
581
582        # Text after student data
583        data.append(Spacer(1, 20))
584        if post_text is None:
585            datelist = student.history.messages[0].split()[0].split('-')
586            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
587            post_text = trans(_(
588                'Your Kofa student record was created on ${a}.',
589                mapping = {'a': creation_date}),
590                portal_language)
591        #html = format_html(post_text)
592        #data.append(Paragraph(html, NOTE_STYLE))
593
594        # Create pdf stream
595        view.response.setHeader(
596            'Content-Type', 'application/pdf')
597        pdf_stream = creator.create_pdf(
598            data, None, doc_title, author=author, footer=footer_text,
599            note=post_text)
600        return pdf_stream
601
602    def getPDFCreator(self, context):
603        """Get a pdf creator suitable for `context`.
604
605        The default implementation always returns the default creator.
606        """
607        return getUtility(IPDFCreator)
608
609    def renderPDF(self, view, filename='slip.pdf', student=None,
610                  studentview=None,
611                  tableheader=[], tabledata=[],
612                  note=None, signatures=None, sigs_in_footer=(),
613                  show_scans=True, topMargin=1.5,
614                  omit_fields=()):
615        """Render pdf slips for various pages.
616        """
617        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
618        # XXX: tell what the different parameters mean
619        style = getSampleStyleSheet()
620        creator = self.getPDFCreator(student)
621        data = []
622        doc_title = view.label
623        author = '%s (%s)' % (view.request.principal.title,
624                              view.request.principal.id)
625        footer_text = view.label.split('\n')
626        if len(footer_text) > 2:
627            # We can add a department in first line
628            footer_text = footer_text[1]
629        else:
630            # Only the first line is used for the footer
631            footer_text = footer_text[0]
632        if getattr(student, 'student_id', None) is not None:
633            footer_text = "%s - %s - " % (student.student_id, footer_text)
634
635        # Insert student data table
636        if student is not None:
637            bd_translation = trans(_('Base Data'), portal_language)
638            data.append(Paragraph(bd_translation, HEADING_STYLE))
639            data.append(render_student_data(
640                studentview, omit_fields, lang=portal_language))
641
642        # Insert widgets
643        if view.form_fields:
644            data.append(Paragraph(view.title, HEADING_STYLE))
645            separators = getattr(self, 'SEPARATORS_DICT', {})
646            table = creator.getWidgetsTable(
647                view.form_fields, view.context, None, lang=portal_language,
648                separators=separators)
649            data.append(table)
650
651        # Insert scanned docs
652        if show_scans:
653            data.extend(docs_as_flowables(view, portal_language))
654
655        # Insert history
656        if filename.startswith('clearance'):
657            hist_translation = trans(_('Workflow History'), portal_language)
658            data.append(Paragraph(hist_translation, HEADING_STYLE))
659            data.extend(creator.fromStringList(student.history.messages))
660
661        # Insert content tables (optionally on second page)
662        if hasattr(view, 'tabletitle'):
663            for i in range(len(view.tabletitle)):
664                if tabledata[i] and tableheader[i]:
665                    #data.append(PageBreak())
666                    #data.append(Spacer(1, 20))
667                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
668                    data.append(Spacer(1, 8))
669                    contenttable = render_table_data(tableheader[i],tabledata[i])
670                    data.append(contenttable)
671
672        # Insert signatures
673        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
674        # do not have a test for the following lines.
675        if signatures and not sigs_in_footer:
676            data.append(Spacer(1, 20))
677            # Render one signature table per signature to
678            # get date and signature in line.
679            for signature in signatures:
680                signaturetables = get_signature_tables(signature)
681                data.append(signaturetables[0])
682
683        view.response.setHeader(
684            'Content-Type', 'application/pdf')
685        try:
686            pdf_stream = creator.create_pdf(
687                data, None, doc_title, author=author, footer=footer_text,
688                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
689        except IOError:
690            view.flash('Error in image file.')
691            return view.redirect(view.url(view.context))
692        return pdf_stream
693
694    gpa_boundaries = ((1, 'Fail'),
695                      (1.5, 'Pass'),
696                      (2.4, '3rd Class'),
697                      (3.5, '2nd Class Lower'),
698                      (4.5, '2nd Class Upper'),
699                      (5, '1st Class'))
700
701    def getClassFromCGPA(self, gpa):
702        if gpa < self.gpa_boundaries[0][0]:
703            return 0, self.gpa_boundaries[0][1]
704        if gpa < self.gpa_boundaries[1][0]:
705            return 1, self.gpa_boundaries[1][1]
706        if gpa < self.gpa_boundaries[2][0]:
707            return 2, self.gpa_boundaries[2][1]
708        if gpa < self.gpa_boundaries[3][0]:
709            return 3, self.gpa_boundaries[3][1]
710        if gpa < self.gpa_boundaries[4][0]:
711            return 4, self.gpa_boundaries[4][1]
712        if gpa <= self.gpa_boundaries[5][0]:
713            return 5, self.gpa_boundaries[5][1]
714        return 'N/A'
715
716    def renderPDFTranscript(self, view, filename='transcript.pdf',
717                  student=None,
718                  studentview=None,
719                  note=None, signatures=None, sigs_in_footer=(),
720                  show_scans=True, topMargin=1.5,
721                  omit_fields=(),
722                  tableheader=None):
723        """Render pdf slips for transcript.
724        """
725        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
726        # XXX: tell what the different parameters mean
727        style = getSampleStyleSheet()
728        creator = self.getPDFCreator(student)
729        data = []
730        doc_title = view.label
731        author = '%s (%s)' % (view.request.principal.title,
732                              view.request.principal.id)
733        footer_text = view.label.split('\n')
734        if len(footer_text) > 2:
735            # We can add a department in first line
736            footer_text = footer_text[1]
737        else:
738            # Only the first line is used for the footer
739            footer_text = footer_text[0]
740        if getattr(student, 'student_id', None) is not None:
741            footer_text = "%s - %s - " % (student.student_id, footer_text)
742
743        # Insert student data table
744        if student is not None:
745            #bd_translation = trans(_('Base Data'), portal_language)
746            #data.append(Paragraph(bd_translation, HEADING_STYLE))
747            data.append(render_student_data(
748                studentview, omit_fields, lang=portal_language))
749
750
751        transcript_data = view.context.getTranscriptData()
752        levels_data = transcript_data[0]
753        gpa = transcript_data[1]
754
755        contextdata = []
756        f_label = trans(_('Course of Study:'), portal_language)
757        f_label = Paragraph(f_label, ENTRY1_STYLE)
758        f_text = formatted_text(view.context.certificate.longtitle)
759        f_text = Paragraph(f_text, ENTRY1_STYLE)
760        contextdata.append([f_label,f_text])
761
762        f_label = trans(_('Faculty:'), portal_language)
763        f_label = Paragraph(f_label, ENTRY1_STYLE)
764        f_text = formatted_text(
765            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
766        f_text = Paragraph(f_text, ENTRY1_STYLE)
767        contextdata.append([f_label,f_text])
768
769        f_label = trans(_('Department:'), portal_language)
770        f_label = Paragraph(f_label, ENTRY1_STYLE)
771        f_text = formatted_text(
772            view.context.certificate.__parent__.__parent__.longtitle)
773        f_text = Paragraph(f_text, ENTRY1_STYLE)
774        contextdata.append([f_label,f_text])
775
776        f_label = trans(_('Entry Session:'), portal_language)
777        f_label = Paragraph(f_label, ENTRY1_STYLE)
778        f_text = formatted_text(
779            view.session_dict.get(view.context.entry_session))
780        f_text = Paragraph(f_text, ENTRY1_STYLE)
781        contextdata.append([f_label,f_text])
782
783        f_label = trans(_('Entry Mode:'), portal_language)
784        f_label = Paragraph(f_label, ENTRY1_STYLE)
785        f_text = formatted_text(view.studymode_dict.get(
786            view.context.entry_mode))
787        f_text = Paragraph(f_text, ENTRY1_STYLE)
788        contextdata.append([f_label,f_text])
789
790        f_label = trans(_('Cumulative GPA:'), portal_language)
791        f_label = Paragraph(f_label, ENTRY1_STYLE)
792        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
793        f_text = Paragraph(f_text, ENTRY1_STYLE)
794        contextdata.append([f_label,f_text])
795
796        contexttable = Table(contextdata,style=SLIP_STYLE)
797        data.append(contexttable)
798
799        transcripttables = render_transcript_data(
800            view, tableheader, levels_data, lang=portal_language)
801        data.extend(transcripttables)
802
803        # Insert signatures
804        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
805        # do not have a test for the following lines.
806        if signatures and not sigs_in_footer:
807            data.append(Spacer(1, 20))
808            # Render one signature table per signature to
809            # get date and signature in line.
810            for signature in signatures:
811                signaturetables = get_signature_tables(signature)
812                data.append(signaturetables[0])
813
814        view.response.setHeader(
815            'Content-Type', 'application/pdf')
816        try:
817            pdf_stream = creator.create_pdf(
818                data, None, doc_title, author=author, footer=footer_text,
819                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
820        except IOError:
821            view.flash(_('Error in image file.'))
822            return view.redirect(view.url(view.context))
823        return pdf_stream
824
825    def maxCredits(self, studylevel):
826        """Return maximum credits.
827
828        In some universities maximum credits is not constant, it
829        depends on the student's study level.
830        """
831        return 50
832
833    def maxCreditsExceeded(self, studylevel, course):
834        max_credits = self.maxCredits(studylevel)
835        if max_credits and \
836            studylevel.total_credits + course.credits > max_credits:
837            return max_credits
838        return 0
839
840    def getBedCoordinates(self, bedticket):
841        """Return bed coordinates.
842
843        This method can be used to customize the display_coordinates
844        property method.
845        """
846        return bedticket.bed_coordinates
847
848    VERDICTS_DICT = {
849        '0': _('(not yet)'),
850        'A': 'Successful student',
851        'B': 'Student with carryover courses',
852        'C': 'Student on probation',
853        }
854
855    SEPARATORS_DICT = {
856        }
857
858    SKIP_UPLOAD_VIEWLETS = ()
859
860    PWCHANGE_STATES = (ADMITTED,)
861
862    #: A prefix used when generating new student ids. Each student id will
863    #: start with this string. The default is 'K' for ``Kofa``.
864    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.