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

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