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

Last change on this file since 13699 was 13665, checked in by Henrik Bettermann, 9 years ago

Very long matriculation numbers need to be wrapped on slips.

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