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

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

Add 'No favoured hostel' option.

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