source: main/waeup.kofa/branches/uli-stud-utils-cleanup/src/waeup/kofa/students/utils.py @ 13116

Last change on this file since 13116 was 11912, checked in by uli, 10 years ago

Remove trash.

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