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

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

Determine the class of degree which may depend on student parameters in custom packages.

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