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

Last change on this file since 14915 was 14915, checked in by Henrik Bettermann, 7 years ago

GPABoundaries may also depend on certcode.

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