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

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

Configure transfer payments and let students enter their desired
study course. Save entered text in p_item attribute.

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