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

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

Add option to render transcripts without passport picture and QR code.

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