source: main/waeup.kofa/branches/uli-rm-bootstrap/src/waeup/kofa/students/utils.py @ 17209

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