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

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

Rename getDegreeClass.

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