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

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

Extend year_range.

  • Property svn:keywords set to Id
File size: 45.4 KB
RevLine 
[7191]1## $Id: utils.py 14596 2017-02-28 14:23:09Z 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
[10578]819    gpa_boundaries = ((1, 'Fail'),
820                      (1.5, 'Pass'),
821                      (2.4, '3rd Class'),
822                      (3.5, '2nd Class Lower'),
823                      (4.5, '2nd Class Upper'),
824                      (5, '1st Class'))
[10576]825
[14461]826    def getClassFromCGPA(self, gpa, student):
827        """Determine the class of degree. In some custom packages
828        this class depends on e.g. the entry session of the student. In the
829        base package, it does not.
830        """
[10578]831        if gpa < self.gpa_boundaries[0][0]:
832            return 0, self.gpa_boundaries[0][1]
833        if gpa < self.gpa_boundaries[1][0]:
834            return 1, self.gpa_boundaries[1][1]
835        if gpa < self.gpa_boundaries[2][0]:
836            return 2, self.gpa_boundaries[2][1]
837        if gpa < self.gpa_boundaries[3][0]:
838            return 3, self.gpa_boundaries[3][1]
839        if gpa < self.gpa_boundaries[4][0]:
840            return 4, self.gpa_boundaries[4][1]
841        if gpa <= self.gpa_boundaries[5][0]:
842            return 5, self.gpa_boundaries[5][1]
843        return 'N/A'
[10445]844
[14159]845    def getDegreeClassNumber(self, level_obj):
846        """Get degree class number (used for SessionResultsPresentation
[14157]847        reports).
848        """
[14410]849        if level_obj.gpa_params[1] == 0:
850            # No credits weighted
851            return 6
[14461]852        return self.getClassFromCGPA(
853            level_obj.cumulative_params[0], level_obj.student)[0]
[14157]854
[10250]855    def renderPDFTranscript(self, view, filename='transcript.pdf',
856                  student=None,
857                  studentview=None,
858                  note=None, signatures=None, sigs_in_footer=(),
859                  show_scans=True, topMargin=1.5,
860                  omit_fields=(),
[14292]861                  tableheader=None,
862                  no_passport=False):
[14583]863        """Render pdf slip of a transcripts.
[10250]864        """
[10261]865        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[10250]866        # XXX: tell what the different parameters mean
867        style = getSampleStyleSheet()
868        creator = self.getPDFCreator(student)
869        data = []
870        doc_title = view.label
871        author = '%s (%s)' % (view.request.principal.title,
872                              view.request.principal.id)
873        footer_text = view.label.split('\n')
874        if len(footer_text) > 2:
875            # We can add a department in first line
876            footer_text = footer_text[1]
877        else:
878            # Only the first line is used for the footer
879            footer_text = footer_text[0]
880        if getattr(student, 'student_id', None) is not None:
881            footer_text = "%s - %s - " % (student.student_id, footer_text)
882
883        # Insert student data table
884        if student is not None:
885            #bd_translation = trans(_('Base Data'), portal_language)
886            #data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]887            data.append(render_student_data(
[11550]888                studentview, view.context,
889                omit_fields, lang=portal_language,
[14292]890                slipname=filename,
891                no_passport=no_passport))
[10250]892
893        transcript_data = view.context.getTranscriptData()
894        levels_data = transcript_data[0]
895
896        contextdata = []
[10261]897        f_label = trans(_('Course of Study:'), portal_language)
[10250]898        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10650]899        f_text = formatted_text(view.context.certificate.longtitle)
[10250]900        f_text = Paragraph(f_text, ENTRY1_STYLE)
901        contextdata.append([f_label,f_text])
902
[10261]903        f_label = trans(_('Faculty:'), portal_language)
[10250]904        f_label = Paragraph(f_label, ENTRY1_STYLE)
905        f_text = formatted_text(
[10650]906            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
[10250]907        f_text = Paragraph(f_text, ENTRY1_STYLE)
908        contextdata.append([f_label,f_text])
909
[10261]910        f_label = trans(_('Department:'), portal_language)
[10250]911        f_label = Paragraph(f_label, ENTRY1_STYLE)
912        f_text = formatted_text(
[10650]913            view.context.certificate.__parent__.__parent__.longtitle)
[10250]914        f_text = Paragraph(f_text, ENTRY1_STYLE)
915        contextdata.append([f_label,f_text])
916
[10261]917        f_label = trans(_('Entry Session:'), portal_language)
[10250]918        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]919        f_text = formatted_text(
920            view.session_dict.get(view.context.entry_session))
[10250]921        f_text = Paragraph(f_text, ENTRY1_STYLE)
922        contextdata.append([f_label,f_text])
923
[10261]924        f_label = trans(_('Entry Mode:'), portal_language)
[10250]925        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]926        f_text = formatted_text(view.studymode_dict.get(
927            view.context.entry_mode))
[10250]928        f_text = Paragraph(f_text, ENTRY1_STYLE)
929        contextdata.append([f_label,f_text])
930
[10262]931        f_label = trans(_('Cumulative GPA:'), portal_language)
[10250]932        f_label = Paragraph(f_label, ENTRY1_STYLE)
[14473]933        format_float = getUtility(IKofaUtils).format_float
934        cgpa = format_float(transcript_data[1], 3)
935        f_text = formatted_text('%s (%s)' % (
936            cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
[10250]937        f_text = Paragraph(f_text, ENTRY1_STYLE)
938        contextdata.append([f_label,f_text])
939
940        contexttable = Table(contextdata,style=SLIP_STYLE)
941        data.append(contexttable)
942
943        transcripttables = render_transcript_data(
[10261]944            view, tableheader, levels_data, lang=portal_language)
[10250]945        data.extend(transcripttables)
946
947        # Insert signatures
948        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
949        # do not have a test for the following lines.
950        if signatures and not sigs_in_footer:
951            data.append(Spacer(1, 20))
952            # Render one signature table per signature to
953            # get date and signature in line.
954            for signature in signatures:
955                signaturetables = get_signature_tables(signature)
956                data.append(signaturetables[0])
957
958        view.response.setHeader(
959            'Content-Type', 'application/pdf')
960        try:
961            pdf_stream = creator.create_pdf(
962                data, None, doc_title, author=author, footer=footer_text,
963                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
964        except IOError:
[10261]965            view.flash(_('Error in image file.'))
[10250]966            return view.redirect(view.url(view.context))
967        return pdf_stream
968
[13898]969    def renderPDFCourseticketsOverview(
[14314]970            self, view, session, data, lecturers):
[14583]971        """Render pdf slip of course tickets for a lecturer.
972        """
[13898]973        filename = 'coursetickets_%s_%s_%s.pdf' % (
974            view.context.code, session, view.request.principal.id)
975        session = academic_sessions_vocab.getTerm(session).title
[13899]976        creator = getUtility(IPDFCreator, name='landscape')
[13898]977        style = getSampleStyleSheet()
[14151]978        pdf_data = [Paragraph(
979            translate(_('<b>Lecturer(s): ${a}</b>',
980                      mapping = {'a':lecturers})), style["Normal"]),]
981        pdf_data += [Paragraph(
982            translate(_('<b>Credits: ${a}</b>',
983                      mapping = {'a':view.context.credits})), style["Normal"]),]
[14314]984        # Not used in base package.
985        if data[1]:
986            pdf_data += [Paragraph(
[14319]987                translate(_('<b>Total Students: ${a}</b>',
[14314]988                    mapping = {'a':data[1][0]})), style["Normal"]),]
989            pdf_data += [Paragraph(
[14319]990                translate(_('<b>Total Pass: ${a} (${b}%)</b>',
991                mapping = {'a':data[1][1],'b':data[1][2]})), style["Normal"]),]
992            pdf_data += [Paragraph(
993                translate(_('<b>Total Fail: ${a} (${b}%)</b>',
994                mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),]
[13899]995        pdf_data.append(Spacer(1, 20))
[14314]996        pdf_data += [Table(data[0], style=CONTENT_STYLE)]
[14151]997        doc_title = translate(_('${a} (${b}) - Academic Session ${d}',
998            mapping = {'a':view.context.title,
999                       'b':view.context.code,
1000                       'd':session}))
[13898]1001        author = '%s (%s)' % (view.request.principal.title,
1002                              view.request.principal.id)
1003        view.response.setHeader(
1004            'Content-Type', 'application/pdf')
1005        view.response.setHeader(
1006            'Content-Disposition:', 'attachment; filename="%s' % filename)
1007        pdf_stream = creator.create_pdf(
1008            pdf_data, None, doc_title, author, doc_title + ' -'
1009            )
1010        return pdf_stream
1011
[14584]1012    def warnCreditsOOR(self, studylevel, course=None):
1013        """Return message if credits are out of range. In the base
1014        package only maximum credits is set.
[9830]1015        """
[14582]1016        if course and studylevel.total_credits + course.credits > 50:
[14584]1017            return _('Maximum credits exceeded.')
[14582]1018        elif studylevel.total_credits > 50:
[14584]1019            return _('Maximum credits exceeded.')
[14596]1020        return
[9830]1021
[9987]1022    def getBedCoordinates(self, bedticket):
[13132]1023        """Return descriptive bed coordinates.
[13124]1024        This method can be used to customize the `display_coordinates`
[13132]1025        property method in order to  display a
1026        customary description of the bed space.
[9987]1027        """
1028        return bedticket.bed_coordinates
1029
[11772]1030    def clearance_disabled_message(self, student):
[14583]1031        """Render message if clearance is disabled.
1032        """
[11772]1033        try:
1034            session_config = grok.getSite()[
1035                'configuration'][str(student.current_session)]
1036        except KeyError:
1037            return _('Session configuration object is not available.')
1038        if not session_config.clearance_enabled:
1039            return _('Clearance is disabled for this session.')
1040        return None
1041
[13132]1042    #: A dictionary which maps widget names to headlines. The headline
1043    #: is rendered in forms and on pdf slips above the respective
1044    #: display or input widget. There are no separating headlines
1045    #: in the base package.
[13129]1046    SEPARATORS_DICT = {}
[8410]1047
[13132]1048    #: A tuple containing names of file upload viewlets which are not shown
1049    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1050    #: in the base package. This attribute makes only sense, if intermediate
1051    #: custom packages are being used, like we do for all Nigerian portals.
[10021]1052    SKIP_UPLOAD_VIEWLETS = ()
1053
[13132]1054    #: A tuple containing the names of registration states in which changing of
1055    #: passport pictures is allowed.
[13129]1056    PORTRAIT_CHANGE_STATES = (ADMITTED,)
[10706]1057
[12104]1058    #: A tuple containing all exporter names referring to students or
1059    #: subobjects thereof.
1060    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
1061            'studentstudylevels', 'coursetickets',
[12971]1062            'studentpayments', 'studentunpaidpayments',
1063            'bedtickets', 'paymentsoverview',
[12104]1064            'studylevelsoverview', 'combocard', 'bursary')
1065
[12971]1066    #: A tuple containing all exporter names needed for backing
1067    #: up student data
1068    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
1069            'studentstudylevels', 'coursetickets',
1070            'studentpayments', 'bedtickets')
1071
[8410]1072    #: A prefix used when generating new student ids. Each student id will
[13129]1073    #: start with this string. The default is 'K' for Kofa.
[8410]1074    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.