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

Last change on this file since 14287 was 14256, checked in by Henrik Bettermann, 8 years ago

Catch Reportlab LayoutError.

  • Property svn:keywords set to Id
File size: 44.0 KB
Line 
1## $Id: utils.py 14256 2016-11-04 05:23:01Z 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 students 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 reportlab.platypus.doctemplate import LayoutError
28from zope.event import notify
29from zope.schema.interfaces import ConstraintNotSatisfied
30from zope.component import getUtility, createObject
31from zope.formlib.form import setUpEditWidgets
32from zope.i18n import translate
33from waeup.kofa.interfaces import (
34    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
35    academic_sessions_vocab)
36from waeup.kofa.interfaces import MessageFactory as _
37from waeup.kofa.students.interfaces import IStudentsUtils
38from waeup.kofa.students.workflow import ADMITTED
39from waeup.kofa.students.vocabularies import StudyLevelSource, MatNumNotInSource
40from waeup.kofa.browser.pdf import (
41    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
42    get_signature_tables, get_qrcode)
43from waeup.kofa.browser.interfaces import IPDFCreator
44from waeup.kofa.utils.helpers import to_timezone
45
46SLIP_STYLE = [
47    ('VALIGN',(0,0),(-1,-1),'TOP'),
48    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
49    ]
50
51CONTENT_STYLE = [
52    ('VALIGN',(0,0),(-1,-1),'TOP'),
53    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
54    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
55    #('BACKGROUND',(0,0),(-1,0),colors.black),
56    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
57    ('BOX', (0,0), (-1,-1), 1, colors.black),
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    # Very long matriculation numbers need to be wrapped
89    if text.find(' ') == -1 and len(text.split('/')) > 6:
90        text = '/'.join(text.split('/')[:5]) + \
91            '/ ' + '/'.join(text.split('/')[5:])
92    # Mainly for boolean values we need our customized
93    # localisation of the zope domain
94    text = translate(text, 'zope', target_language=lang)
95    text = text.replace('</div>', '<br /></div>')
96    tag1 = u'<font color="%s">' % (color)
97    return tag1 + u'%s</font>' % text
98
99def generate_student_id():
100    students = grok.getSite()['students']
101    new_id = students.unique_student_id
102    return new_id
103
104def set_up_widgets(view, ignore_request=False):
105    view.adapters = {}
106    view.widgets = setUpEditWidgets(
107        view.form_fields, view.prefix, view.context, view.request,
108        adapters=view.adapters, for_display=True,
109        ignore_request=ignore_request
110        )
111
112def render_student_data(studentview, context, omit_fields=(),
113                        lang='en', slipname=None):
114    """Render student table for an existing frame.
115    """
116    width, height = A4
117    set_up_widgets(studentview, ignore_request=True)
118    data_left = []
119    data_middle = []
120    style = getSampleStyleSheet()
121    img = getUtility(IExtFileStore).getFileByContext(
122        studentview.context, attr='passport.jpg')
123    if img is None:
124        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
125        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
126    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
127    data_left.append([doc_img])
128    #data.append([Spacer(1, 12)])
129
130    f_label = trans(_('Name:'), lang)
131    f_label = Paragraph(f_label, ENTRY1_STYLE)
132    f_text = formatted_text(studentview.context.display_fullname)
133    f_text = Paragraph(f_text, ENTRY1_STYLE)
134    data_middle.append([f_label,f_text])
135
136    for widget in studentview.widgets:
137        if 'name' in widget.name:
138            continue
139        f_label = translate(
140            widget.label.strip(), 'waeup.kofa',
141            target_language=lang)
142        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
143        f_text = formatted_text(widget(), lang=lang)
144        f_text = Paragraph(f_text, ENTRY1_STYLE)
145        data_middle.append([f_label,f_text])
146
147    if getattr(studentview.context, 'certcode', None):
148        if not 'certificate' in omit_fields:
149            f_label = trans(_('Study Course:'), lang)
150            f_label = Paragraph(f_label, ENTRY1_STYLE)
151            f_text = formatted_text(
152                studentview.context['studycourse'].certificate.longtitle)
153            f_text = Paragraph(f_text, ENTRY1_STYLE)
154            data_middle.append([f_label,f_text])
155        if not 'department' in omit_fields:
156            f_label = trans(_('Department:'), lang)
157            f_label = Paragraph(f_label, ENTRY1_STYLE)
158            f_text = formatted_text(
159                studentview.context[
160                'studycourse'].certificate.__parent__.__parent__.longtitle,
161                )
162            f_text = Paragraph(f_text, ENTRY1_STYLE)
163            data_middle.append([f_label,f_text])
164        if not 'faculty' in omit_fields:
165            f_label = trans(_('Faculty:'), lang)
166            f_label = Paragraph(f_label, ENTRY1_STYLE)
167            f_text = formatted_text(
168                studentview.context[
169                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
170                )
171            f_text = Paragraph(f_text, ENTRY1_STYLE)
172            data_middle.append([f_label,f_text])
173        if not 'current_mode' in omit_fields:
174            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
175            sm = studymodes_dict[studentview.context.current_mode]
176            f_label = trans(_('Study Mode:'), lang)
177            f_label = Paragraph(f_label, ENTRY1_STYLE)
178            f_text = formatted_text(sm)
179            f_text = Paragraph(f_text, ENTRY1_STYLE)
180            data_middle.append([f_label,f_text])
181        if not 'entry_session' in omit_fields:
182            f_label = trans(_('Entry Session:'), lang)
183            f_label = Paragraph(f_label, ENTRY1_STYLE)
184            entry_session = studentview.context.entry_session
185            entry_session = academic_sessions_vocab.getTerm(entry_session).title
186            f_text = formatted_text(entry_session)
187            f_text = Paragraph(f_text, ENTRY1_STYLE)
188            data_middle.append([f_label,f_text])
189        # Requested by Uniben, does not really make sense
190        if not 'current_level' in omit_fields:
191            f_label = trans(_('Current Level:'), lang)
192            f_label = Paragraph(f_label, ENTRY1_STYLE)
193            current_level = studentview.context['studycourse'].current_level
194            studylevelsource = StudyLevelSource().factory
195            current_level = studylevelsource.getTitle(
196                studentview.context, current_level)
197            f_text = formatted_text(current_level)
198            f_text = Paragraph(f_text, ENTRY1_STYLE)
199            data_middle.append([f_label,f_text])
200        if not 'date_of_birth' in omit_fields:
201            f_label = trans(_('Date of Birth:'), lang)
202            f_label = Paragraph(f_label, ENTRY1_STYLE)
203            date_of_birth = studentview.context.date_of_birth
204            tz = getUtility(IKofaUtils).tzinfo
205            date_of_birth = to_timezone(date_of_birth, tz)
206            if date_of_birth is not None:
207                date_of_birth = date_of_birth.strftime("%d/%m/%Y")
208            f_text = formatted_text(date_of_birth)
209            f_text = Paragraph(f_text, ENTRY1_STYLE)
210            data_middle.append([f_label,f_text])
211
212    # append QR code to the right
213    if slipname:
214        url = studentview.url(context, slipname)
215        data_right = [[get_qrcode(url, width=70.0)]]
216        table_right = Table(data_right,style=SLIP_STYLE)
217    else:
218        table_right = None
219
220    table_left = Table(data_left,style=SLIP_STYLE)
221    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
222    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
223    return table
224
225def render_table_data(tableheader, tabledata, lang='en'):
226    """Render children table for an existing frame.
227    """
228    data = []
229    #data.append([Spacer(1, 12)])
230    line = []
231    style = getSampleStyleSheet()
232    for element in tableheader:
233        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
234        field = Paragraph(field, style["Normal"])
235        line.append(field)
236    data.append(line)
237    for ticket in tabledata:
238        line = []
239        for element in tableheader:
240              field = formatted_text(getattr(ticket,element[1],u' '))
241              field = Paragraph(field, style["Normal"])
242              line.append(field)
243        data.append(line)
244    table = Table(data,colWidths=[
245        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
246    return table
247
248def render_transcript_data(view, tableheader, levels_data, lang='en'):
249    """Render children table for an existing frame.
250    """
251    data = []
252    style = getSampleStyleSheet()
253    for level in levels_data:
254        level_obj = level['level']
255        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
256        headerline = []
257        tabledata = []
258        subheader = '%s %s, %s %s' % (
259            trans(_('Session'), lang),
260            view.session_dict[level_obj.level_session],
261            trans(_('Level'), lang),
262            view.level_dict[level_obj.level])
263        data.append(Paragraph(subheader, HEADING_STYLE))
264        for element in tableheader:
265            field = '<strong>%s</strong>' % formatted_text(element[0])
266            field = Paragraph(field, style["Normal"])
267            headerline.append(field)
268        tabledata.append(headerline)
269        for ticket in tickets:
270            ticketline = []
271            for element in tableheader:
272                  field = formatted_text(getattr(ticket,element[1],u' '))
273                  field = Paragraph(field, style["Normal"])
274                  ticketline.append(field)
275            tabledata.append(ticketline)
276        table = Table(tabledata,colWidths=[
277            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
278        data.append(table)
279        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
280        data.append(Paragraph(sgpa, style["Normal"]))
281    return data
282
283def docs_as_flowables(view, lang='en'):
284    """Create reportlab flowables out of scanned docs.
285    """
286    # XXX: fix circular import problem
287    from waeup.kofa.browser.fileviewlets import FileManager
288    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
289    style = getSampleStyleSheet()
290    data = []
291
292    # Collect viewlets
293    fm = FileManager(view.context, view.request, view)
294    fm.update()
295    if fm.viewlets:
296        sc_translation = trans(_('Scanned Documents'), lang)
297        data.append(Paragraph(sc_translation, HEADING_STYLE))
298        # Insert list of scanned documents
299        table_data = []
300        for viewlet in fm.viewlets:
301            if viewlet.file_exists:
302                # Show viewlet only if file exists
303                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
304                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
305                    view.context, attr=viewlet.download_name), 'name', None)
306                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
307                if img_path is None:
308                    pass
309                elif not img_path[-4:] in ('.jpg', '.JPG'):
310                    # reportlab requires jpg images, I think.
311                    f_text = Paragraph('%s (not displayable)' % (
312                        viewlet.title,), ENTRY1_STYLE)
313                else:
314                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
315                table_data.append([f_label, f_text])
316        if table_data:
317            # safety belt; empty tables lead to problems.
318            data.append(Table(table_data, style=SLIP_STYLE))
319    return data
320
321class StudentsUtils(grok.GlobalUtility):
322    """A collection of methods subject to customization.
323    """
324    grok.implements(IStudentsUtils)
325
326    def getReturningData(self, student):
327        """ Define what happens after school fee payment
328        depending on the student's senate verdict.
329        In the base configuration current level is always increased
330        by 100 no matter which verdict has been assigned.
331        """
332        new_level = student['studycourse'].current_level + 100
333        new_session = student['studycourse'].current_session + 1
334        return new_session, new_level
335
336    def setReturningData(self, student):
337        """ Define what happens after school fee payment
338        depending on the student's senate verdict.
339        This method folllows the same algorithm as `getReturningData` but
340        it also sets the new values.
341        """
342        new_session, new_level = self.getReturningData(student)
343        try:
344            student['studycourse'].current_level = new_level
345        except ConstraintNotSatisfied:
346            # Do not change level if level exceeds the
347            # certificate's end_level.
348            pass
349        student['studycourse'].current_session = new_session
350        verdict = student['studycourse'].current_verdict
351        student['studycourse'].current_verdict = '0'
352        student['studycourse'].previous_verdict = verdict
353        return
354
355    def _getSessionConfiguration(self, session):
356        try:
357            return grok.getSite()['configuration'][str(session)]
358        except KeyError:
359            return None
360
361    def _isPaymentDisabled(self, p_session, category, student):
362        academic_session = self._getSessionConfiguration(p_session)
363        if category == 'schoolfee' and \
364            'sf_all' in academic_session.payment_disabled:
365            return True
366        return False
367
368    def samePaymentMade(self, student, category, p_item, p_session):
369        for key in student['payments'].keys():
370            ticket = student['payments'][key]
371            if ticket.p_state == 'paid' and\
372               ticket.p_category == category and \
373               ticket.p_item == p_item and \
374               ticket.p_session == p_session:
375                  return True
376        return False
377
378    def setPaymentDetails(self, category, student,
379            previous_session, previous_level):
380        """Create a payment ticket and set the payment data of a
381        student for the payment category specified.
382        """
383        p_item = u''
384        amount = 0.0
385        if previous_session:
386            if previous_session < student['studycourse'].entry_session:
387                return _('The previous session must not fall below '
388                         'your entry session.'), None
389            if category == 'schoolfee':
390                # School fee is always paid for the following session
391                if previous_session > student['studycourse'].current_session:
392                    return _('This is not a previous session.'), None
393            else:
394                if previous_session > student['studycourse'].current_session - 1:
395                    return _('This is not a previous session.'), None
396            p_session = previous_session
397            p_level = previous_level
398            p_current = False
399        else:
400            p_session = student['studycourse'].current_session
401            p_level = student['studycourse'].current_level
402            p_current = True
403        academic_session = self._getSessionConfiguration(p_session)
404        if academic_session == None:
405            return _(u'Session configuration object is not available.'), None
406        # Determine fee.
407        if category == 'schoolfee':
408            try:
409                certificate = student['studycourse'].certificate
410                p_item = certificate.code
411            except (AttributeError, TypeError):
412                return _('Study course data are incomplete.'), None
413            if previous_session:
414                # Students can pay for previous sessions in all
415                # workflow states.  Fresh students are excluded by the
416                # update method of the PreviousPaymentAddFormPage.
417                if previous_level == 100:
418                    amount = getattr(certificate, 'school_fee_1', 0.0)
419                else:
420                    amount = getattr(certificate, 'school_fee_2', 0.0)
421            else:
422                if student.state == CLEARED:
423                    amount = getattr(certificate, 'school_fee_1', 0.0)
424                elif student.state == RETURNING:
425                    # In case of returning school fee payment the
426                    # payment session and level contain the values of
427                    # the session the student has paid for. Payment
428                    # session is always next session.
429                    p_session, p_level = self.getReturningData(student)
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 student.is_postgrad and student.state == PAID:
437                    # Returning postgraduate students also pay for the
438                    # next session but their level always remains the
439                    # same.
440                    p_session += 1
441                    academic_session = self._getSessionConfiguration(p_session)
442                    if academic_session == None:
443                        return _(
444                            u'Session configuration object is not available.'
445                            ), None
446                    amount = getattr(certificate, 'school_fee_2', 0.0)
447        elif category == 'clearance':
448            try:
449                p_item = student['studycourse'].certificate.code
450            except (AttributeError, TypeError):
451                return _('Study course data are incomplete.'), None
452            amount = academic_session.clearance_fee
453        elif category == 'bed_allocation':
454            p_item = self.getAccommodationDetails(student)['bt']
455            amount = academic_session.booking_fee
456        elif category == 'hostel_maintenance':
457            amount = 0.0
458            bedticket = student['accommodation'].get(
459                str(student.current_session), None)
460            if bedticket is not None and bedticket.bed is not None:
461                p_item = bedticket.bed_coordinates
462                if bedticket.bed.__parent__.maint_fee > 0:
463                    amount = bedticket.bed.__parent__.maint_fee
464                else:
465                    # fallback
466                    amount = academic_session.maint_fee
467            else:
468                return _(u'No bed allocated.'), None
469        elif category == 'transcript':
470            amount = academic_session.transcript_fee
471        elif category == 'transfer':
472            amount = academic_session.transfer_fee
473        elif category == 'late_registration':
474            amount = academic_session.late_registration_fee
475        if amount in (0.0, None):
476            return _('Amount could not be determined.'), None
477        if self.samePaymentMade(student, category, p_item, p_session):
478            return _('This type of payment has already been made.'), None
479        if self._isPaymentDisabled(p_session, category, student):
480            return _('This category of payments has been disabled.'), None
481        payment = createObject(u'waeup.StudentOnlinePayment')
482        timestamp = ("%d" % int(time()*10000))[1:]
483        payment.p_id = "p%s" % timestamp
484        payment.p_category = category
485        payment.p_item = p_item
486        payment.p_session = p_session
487        payment.p_level = p_level
488        payment.p_current = p_current
489        payment.amount_auth = amount
490        return None, payment
491
492    def setBalanceDetails(self, category, student,
493            balance_session, balance_level, balance_amount):
494        """Create a balance payment ticket and set the payment data
495        as selected by the student.
496        """
497        p_item = u'Balance'
498        p_session = balance_session
499        p_level = balance_level
500        p_current = False
501        amount = balance_amount
502        academic_session = self._getSessionConfiguration(p_session)
503        if academic_session == None:
504            return _(u'Session configuration object is not available.'), None
505        if amount in (0.0, None) or amount < 0:
506            return _('Amount must be greater than 0.'), 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 increaseMatricInteger(self, student):
519        """Increase counter for matric numbers.
520        This counter can be a centrally stored attribute or an attribute of
521        faculties, departments or certificates. In the base package the counter
522        is as an attribute of the site configuration container.
523        """
524        grok.getSite()['configuration'].next_matric_integer += 1
525        return
526
527    def constructMatricNumber(self, student):
528        """Fetch the matric number counter which fits the student and
529        construct the new matric number of the student.
530        In the base package the counter is returned which is as an attribute
531        of the site configuration container.
532        """
533        next_integer = grok.getSite()['configuration'].next_matric_integer
534        if next_integer == 0:
535            return _('Matriculation number cannot be set.'), None
536        return None, unicode(next_integer)
537
538    def setMatricNumber(self, student):
539        """Set matriculation number of student. If the student's matric number
540        is unset a new matric number is
541        constructed according to the matriculation number construction rules
542        defined in the `constructMatricNumber` method. The new matric number is
543        set, the students catalog updated. The corresponding matric number
544        counter is 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 `increaseMatricInteger` and `constructMatricNumber`
549        according to the university's matriculation number construction rules.
550
551        The method can be disabled by setting the counter 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 %s exists.' % matric_number), None
564        notify(grok.ObjectModifiedEvent(student))
565        self.increaseMatricInteger(student)
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 checkAccommodationRequirements(self, student, acc_details):
604        if acc_details.get('expired', False):
605            startdate = acc_details.get('startdate')
606            enddate = acc_details.get('enddate')
607            if startdate and enddate:
608                tz = getUtility(IKofaUtils).tzinfo
609                startdate = to_timezone(
610                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
611                enddate = to_timezone(
612                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
613                return _("Outside booking period: ${a} - ${b}",
614                         mapping = {'a': startdate, 'b': enddate})
615            else:
616                return _("Outside booking period.")
617        if not acc_details.get('bt'):
618            return _("Your data are incomplete.")
619        if not student.state in acc_details['allowed_states']:
620            return _("You are in the wrong registration state.")
621        if student['studycourse'].current_session != acc_details[
622            'booking_session']:
623            return _('Your current session does not '
624                     'match accommodation session.')
625        if str(acc_details['booking_session']) in student['accommodation'].keys():
626            return _('You already booked a bed space in '
627                     'current accommodation session.')
628        return
629
630    def selectBed(self, available_beds, desired_hostel=None):
631        """Select a bed from a filtered list of available beds.
632        In the base configuration beds are sorted by the sort id
633        of the hostel and the bed number. The first bed found in
634        this sorted list is taken.
635        """
636        sorted_beds = sorted(available_beds,
637                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
638        if desired_hostel:
639            # Filter desired hostel beds
640            filtered_beds = [bed for bed in sorted_beds
641                             if bed.bed_id.startswith(desired_hostel)]
642            if not filtered_beds:
643                return
644            return filtered_beds[0]
645        return sorted_beds[0]
646
647    def _admissionText(self, student, portal_language):
648        inst_name = grok.getSite()['configuration'].name
649        text = trans(_(
650            'This is to inform you that you have been provisionally'
651            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
652            portal_language)
653        return text
654
655    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
656                                 pre_text=None, post_text=None,):
657        """Render pdf admission letter.
658        """
659        if student is None:
660            return
661        style = getSampleStyleSheet()
662        creator = self.getPDFCreator(student)
663        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
664        data = []
665        doc_title = view.label
666        author = '%s (%s)' % (view.request.principal.title,
667                              view.request.principal.id)
668        footer_text = view.label.split('\n')
669        if len(footer_text) > 1:
670            # We can add a department in first line
671            footer_text = footer_text[1]
672        else:
673            # Only the first line is used for the footer
674            footer_text = footer_text[0]
675        if getattr(student, 'student_id', None) is not None:
676            footer_text = "%s - %s - " % (student.student_id, footer_text)
677
678        # Text before student data
679        if pre_text is None:
680            html = format_html(self._admissionText(student, portal_language))
681        else:
682            html = format_html(pre_text)
683        if html:
684            data.append(Paragraph(html, NOTE_STYLE))
685            data.append(Spacer(1, 20))
686
687        # Student data
688        data.append(render_student_data(view, student,
689                    omit_fields, lang=portal_language,
690                    slipname='admission_slip.pdf'))
691
692        # Text after student data
693        data.append(Spacer(1, 20))
694        if post_text is None:
695            datelist = student.history.messages[0].split()[0].split('-')
696            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
697            post_text = trans(_(
698                'Your Kofa student record was created on ${a}.',
699                mapping = {'a': creation_date}),
700                portal_language)
701        #html = format_html(post_text)
702        #data.append(Paragraph(html, NOTE_STYLE))
703
704        # Create pdf stream
705        view.response.setHeader(
706            'Content-Type', 'application/pdf')
707        pdf_stream = creator.create_pdf(
708            data, None, doc_title, author=author, footer=footer_text,
709            note=post_text)
710        return pdf_stream
711
712    def getPDFCreator(self, context):
713        """Get a pdf creator suitable for `context`.
714        The default implementation always returns the default creator.
715        """
716        return getUtility(IPDFCreator)
717
718    def renderPDF(self, view, filename='slip.pdf', student=None,
719                  studentview=None,
720                  tableheader=[], tabledata=[],
721                  note=None, signatures=None, sigs_in_footer=(),
722                  show_scans=True, topMargin=1.5,
723                  omit_fields=()):
724        """Render pdf slips for various pages (also some pages
725        in the applicants module).
726        """
727        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
728        # XXX: tell what the different parameters mean
729        style = getSampleStyleSheet()
730        creator = self.getPDFCreator(student)
731        data = []
732        doc_title = view.label
733        author = '%s (%s)' % (view.request.principal.title,
734                              view.request.principal.id)
735        footer_text = view.label.split('\n')
736        if len(footer_text) > 1:
737            # We can add a department in first line, second line is used
738            footer_text = footer_text[1]
739        else:
740            # Only the first line is used for the footer
741            footer_text = footer_text[0]
742        if getattr(student, 'student_id', None) is not None:
743            footer_text = "%s - %s - " % (student.student_id, footer_text)
744
745        # Insert student data table
746        if student is not None:
747            bd_translation = trans(_('Base Data'), portal_language)
748            data.append(Paragraph(bd_translation, HEADING_STYLE))
749            data.append(render_student_data(
750                studentview, view.context, omit_fields, lang=portal_language,
751                slipname=filename))
752
753        # Insert widgets
754        if view.form_fields:
755            data.append(Paragraph(view.title, HEADING_STYLE))
756            separators = getattr(self, 'SEPARATORS_DICT', {})
757            table = creator.getWidgetsTable(
758                view.form_fields, view.context, None, lang=portal_language,
759                separators=separators)
760            data.append(table)
761
762        # Insert scanned docs
763        if show_scans:
764            data.extend(docs_as_flowables(view, portal_language))
765
766        # Insert history
767        if filename.startswith('clearance'):
768            hist_translation = trans(_('Workflow History'), portal_language)
769            data.append(Paragraph(hist_translation, HEADING_STYLE))
770            data.extend(creator.fromStringList(student.history.messages))
771
772        # Insert content tables (optionally on second page)
773        if hasattr(view, 'tabletitle'):
774            for i in range(len(view.tabletitle)):
775                if tabledata[i] and tableheader[i]:
776                    #data.append(PageBreak())
777                    #data.append(Spacer(1, 20))
778                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
779                    data.append(Spacer(1, 8))
780                    contenttable = render_table_data(tableheader[i],tabledata[i])
781                    data.append(contenttable)
782
783        # Insert signatures
784        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
785        # do not have a test for the following lines.
786        if signatures and not sigs_in_footer:
787            data.append(Spacer(1, 20))
788            # Render one signature table per signature to
789            # get date and signature in line.
790            for signature in signatures:
791                signaturetables = get_signature_tables(signature)
792                data.append(signaturetables[0])
793
794        view.response.setHeader(
795            'Content-Type', 'application/pdf')
796        try:
797            pdf_stream = creator.create_pdf(
798                data, None, doc_title, author=author, footer=footer_text,
799                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
800        except IOError:
801            view.flash('Error in image file.')
802            return view.redirect(view.url(view.context))
803        except LayoutError, err:
804            view.flash(
805                'PDF file could not be created. Reportlab error message: %s'
806                % escape(err.message),
807                type="danger")
808            return view.redirect(view.url(view.context))
809        return pdf_stream
810
811    gpa_boundaries = ((1, 'Fail'),
812                      (1.5, 'Pass'),
813                      (2.4, '3rd Class'),
814                      (3.5, '2nd Class Lower'),
815                      (4.5, '2nd Class Upper'),
816                      (5, '1st Class'))
817
818    def getClassFromCGPA(self, gpa):
819        if gpa < self.gpa_boundaries[0][0]:
820            return 0, self.gpa_boundaries[0][1]
821        if gpa < self.gpa_boundaries[1][0]:
822            return 1, self.gpa_boundaries[1][1]
823        if gpa < self.gpa_boundaries[2][0]:
824            return 2, self.gpa_boundaries[2][1]
825        if gpa < self.gpa_boundaries[3][0]:
826            return 3, self.gpa_boundaries[3][1]
827        if gpa < self.gpa_boundaries[4][0]:
828            return 4, self.gpa_boundaries[4][1]
829        if gpa <= self.gpa_boundaries[5][0]:
830            return 5, self.gpa_boundaries[5][1]
831        return 'N/A'
832
833    def getDegreeClassNumber(self, level_obj):
834        """Get degree class number (used for SessionResultsPresentation
835        reports).
836        """
837        return self.getClassFromCGPA(level_obj.cumulative_params[0])[0]
838
839    def renderPDFTranscript(self, view, filename='transcript.pdf',
840                  student=None,
841                  studentview=None,
842                  note=None, signatures=None, sigs_in_footer=(),
843                  show_scans=True, topMargin=1.5,
844                  omit_fields=(),
845                  tableheader=None):
846        """Render pdf slips for transcripts.
847        """
848        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
849        # XXX: tell what the different parameters mean
850        style = getSampleStyleSheet()
851        creator = self.getPDFCreator(student)
852        data = []
853        doc_title = view.label
854        author = '%s (%s)' % (view.request.principal.title,
855                              view.request.principal.id)
856        footer_text = view.label.split('\n')
857        if len(footer_text) > 2:
858            # We can add a department in first line
859            footer_text = footer_text[1]
860        else:
861            # Only the first line is used for the footer
862            footer_text = footer_text[0]
863        if getattr(student, 'student_id', None) is not None:
864            footer_text = "%s - %s - " % (student.student_id, footer_text)
865
866        # Insert student data table
867        if student is not None:
868            #bd_translation = trans(_('Base Data'), portal_language)
869            #data.append(Paragraph(bd_translation, HEADING_STYLE))
870            data.append(render_student_data(
871                studentview, view.context,
872                omit_fields, lang=portal_language,
873                slipname=filename))
874
875        transcript_data = view.context.getTranscriptData()
876        levels_data = transcript_data[0]
877        gpa = transcript_data[1]
878
879        contextdata = []
880        f_label = trans(_('Course of Study:'), portal_language)
881        f_label = Paragraph(f_label, ENTRY1_STYLE)
882        f_text = formatted_text(view.context.certificate.longtitle)
883        f_text = Paragraph(f_text, ENTRY1_STYLE)
884        contextdata.append([f_label,f_text])
885
886        f_label = trans(_('Faculty:'), portal_language)
887        f_label = Paragraph(f_label, ENTRY1_STYLE)
888        f_text = formatted_text(
889            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
890        f_text = Paragraph(f_text, ENTRY1_STYLE)
891        contextdata.append([f_label,f_text])
892
893        f_label = trans(_('Department:'), portal_language)
894        f_label = Paragraph(f_label, ENTRY1_STYLE)
895        f_text = formatted_text(
896            view.context.certificate.__parent__.__parent__.longtitle)
897        f_text = Paragraph(f_text, ENTRY1_STYLE)
898        contextdata.append([f_label,f_text])
899
900        f_label = trans(_('Entry Session:'), portal_language)
901        f_label = Paragraph(f_label, ENTRY1_STYLE)
902        f_text = formatted_text(
903            view.session_dict.get(view.context.entry_session))
904        f_text = Paragraph(f_text, ENTRY1_STYLE)
905        contextdata.append([f_label,f_text])
906
907        f_label = trans(_('Entry Mode:'), portal_language)
908        f_label = Paragraph(f_label, ENTRY1_STYLE)
909        f_text = formatted_text(view.studymode_dict.get(
910            view.context.entry_mode))
911        f_text = Paragraph(f_text, ENTRY1_STYLE)
912        contextdata.append([f_label,f_text])
913
914        f_label = trans(_('Cumulative GPA:'), portal_language)
915        f_label = Paragraph(f_label, ENTRY1_STYLE)
916        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
917        f_text = Paragraph(f_text, ENTRY1_STYLE)
918        contextdata.append([f_label,f_text])
919
920        contexttable = Table(contextdata,style=SLIP_STYLE)
921        data.append(contexttable)
922
923        transcripttables = render_transcript_data(
924            view, tableheader, levels_data, lang=portal_language)
925        data.extend(transcripttables)
926
927        # Insert signatures
928        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
929        # do not have a test for the following lines.
930        if signatures and not sigs_in_footer:
931            data.append(Spacer(1, 20))
932            # Render one signature table per signature to
933            # get date and signature in line.
934            for signature in signatures:
935                signaturetables = get_signature_tables(signature)
936                data.append(signaturetables[0])
937
938        view.response.setHeader(
939            'Content-Type', 'application/pdf')
940        try:
941            pdf_stream = creator.create_pdf(
942                data, None, doc_title, author=author, footer=footer_text,
943                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
944        except IOError:
945            view.flash(_('Error in image file.'))
946            return view.redirect(view.url(view.context))
947        return pdf_stream
948
949    def renderPDFCourseticketsOverview(
950            self, view, session, table_data, lecturers):
951        filename = 'coursetickets_%s_%s_%s.pdf' % (
952            view.context.code, session, view.request.principal.id)
953        session = academic_sessions_vocab.getTerm(session).title
954        creator = getUtility(IPDFCreator, name='landscape')
955        style = getSampleStyleSheet()
956        pdf_data = [Paragraph(
957            translate(_('<b>Lecturer(s): ${a}</b>',
958                      mapping = {'a':lecturers})), style["Normal"]),]
959        pdf_data += [Paragraph(
960            translate(_('<b>Credits: ${a}</b>',
961                      mapping = {'a':view.context.credits})), style["Normal"]),]
962        pdf_data.append(Spacer(1, 20))
963        pdf_data += [Table(table_data, style=CONTENT_STYLE)]
964        doc_title = translate(_('${a} (${b}) - Academic Session ${d}',
965            mapping = {'a':view.context.title,
966                       'b':view.context.code,
967                       'd':session}))
968        author = '%s (%s)' % (view.request.principal.title,
969                              view.request.principal.id)
970        view.response.setHeader(
971            'Content-Type', 'application/pdf')
972        view.response.setHeader(
973            'Content-Disposition:', 'attachment; filename="%s' % filename)
974        pdf_stream = creator.create_pdf(
975            pdf_data, None, doc_title, author, doc_title + ' -'
976            )
977        return pdf_stream
978
979    def maxCredits(self, studylevel):
980        """Return maximum credits.
981        At some universities maximum credits is not constant, it
982        depends on the student's study level. Set `maxCredits` None or 0
983        in order to deactivate the limitation.
984        """
985        return 50
986
987    def maxCreditsExceeded(self, studylevel, course):
988        max_credits = self.maxCredits(studylevel)
989        if max_credits and \
990            studylevel.total_credits + course.credits > max_credits:
991            return max_credits
992        return 0
993
994    def getBedCoordinates(self, bedticket):
995        """Return descriptive bed coordinates.
996        This method can be used to customize the `display_coordinates`
997        property method in order to  display a
998        customary description of the bed space.
999        """
1000        return bedticket.bed_coordinates
1001
1002    def clearance_disabled_message(self, student):
1003        try:
1004            session_config = grok.getSite()[
1005                'configuration'][str(student.current_session)]
1006        except KeyError:
1007            return _('Session configuration object is not available.')
1008        if not session_config.clearance_enabled:
1009            return _('Clearance is disabled for this session.')
1010        return None
1011
1012    #: A dictionary which maps widget names to headlines. The headline
1013    #: is rendered in forms and on pdf slips above the respective
1014    #: display or input widget. There are no separating headlines
1015    #: in the base package.
1016    SEPARATORS_DICT = {}
1017
1018    #: A tuple containing names of file upload viewlets which are not shown
1019    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1020    #: in the base package. This attribute makes only sense, if intermediate
1021    #: custom packages are being used, like we do for all Nigerian portals.
1022    SKIP_UPLOAD_VIEWLETS = ()
1023
1024    #: A tuple containing the names of registration states in which changing of
1025    #: passport pictures is allowed.
1026    PORTRAIT_CHANGE_STATES = (ADMITTED,)
1027
1028    #: A tuple containing all exporter names referring to students or
1029    #: subobjects thereof.
1030    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
1031            'studentstudylevels', 'coursetickets',
1032            'studentpayments', 'studentunpaidpayments',
1033            'bedtickets', 'paymentsoverview',
1034            'studylevelsoverview', 'combocard', 'bursary')
1035
1036    #: A tuple containing all exporter names needed for backing
1037    #: up student data
1038    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
1039            'studentstudylevels', 'coursetickets',
1040            'studentpayments', 'bedtickets')
1041
1042    #: A prefix used when generating new student ids. Each student id will
1043    #: start with this string. The default is 'K' for Kofa.
1044    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.