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

Last change on this file since 13764 was 13665, checked in by Henrik Bettermann, 9 years ago

Very long matriculation numbers need to be wrapped on slips.

  • Property svn:keywords set to Id
File size: 42.1 KB
Line 
1## $Id: utils.py 13665 2016-02-08 17:13:42Z 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 zope.event import notify
28from zope.schema.interfaces import ConstraintNotSatisfied
29from zope.component import getUtility, createObject
30from zope.formlib.form import setUpEditWidgets
31from zope.i18n import translate
32from waeup.kofa.interfaces import (
33    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
34    academic_sessions_vocab)
35from waeup.kofa.interfaces import MessageFactory as _
36from waeup.kofa.students.interfaces import IStudentsUtils
37from waeup.kofa.students.workflow import ADMITTED
38from waeup.kofa.students.vocabularies import StudyLevelSource, MatNumNotInSource
39from waeup.kofa.browser.pdf import (
40    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
41    get_signature_tables, get_qrcode)
42from waeup.kofa.browser.interfaces import IPDFCreator
43from waeup.kofa.utils.helpers import to_timezone
44
45SLIP_STYLE = [
46    ('VALIGN',(0,0),(-1,-1),'TOP'),
47    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
48    ]
49
50CONTENT_STYLE = [
51    ('VALIGN',(0,0),(-1,-1),'TOP'),
52    #('FONT', (0,0), (-1,-1), 'Helvetica', 8),
53    #('TEXTCOLOR',(0,0),(-1,0),colors.white),
54    #('BACKGROUND',(0,0),(-1,0),colors.black),
55    ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
56    ('BOX', (0,0), (-1,-1), 1, colors.black),
57
58    ]
59
60FONT_SIZE = 10
61FONT_COLOR = 'black'
62
63def trans(text, lang):
64    # shortcut
65    return translate(text, 'waeup.kofa', target_language=lang)
66
67def formatted_text(text, color=FONT_COLOR, lang='en'):
68    """Turn `text`, `color` and `size` into an HTML snippet.
69
70    The snippet is suitable for use with reportlab and generating PDFs.
71    Wraps the `text` into a ``<font>`` tag with passed attributes.
72
73    Also non-strings are converted. Raw strings are expected to be
74    utf-8 encoded (usually the case for widgets etc.).
75
76    Finally, a br tag is added if widgets contain div tags
77    which are not supported by reportlab.
78
79    The returned snippet is unicode type.
80    """
81    if not isinstance(text, unicode):
82        if isinstance(text, basestring):
83            text = text.decode('utf-8')
84        else:
85            text = unicode(text)
86    if text == 'None':
87        text = ''
88    # 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 _('Payment temporarily 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        if self.samePaymentMade(student, 'balance', p_item, p_session):
508            return _('This type of payment has already been made.'), None
509        payment = createObject(u'waeup.StudentOnlinePayment')
510        timestamp = ("%d" % int(time()*10000))[1:]
511        payment.p_id = "p%s" % timestamp
512        payment.p_category = category
513        payment.p_item = p_item
514        payment.p_session = p_session
515        payment.p_level = p_level
516        payment.p_current = p_current
517        payment.amount_auth = amount
518        return None, payment
519
520    def increaseMatricInteger(self, student):
521        """Increase counter for matric numbers.
522        This counter can be a centrally stored attribute or an attribute of
523        faculties, departments or certificates. In the base package the counter
524        is as an attribute of the site configuration container.
525        """
526        grok.getSite()['configuration'].next_matric_integer += 1
527        return
528
529    def constructMatricNumber(self, student):
530        """Fetch the matric number counter which fits the student and
531        construct the new matric number of the student.
532        In the base package the counter is returned which is as an attribute
533        of the site configuration container.
534        """
535        next_integer = grok.getSite()['configuration'].next_matric_integer
536        if next_integer == 0:
537            return _('Matriculation number cannot be set.'), None
538        return None, unicode(next_integer)
539
540    def setMatricNumber(self, student):
541        """Set matriculation number of student. If the student's matric number
542        is unset a new matric number is
543        constructed according to the matriculation number construction rules
544        defined in the `constructMatricNumber` method. The new matric number is
545        set, the students catalog updated. The corresponding matric number
546        counter is increased by one.
547
548        This method is tested but not used in the base package. It can
549        be used in custom packages by adding respective views
550        and by customizing `increaseMatricInteger` and `constructMatricNumber`
551        according to the university's matriculation number construction rules.
552
553        The method can be disabled by setting the counter to zero.
554        """
555        if student.matric_number is not None:
556            return _('Matriculation number already set.'), None
557        if student.certcode is None:
558            return _('No certificate assigned.'), None
559        error, matric_number = self.constructMatricNumber(student)
560        if error:
561            return error, None
562        try:
563            student.matric_number = matric_number
564        except MatNumNotInSource:
565            return _('Matriculation number %s exists.' % matric_number), None
566        notify(grok.ObjectModifiedEvent(student))
567        self.increaseMatricInteger(student)
568        return None, matric_number
569
570    def getAccommodationDetails(self, student):
571        """Determine the accommodation data of a student.
572        """
573        d = {}
574        d['error'] = u''
575        hostels = grok.getSite()['hostels']
576        d['booking_session'] = hostels.accommodation_session
577        d['allowed_states'] = hostels.accommodation_states
578        d['startdate'] = hostels.startdate
579        d['enddate'] = hostels.enddate
580        d['expired'] = hostels.expired
581        # Determine bed type
582        studycourse = student['studycourse']
583        certificate = getattr(studycourse,'certificate',None)
584        entry_session = studycourse.entry_session
585        current_level = studycourse.current_level
586        if None in (entry_session, current_level, certificate):
587            return d
588        end_level = certificate.end_level
589        if current_level == 10:
590            bt = 'pr'
591        elif entry_session == grok.getSite()['hostels'].accommodation_session:
592            bt = 'fr'
593        elif current_level >= end_level:
594            bt = 'fi'
595        else:
596            bt = 're'
597        if student.sex == 'f':
598            sex = 'female'
599        else:
600            sex = 'male'
601        special_handling = 'regular'
602        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
603        return d
604
605    def checkAccommodationRequirements(self, student, acc_details):
606        if acc_details.get('expired', False):
607            startdate = acc_details.get('startdate')
608            enddate = acc_details.get('enddate')
609            if startdate and enddate:
610                tz = getUtility(IKofaUtils).tzinfo
611                startdate = to_timezone(
612                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
613                enddate = to_timezone(
614                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
615                return _("Outside booking period: ${a} - ${b}",
616                         mapping = {'a': startdate, 'b': enddate})
617            else:
618                return _("Outside booking period.")
619        if not acc_details.get('bt'):
620            return _("Your data are incomplete.")
621        if not student.state in acc_details['allowed_states']:
622            return _("You are in the wrong registration state.")
623        if student['studycourse'].current_session != acc_details[
624            'booking_session']:
625            return _('Your current session does not '
626                     'match accommodation session.')
627        if str(acc_details['booking_session']) in student['accommodation'].keys():
628            return _('You already booked a bed space in '
629                     'current accommodation session.')
630        return
631
632    def selectBed(self, available_beds, desired_hostel=None):
633        """Select a bed from a filtered list of available beds.
634        In the base configuration beds are sorted by the sort id
635        of the hostel and the bed number. The first bed found in
636        this sorted list is taken.
637        """
638        sorted_beds = sorted(available_beds,
639                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
640        if desired_hostel:
641            # Filter desired hostel beds
642            filtered_beds = [bed for bed in sorted_beds
643                             if bed.bed_id.startswith(desired_hostel)]
644            if not filtered_beds:
645                return
646            return filtered_beds[0]
647        return sorted_beds[0]
648
649    def _admissionText(self, student, portal_language):
650        inst_name = grok.getSite()['configuration'].name
651        text = trans(_(
652            'This is to inform you that you have been provisionally'
653            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
654            portal_language)
655        return text
656
657    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
658                                 pre_text=None, post_text=None,):
659        """Render pdf admission letter.
660        """
661        if student is None:
662            return
663        style = getSampleStyleSheet()
664        creator = self.getPDFCreator(student)
665        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
666        data = []
667        doc_title = view.label
668        author = '%s (%s)' % (view.request.principal.title,
669                              view.request.principal.id)
670        footer_text = view.label.split('\n')
671        if len(footer_text) > 1:
672            # We can add a department in first line
673            footer_text = footer_text[1]
674        else:
675            # Only the first line is used for the footer
676            footer_text = footer_text[0]
677        if getattr(student, 'student_id', None) is not None:
678            footer_text = "%s - %s - " % (student.student_id, footer_text)
679
680        # Text before student data
681        if pre_text is None:
682            html = format_html(self._admissionText(student, portal_language))
683        else:
684            html = format_html(pre_text)
685        if html:
686            data.append(Paragraph(html, NOTE_STYLE))
687            data.append(Spacer(1, 20))
688
689        # Student data
690        data.append(render_student_data(view, student,
691                    omit_fields, lang=portal_language,
692                    slipname='admission_slip.pdf'))
693
694        # Text after student data
695        data.append(Spacer(1, 20))
696        if post_text is None:
697            datelist = student.history.messages[0].split()[0].split('-')
698            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
699            post_text = trans(_(
700                'Your Kofa student record was created on ${a}.',
701                mapping = {'a': creation_date}),
702                portal_language)
703        #html = format_html(post_text)
704        #data.append(Paragraph(html, NOTE_STYLE))
705
706        # Create pdf stream
707        view.response.setHeader(
708            'Content-Type', 'application/pdf')
709        pdf_stream = creator.create_pdf(
710            data, None, doc_title, author=author, footer=footer_text,
711            note=post_text)
712        return pdf_stream
713
714    def getPDFCreator(self, context):
715        """Get a pdf creator suitable for `context`.
716        The default implementation always returns the default creator.
717        """
718        return getUtility(IPDFCreator)
719
720    def renderPDF(self, view, filename='slip.pdf', student=None,
721                  studentview=None,
722                  tableheader=[], tabledata=[],
723                  note=None, signatures=None, sigs_in_footer=(),
724                  show_scans=True, topMargin=1.5,
725                  omit_fields=()):
726        """Render pdf slips for various pages.
727        """
728        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
729        # XXX: tell what the different parameters mean
730        style = getSampleStyleSheet()
731        creator = self.getPDFCreator(student)
732        data = []
733        doc_title = view.label
734        author = '%s (%s)' % (view.request.principal.title,
735                              view.request.principal.id)
736        footer_text = view.label.split('\n')
737        if len(footer_text) > 1:
738            # We can add a department in first line, second line is used
739            footer_text = footer_text[1]
740        else:
741            # Only the first line is used for the footer
742            footer_text = footer_text[0]
743        if getattr(student, 'student_id', None) is not None:
744            footer_text = "%s - %s - " % (student.student_id, footer_text)
745
746        # Insert student data table
747        if student is not None:
748            bd_translation = trans(_('Base Data'), portal_language)
749            data.append(Paragraph(bd_translation, HEADING_STYLE))
750            data.append(render_student_data(
751                studentview, view.context, omit_fields, lang=portal_language,
752                slipname=filename))
753
754        # Insert widgets
755        if view.form_fields:
756            data.append(Paragraph(view.title, HEADING_STYLE))
757            separators = getattr(self, 'SEPARATORS_DICT', {})
758            table = creator.getWidgetsTable(
759                view.form_fields, view.context, None, lang=portal_language,
760                separators=separators)
761            data.append(table)
762
763        # Insert scanned docs
764        if show_scans:
765            data.extend(docs_as_flowables(view, portal_language))
766
767        # Insert history
768        if filename.startswith('clearance'):
769            hist_translation = trans(_('Workflow History'), portal_language)
770            data.append(Paragraph(hist_translation, HEADING_STYLE))
771            data.extend(creator.fromStringList(student.history.messages))
772
773        # Insert content tables (optionally on second page)
774        if hasattr(view, 'tabletitle'):
775            for i in range(len(view.tabletitle)):
776                if tabledata[i] and tableheader[i]:
777                    #data.append(PageBreak())
778                    #data.append(Spacer(1, 20))
779                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
780                    data.append(Spacer(1, 8))
781                    contenttable = render_table_data(tableheader[i],tabledata[i])
782                    data.append(contenttable)
783
784        # Insert signatures
785        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
786        # do not have a test for the following lines.
787        if signatures and not sigs_in_footer:
788            data.append(Spacer(1, 20))
789            # Render one signature table per signature to
790            # get date and signature in line.
791            for signature in signatures:
792                signaturetables = get_signature_tables(signature)
793                data.append(signaturetables[0])
794
795        view.response.setHeader(
796            'Content-Type', 'application/pdf')
797        try:
798            pdf_stream = creator.create_pdf(
799                data, None, doc_title, author=author, footer=footer_text,
800                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
801        except IOError:
802            view.flash('Error in image file.')
803            return view.redirect(view.url(view.context))
804        return pdf_stream
805
806    gpa_boundaries = ((1, 'Fail'),
807                      (1.5, 'Pass'),
808                      (2.4, '3rd Class'),
809                      (3.5, '2nd Class Lower'),
810                      (4.5, '2nd Class Upper'),
811                      (5, '1st Class'))
812
813    def getClassFromCGPA(self, gpa):
814        if gpa < self.gpa_boundaries[0][0]:
815            return 0, self.gpa_boundaries[0][1]
816        if gpa < self.gpa_boundaries[1][0]:
817            return 1, self.gpa_boundaries[1][1]
818        if gpa < self.gpa_boundaries[2][0]:
819            return 2, self.gpa_boundaries[2][1]
820        if gpa < self.gpa_boundaries[3][0]:
821            return 3, self.gpa_boundaries[3][1]
822        if gpa < self.gpa_boundaries[4][0]:
823            return 4, self.gpa_boundaries[4][1]
824        if gpa <= self.gpa_boundaries[5][0]:
825            return 5, self.gpa_boundaries[5][1]
826        return 'N/A'
827
828    def renderPDFTranscript(self, view, filename='transcript.pdf',
829                  student=None,
830                  studentview=None,
831                  note=None, signatures=None, sigs_in_footer=(),
832                  show_scans=True, topMargin=1.5,
833                  omit_fields=(),
834                  tableheader=None):
835        """Render pdf slips for transcripts.
836        """
837        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
838        # XXX: tell what the different parameters mean
839        style = getSampleStyleSheet()
840        creator = self.getPDFCreator(student)
841        data = []
842        doc_title = view.label
843        author = '%s (%s)' % (view.request.principal.title,
844                              view.request.principal.id)
845        footer_text = view.label.split('\n')
846        if len(footer_text) > 2:
847            # We can add a department in first line
848            footer_text = footer_text[1]
849        else:
850            # Only the first line is used for the footer
851            footer_text = footer_text[0]
852        if getattr(student, 'student_id', None) is not None:
853            footer_text = "%s - %s - " % (student.student_id, footer_text)
854
855        # Insert student data table
856        if student is not None:
857            #bd_translation = trans(_('Base Data'), portal_language)
858            #data.append(Paragraph(bd_translation, HEADING_STYLE))
859            data.append(render_student_data(
860                studentview, view.context,
861                omit_fields, lang=portal_language,
862                slipname=filename))
863
864        transcript_data = view.context.getTranscriptData()
865        levels_data = transcript_data[0]
866        gpa = transcript_data[1]
867
868        contextdata = []
869        f_label = trans(_('Course of Study:'), portal_language)
870        f_label = Paragraph(f_label, ENTRY1_STYLE)
871        f_text = formatted_text(view.context.certificate.longtitle)
872        f_text = Paragraph(f_text, ENTRY1_STYLE)
873        contextdata.append([f_label,f_text])
874
875        f_label = trans(_('Faculty:'), portal_language)
876        f_label = Paragraph(f_label, ENTRY1_STYLE)
877        f_text = formatted_text(
878            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
879        f_text = Paragraph(f_text, ENTRY1_STYLE)
880        contextdata.append([f_label,f_text])
881
882        f_label = trans(_('Department:'), portal_language)
883        f_label = Paragraph(f_label, ENTRY1_STYLE)
884        f_text = formatted_text(
885            view.context.certificate.__parent__.__parent__.longtitle)
886        f_text = Paragraph(f_text, ENTRY1_STYLE)
887        contextdata.append([f_label,f_text])
888
889        f_label = trans(_('Entry Session:'), portal_language)
890        f_label = Paragraph(f_label, ENTRY1_STYLE)
891        f_text = formatted_text(
892            view.session_dict.get(view.context.entry_session))
893        f_text = Paragraph(f_text, ENTRY1_STYLE)
894        contextdata.append([f_label,f_text])
895
896        f_label = trans(_('Entry Mode:'), portal_language)
897        f_label = Paragraph(f_label, ENTRY1_STYLE)
898        f_text = formatted_text(view.studymode_dict.get(
899            view.context.entry_mode))
900        f_text = Paragraph(f_text, ENTRY1_STYLE)
901        contextdata.append([f_label,f_text])
902
903        f_label = trans(_('Cumulative GPA:'), portal_language)
904        f_label = Paragraph(f_label, ENTRY1_STYLE)
905        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
906        f_text = Paragraph(f_text, ENTRY1_STYLE)
907        contextdata.append([f_label,f_text])
908
909        contexttable = Table(contextdata,style=SLIP_STYLE)
910        data.append(contexttable)
911
912        transcripttables = render_transcript_data(
913            view, tableheader, levels_data, lang=portal_language)
914        data.extend(transcripttables)
915
916        # Insert signatures
917        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
918        # do not have a test for the following lines.
919        if signatures and not sigs_in_footer:
920            data.append(Spacer(1, 20))
921            # Render one signature table per signature to
922            # get date and signature in line.
923            for signature in signatures:
924                signaturetables = get_signature_tables(signature)
925                data.append(signaturetables[0])
926
927        view.response.setHeader(
928            'Content-Type', 'application/pdf')
929        try:
930            pdf_stream = creator.create_pdf(
931                data, None, doc_title, author=author, footer=footer_text,
932                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
933        except IOError:
934            view.flash(_('Error in image file.'))
935            return view.redirect(view.url(view.context))
936        return pdf_stream
937
938    def maxCredits(self, studylevel):
939        """Return maximum credits.
940        At some universities maximum credits is not constant, it
941        depends on the student's study level. Set `maxCredits` None or 0
942        in order to deactivate the limitation.
943        """
944        return 50
945
946    def maxCreditsExceeded(self, studylevel, course):
947        max_credits = self.maxCredits(studylevel)
948        if max_credits and \
949            studylevel.total_credits + course.credits > max_credits:
950            return max_credits
951        return 0
952
953    def getBedCoordinates(self, bedticket):
954        """Return descriptive bed coordinates.
955        This method can be used to customize the `display_coordinates`
956        property method in order to  display a
957        customary description of the bed space.
958        """
959        return bedticket.bed_coordinates
960
961    def clearance_disabled_message(self, student):
962        try:
963            session_config = grok.getSite()[
964                'configuration'][str(student.current_session)]
965        except KeyError:
966            return _('Session configuration object is not available.')
967        if not session_config.clearance_enabled:
968            return _('Clearance is disabled for this session.')
969        return None
970
971    #: A dictionary which maps widget names to headlines. The headline
972    #: is rendered in forms and on pdf slips above the respective
973    #: display or input widget. There are no separating headlines
974    #: in the base package.
975    SEPARATORS_DICT = {}
976
977    #: A tuple containing names of file upload viewlets which are not shown
978    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
979    #: in the base package. This attribute makes only sense, if intermediate
980    #: custom packages are being used, like we do for all Nigerian portals.
981    SKIP_UPLOAD_VIEWLETS = ()
982
983    #: A tuple containing the names of registration states in which changing of
984    #: passport pictures is allowed.
985    PORTRAIT_CHANGE_STATES = (ADMITTED,)
986
987    #: A tuple containing all exporter names referring to students or
988    #: subobjects thereof.
989    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
990            'studentstudylevels', 'coursetickets',
991            'studentpayments', 'studentunpaidpayments',
992            'bedtickets', 'paymentsoverview',
993            'studylevelsoverview', 'combocard', 'bursary')
994
995    #: A tuple containing all exporter names needed for backing
996    #: up student data
997    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
998            'studentstudylevels', 'coursetickets',
999            'studentpayments', 'bedtickets')
1000
1001    #: A prefix used when generating new student ids. Each student id will
1002    #: start with this string. The default is 'K' for Kofa.
1003    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.