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

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

Move accommodation requirements checking into StudentsUtils? to ease customization.

  • Property svn:keywords set to Id
File size: 41.5 KB
RevLine 
[7191]1## $Id: utils.py 13247 2015-09-05 10:15:24Z 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)
456            if bedticket:
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:
464                # Should not happen because this is already checked
465                # in the browser module, but anyway ...
466                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
467                p_item = trans(_('no bed allocated'), portal_language)
[10449]468        elif category == 'transcript':
469            amount = academic_session.transcript_fee
[13031]470        elif category == 'late_registration':
471            amount = academic_session.late_registration_fee
[8595]472        if amount in (0.0, None):
[9517]473            return _('Amount could not be determined.'), None
[11641]474        if self.samePaymentMade(student, category, p_item, p_session):
475            return _('This type of payment has already been made.'), None
[11451]476        if self._isPaymentDisabled(p_session, category, student):
477            return _('Payment temporarily disabled.'), None
[8708]478        payment = createObject(u'waeup.StudentOnlinePayment')
[8951]479        timestamp = ("%d" % int(time()*10000))[1:]
[8595]480        payment.p_id = "p%s" % timestamp
481        payment.p_category = category
482        payment.p_item = p_item
483        payment.p_session = p_session
484        payment.p_level = p_level
[9148]485        payment.p_current = p_current
[8595]486        payment.amount_auth = amount
487        return None, payment
[7019]488
[9868]489    def setBalanceDetails(self, category, student,
[9864]490            balance_session, balance_level, balance_amount):
[13124]491        """Create a balance payment ticket and set the payment data
492        as selected by the student.
[9864]493        """
[9868]494        p_item = u'Balance'
[9864]495        p_session = balance_session
496        p_level = balance_level
497        p_current = False
498        amount = balance_amount
499        academic_session = self._getSessionConfiguration(p_session)
500        if academic_session == None:
501            return _(u'Session configuration object is not available.'), None
[9874]502        if amount in (0.0, None) or amount < 0:
503            return _('Amount must be greater than 0.'), None
[11641]504        if self.samePaymentMade(student, 'balance', p_item, p_session):
505            return _('This type of payment has already been made.'), None
[9864]506        payment = createObject(u'waeup.StudentOnlinePayment')
507        timestamp = ("%d" % int(time()*10000))[1:]
508        payment.p_id = "p%s" % timestamp
[9868]509        payment.p_category = category
[9864]510        payment.p_item = p_item
511        payment.p_session = p_session
512        payment.p_level = p_level
513        payment.p_current = p_current
514        payment.amount_auth = amount
515        return None, payment
516
[12896]517    def increaseMatricInteger(self, student):
518        """Increase counter for matric numbers.
519        This counter can be a centrally stored attribute or an attribute of
520        faculties, departments or certificates. In the base package the counter
[13124]521        is as an attribute of the site configuration container.
[12896]522        """
523        grok.getSite()['configuration'].next_matric_integer += 1
524        return
525
[11595]526    def constructMatricNumber(self, student):
[12896]527        """Fetch the matric number counter which fits the student and
528        construct the new matric number of the student.
[12902]529        In the base package the counter is returned which is as an attribute
[13124]530        of the site configuration container.
[12896]531        """
[11595]532        next_integer = grok.getSite()['configuration'].next_matric_integer
533        if next_integer == 0:
[11619]534            return _('Matriculation number cannot be set.'), None
535        return None, unicode(next_integer)
[11589]536
537    def setMatricNumber(self, student):
[13124]538        """Set matriculation number of student. If the student's matric number
539        is unset a new matric number is
[12896]540        constructed according to the matriculation number construction rules
[13124]541        defined in the `constructMatricNumber` method. The new matric number is
[12896]542        set, the students catalog updated. The corresponding matric number
543        counter is increased by one.
[11589]544
545        This method is tested but not used in the base package. It can
546        be used in custom packages by adding respective views
[13124]547        and by customizing `increaseMatricInteger` and `constructMatricNumber`
[12896]548        according to the university's matriculation number construction rules.
[11589]549
[12896]550        The method can be disabled by setting the counter to zero.
[11589]551        """
552        if student.matric_number is not None:
553            return _('Matriculation number already set.'), None
[11590]554        if student.certcode is None:
555            return _('No certificate assigned.'), None
[11619]556        error, matric_number = self.constructMatricNumber(student)
557        if error:
558            return error, None
[11589]559        try:
[11592]560            student.matric_number = matric_number
[11589]561        except MatNumNotInSource:
[13224]562            return _('Matriculation number %s exists.' % matric_number), None
[11589]563        notify(grok.ObjectModifiedEvent(student))
[12896]564        self.increaseMatricInteger(student)
[11595]565        return None, matric_number
[11589]566
[7186]567    def getAccommodationDetails(self, student):
[9219]568        """Determine the accommodation data of a student.
[7841]569        """
[7150]570        d = {}
571        d['error'] = u''
[8685]572        hostels = grok.getSite()['hostels']
573        d['booking_session'] = hostels.accommodation_session
574        d['allowed_states'] = hostels.accommodation_states
[8688]575        d['startdate'] = hostels.startdate
576        d['enddate'] = hostels.enddate
577        d['expired'] = hostels.expired
[7150]578        # Determine bed type
579        studycourse = student['studycourse']
[7369]580        certificate = getattr(studycourse,'certificate',None)
[7150]581        entry_session = studycourse.entry_session
582        current_level = studycourse.current_level
[9187]583        if None in (entry_session, current_level, certificate):
584            return d
[7369]585        end_level = certificate.end_level
[9148]586        if current_level == 10:
587            bt = 'pr'
588        elif entry_session == grok.getSite()['hostels'].accommodation_session:
[7150]589            bt = 'fr'
590        elif current_level >= end_level:
591            bt = 'fi'
592        else:
593            bt = 're'
594        if student.sex == 'f':
595            sex = 'female'
596        else:
597            sex = 'male'
598        special_handling = 'regular'
599        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
600        return d
[7019]601
[13247]602    def checkAccommodationRequirements(self, student, acc_details):
603        if acc_details.get('expired', False):
604            startdate = acc_details.get('startdate')
605            enddate = acc_details.get('enddate')
606            if startdate and enddate:
607                tz = getUtility(IKofaUtils).tzinfo
608                startdate = to_timezone(
609                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
610                enddate = to_timezone(
611                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
612                return _("Outside booking period: ${a} - ${b}",
613                         mapping = {'a': startdate, 'b': enddate})
614            else:
615                return _("Outside booking period.")
616        if not acc_details.get('bt'):
617            return _("Your data are incomplete.")
618        if not student.state in acc_details['allowed_states']:
619            return _("You are in the wrong registration state.")
620        if student['studycourse'].current_session != acc_details[
621            'booking_session']:
622            return _('Your current session does not '
623                     'match accommodation session.')
624        if str(acc_details['booking_session']) in student['accommodation'].keys():
625            return _('You already booked a bed space in '
626                     'current accommodation session.')
627        return
628
[7186]629    def selectBed(self, available_beds):
[7841]630        """Select a bed from a list of available beds.
631        In the base configuration we select the first bed found,
632        but can also randomize the selection if we like.
633        """
[7150]634        return available_beds[0]
635
[9981]636    def _admissionText(self, student, portal_language):
[9979]637        inst_name = grok.getSite()['configuration'].name
638        text = trans(_(
639            'This is to inform you that you have been provisionally'
640            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
641            portal_language)
642        return text
643
[10686]644    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
645                                 pre_text=None, post_text=None,):
[9191]646        """Render pdf admission letter.
647        """
648        if student is None:
649            return
650        style = getSampleStyleSheet()
[9949]651        creator = self.getPDFCreator(student)
[9979]652        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9191]653        data = []
654        doc_title = view.label
655        author = '%s (%s)' % (view.request.principal.title,
656                              view.request.principal.id)
[9944]657        footer_text = view.label.split('\n')
658        if len(footer_text) > 1:
659            # We can add a department in first line
660            footer_text = footer_text[1]
661        else:
662            # Only the first line is used for the footer
663            footer_text = footer_text[0]
[9191]664        if getattr(student, 'student_id', None) is not None:
665            footer_text = "%s - %s - " % (student.student_id, footer_text)
666
[10702]667        # Text before student data
[10686]668        if pre_text is None:
669            html = format_html(self._admissionText(student, portal_language))
670        else:
671            html = format_html(pre_text)
[11875]672        if html:
673            data.append(Paragraph(html, NOTE_STYLE))
674            data.append(Spacer(1, 20))
[9191]675
676        # Student data
[11550]677        data.append(render_student_data(view, student,
678                    omit_fields, lang=portal_language,
679                    slipname='admission_slip.pdf'))
[9191]680
[10702]681        # Text after student data
[9191]682        data.append(Spacer(1, 20))
[10686]683        if post_text is None:
684            datelist = student.history.messages[0].split()[0].split('-')
685            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
[10702]686            post_text = trans(_(
[10686]687                'Your Kofa student record was created on ${a}.',
688                mapping = {'a': creation_date}),
689                portal_language)
[10702]690        #html = format_html(post_text)
691        #data.append(Paragraph(html, NOTE_STYLE))
[9191]692
693        # Create pdf stream
694        view.response.setHeader(
695            'Content-Type', 'application/pdf')
696        pdf_stream = creator.create_pdf(
697            data, None, doc_title, author=author, footer=footer_text,
[10702]698            note=post_text)
[9191]699        return pdf_stream
700
[9949]701    def getPDFCreator(self, context):
702        """Get a pdf creator suitable for `context`.
703        The default implementation always returns the default creator.
704        """
705        return getUtility(IPDFCreator)
706
[8257]707    def renderPDF(self, view, filename='slip.pdf', student=None,
[9906]708                  studentview=None,
[10439]709                  tableheader=[], tabledata=[],
[9555]710                  note=None, signatures=None, sigs_in_footer=(),
[10250]711                  show_scans=True, topMargin=1.5,
712                  omit_fields=()):
[7841]713        """Render pdf slips for various pages.
714        """
[10261]715        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9916]716        # XXX: tell what the different parameters mean
[8112]717        style = getSampleStyleSheet()
[9949]718        creator = self.getPDFCreator(student)
[8112]719        data = []
720        doc_title = view.label
721        author = '%s (%s)' % (view.request.principal.title,
722                              view.request.principal.id)
[9913]723        footer_text = view.label.split('\n')
724        if len(footer_text) > 2:
725            # We can add a department in first line
726            footer_text = footer_text[1]
727        else:
[9917]728            # Only the first line is used for the footer
[9913]729            footer_text = footer_text[0]
[7714]730        if getattr(student, 'student_id', None) is not None:
[7310]731            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]732
[7318]733        # Insert student data table
[7310]734        if student is not None:
[8112]735            bd_translation = trans(_('Base Data'), portal_language)
[9910]736            data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]737            data.append(render_student_data(
[11550]738                studentview, view.context, omit_fields, lang=portal_language,
739                slipname=filename))
[7304]740
[7318]741        # Insert widgets
[9191]742        if view.form_fields:
[9910]743            data.append(Paragraph(view.title, HEADING_STYLE))
[9191]744            separators = getattr(self, 'SEPARATORS_DICT', {})
745            table = creator.getWidgetsTable(
746                view.form_fields, view.context, None, lang=portal_language,
747                separators=separators)
748            data.append(table)
[7318]749
[8112]750        # Insert scanned docs
[9550]751        if show_scans:
752            data.extend(docs_as_flowables(view, portal_language))
[7318]753
[9452]754        # Insert history
[9910]755        if filename.startswith('clearance'):
[9452]756            hist_translation = trans(_('Workflow History'), portal_language)
[9910]757            data.append(Paragraph(hist_translation, HEADING_STYLE))
[9452]758            data.extend(creator.fromStringList(student.history.messages))
759
[10438]760        # Insert content tables (optionally on second page)
[10439]761        if hasattr(view, 'tabletitle'):
762            for i in range(len(view.tabletitle)):
763                if tabledata[i] and tableheader[i]:
764                    #data.append(PageBreak())
765                    #data.append(Spacer(1, 20))
766                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
767                    data.append(Spacer(1, 8))
768                    contenttable = render_table_data(tableheader[i],tabledata[i])
769                    data.append(contenttable)
[7318]770
[9010]771        # Insert signatures
[9965]772        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
773        # do not have a test for the following lines.
[9555]774        if signatures and not sigs_in_footer:
[9010]775            data.append(Spacer(1, 20))
[9966]776            # Render one signature table per signature to
777            # get date and signature in line.
778            for signature in signatures:
779                signaturetables = get_signature_tables(signature)
780                data.append(signaturetables[0])
[9010]781
[7150]782        view.response.setHeader(
783            'Content-Type', 'application/pdf')
[8112]784        try:
785            pdf_stream = creator.create_pdf(
[8257]786                data, None, doc_title, author=author, footer=footer_text,
[9948]787                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
[8112]788        except IOError:
789            view.flash('Error in image file.')
790            return view.redirect(view.url(view.context))
791        return pdf_stream
[7620]792
[10578]793    gpa_boundaries = ((1, 'Fail'),
794                      (1.5, 'Pass'),
795                      (2.4, '3rd Class'),
796                      (3.5, '2nd Class Lower'),
797                      (4.5, '2nd Class Upper'),
798                      (5, '1st Class'))
[10576]799
[10445]800    def getClassFromCGPA(self, gpa):
[10578]801        if gpa < self.gpa_boundaries[0][0]:
802            return 0, self.gpa_boundaries[0][1]
803        if gpa < self.gpa_boundaries[1][0]:
804            return 1, self.gpa_boundaries[1][1]
805        if gpa < self.gpa_boundaries[2][0]:
806            return 2, self.gpa_boundaries[2][1]
807        if gpa < self.gpa_boundaries[3][0]:
808            return 3, self.gpa_boundaries[3][1]
809        if gpa < self.gpa_boundaries[4][0]:
810            return 4, self.gpa_boundaries[4][1]
811        if gpa <= self.gpa_boundaries[5][0]:
812            return 5, self.gpa_boundaries[5][1]
813        return 'N/A'
[10445]814
[10250]815    def renderPDFTranscript(self, view, filename='transcript.pdf',
816                  student=None,
817                  studentview=None,
818                  note=None, signatures=None, sigs_in_footer=(),
819                  show_scans=True, topMargin=1.5,
820                  omit_fields=(),
821                  tableheader=None):
[13124]822        """Render pdf slips for transcripts.
[10250]823        """
[10261]824        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[10250]825        # XXX: tell what the different parameters mean
826        style = getSampleStyleSheet()
827        creator = self.getPDFCreator(student)
828        data = []
829        doc_title = view.label
830        author = '%s (%s)' % (view.request.principal.title,
831                              view.request.principal.id)
832        footer_text = view.label.split('\n')
833        if len(footer_text) > 2:
834            # We can add a department in first line
835            footer_text = footer_text[1]
836        else:
837            # Only the first line is used for the footer
838            footer_text = footer_text[0]
839        if getattr(student, 'student_id', None) is not None:
840            footer_text = "%s - %s - " % (student.student_id, footer_text)
841
842        # Insert student data table
843        if student is not None:
844            #bd_translation = trans(_('Base Data'), portal_language)
845            #data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]846            data.append(render_student_data(
[11550]847                studentview, view.context,
848                omit_fields, lang=portal_language,
849                slipname=filename))
[10250]850
851        transcript_data = view.context.getTranscriptData()
852        levels_data = transcript_data[0]
853        gpa = transcript_data[1]
854
855        contextdata = []
[10261]856        f_label = trans(_('Course of Study:'), portal_language)
[10250]857        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10650]858        f_text = formatted_text(view.context.certificate.longtitle)
[10250]859        f_text = Paragraph(f_text, ENTRY1_STYLE)
860        contextdata.append([f_label,f_text])
861
[10261]862        f_label = trans(_('Faculty:'), portal_language)
[10250]863        f_label = Paragraph(f_label, ENTRY1_STYLE)
864        f_text = formatted_text(
[10650]865            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
[10250]866        f_text = Paragraph(f_text, ENTRY1_STYLE)
867        contextdata.append([f_label,f_text])
868
[10261]869        f_label = trans(_('Department:'), portal_language)
[10250]870        f_label = Paragraph(f_label, ENTRY1_STYLE)
871        f_text = formatted_text(
[10650]872            view.context.certificate.__parent__.__parent__.longtitle)
[10250]873        f_text = Paragraph(f_text, ENTRY1_STYLE)
874        contextdata.append([f_label,f_text])
875
[10261]876        f_label = trans(_('Entry Session:'), portal_language)
[10250]877        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]878        f_text = formatted_text(
879            view.session_dict.get(view.context.entry_session))
[10250]880        f_text = Paragraph(f_text, ENTRY1_STYLE)
881        contextdata.append([f_label,f_text])
882
[10261]883        f_label = trans(_('Entry Mode:'), portal_language)
[10250]884        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]885        f_text = formatted_text(view.studymode_dict.get(
886            view.context.entry_mode))
[10250]887        f_text = Paragraph(f_text, ENTRY1_STYLE)
888        contextdata.append([f_label,f_text])
889
[10262]890        f_label = trans(_('Cumulative GPA:'), portal_language)
[10250]891        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10576]892        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
[10250]893        f_text = Paragraph(f_text, ENTRY1_STYLE)
894        contextdata.append([f_label,f_text])
895
896        contexttable = Table(contextdata,style=SLIP_STYLE)
897        data.append(contexttable)
898
899        transcripttables = render_transcript_data(
[10261]900            view, tableheader, levels_data, lang=portal_language)
[10250]901        data.extend(transcripttables)
902
903        # Insert signatures
904        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
905        # do not have a test for the following lines.
906        if signatures and not sigs_in_footer:
907            data.append(Spacer(1, 20))
908            # Render one signature table per signature to
909            # get date and signature in line.
910            for signature in signatures:
911                signaturetables = get_signature_tables(signature)
912                data.append(signaturetables[0])
913
914        view.response.setHeader(
915            'Content-Type', 'application/pdf')
916        try:
917            pdf_stream = creator.create_pdf(
918                data, None, doc_title, author=author, footer=footer_text,
919                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
920        except IOError:
[10261]921            view.flash(_('Error in image file.'))
[10250]922            return view.redirect(view.url(view.context))
923        return pdf_stream
924
[9830]925    def maxCredits(self, studylevel):
926        """Return maximum credits.
[13124]927        At some universities maximum credits is not constant, it
928        depends on the student's study level. Set `maxCredits` None or 0
[12048]929        in order to deactivate the limitation.
[9830]930        """
931        return 50
932
[9532]933    def maxCreditsExceeded(self, studylevel, course):
[9830]934        max_credits = self.maxCredits(studylevel)
935        if max_credits and \
936            studylevel.total_credits + course.credits > max_credits:
937            return max_credits
[9532]938        return 0
939
[9987]940    def getBedCoordinates(self, bedticket):
[13132]941        """Return descriptive bed coordinates.
[13124]942        This method can be used to customize the `display_coordinates`
[13132]943        property method in order to  display a
944        customary description of the bed space.
[9987]945        """
946        return bedticket.bed_coordinates
947
[11772]948    def clearance_disabled_message(self, student):
949        try:
950            session_config = grok.getSite()[
951                'configuration'][str(student.current_session)]
952        except KeyError:
953            return _('Session configuration object is not available.')
954        if not session_config.clearance_enabled:
955            return _('Clearance is disabled for this session.')
956        return None
957
[13132]958    #: A dictionary which maps widget names to headlines. The headline
959    #: is rendered in forms and on pdf slips above the respective
960    #: display or input widget. There are no separating headlines
961    #: in the base package.
[13129]962    SEPARATORS_DICT = {}
[8410]963
[13132]964    #: A tuple containing names of file upload viewlets which are not shown
965    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
966    #: in the base package. This attribute makes only sense, if intermediate
967    #: custom packages are being used, like we do for all Nigerian portals.
[10021]968    SKIP_UPLOAD_VIEWLETS = ()
969
[13132]970    #: A tuple containing the names of registration states in which changing of
971    #: passport pictures is allowed.
[13129]972    PORTRAIT_CHANGE_STATES = (ADMITTED,)
[10706]973
[12104]974    #: A tuple containing all exporter names referring to students or
975    #: subobjects thereof.
976    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
977            'studentstudylevels', 'coursetickets',
[12971]978            'studentpayments', 'studentunpaidpayments',
979            'bedtickets', 'paymentsoverview',
[12104]980            'studylevelsoverview', 'combocard', 'bursary')
981
[12971]982    #: A tuple containing all exporter names needed for backing
983    #: up student data
984    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
985            'studentstudylevels', 'coursetickets',
986            'studentpayments', 'bedtickets')
987
[8410]988    #: A prefix used when generating new student ids. Each student id will
[13129]989    #: start with this string. The default is 'K' for Kofa.
[8410]990    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.