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

Last change on this file since 15419 was 15337, checked in by Henrik Bettermann, 6 years ago

Do not show history on Uniben clearance invitation slips.

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