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

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

pep8.

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