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

Last change on this file since 15611 was 15584, checked in by Henrik Bettermann, 5 years ago

Import missing escape function.

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