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

Last change on this file since 13034 was 13031, checked in by Henrik Bettermann, 10 years ago

Implement session-specific course registration deadline and
late registration payment.

  • Property svn:keywords set to Id
File size: 39.6 KB
RevLine 
[7191]1## $Id: utils.py 13031 2015-06-04 14:21:23Z 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##
[7358]18"""General helper functions and utilities for the student 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
326        In the base configuration current level is always increased
327        by 100 no matter which verdict has been assigned.
328        """
[8268]329        new_level = student['studycourse'].current_level + 100
330        new_session = student['studycourse'].current_session + 1
331        return new_session, new_level
332
333    def setReturningData(self, student):
[9005]334        """ Define what happens after school fee payment
335        depending on the student's senate verdict.
[8268]336
[9005]337        This method folllows the same algorithm as getReturningData but
338        it also sets the new values.
[8268]339        """
340        new_session, new_level = self.getReturningData(student)
[9922]341        try:
342            student['studycourse'].current_level = new_level
343        except ConstraintNotSatisfied:
344            # Do not change level if level exceeds the
345            # certificate's end_level.
346            pass
[8268]347        student['studycourse'].current_session = new_session
[7615]348        verdict = student['studycourse'].current_verdict
[8820]349        student['studycourse'].current_verdict = '0'
[7615]350        student['studycourse'].previous_verdict = verdict
351        return
352
[9519]353    def _getSessionConfiguration(self, session):
354        try:
355            return grok.getSite()['configuration'][str(session)]
356        except KeyError:
357            return None
358
[11451]359    def _isPaymentDisabled(self, p_session, category, student):
360        academic_session = self._getSessionConfiguration(p_session)
[11452]361        if category == 'schoolfee' and \
362            'sf_all' in academic_session.payment_disabled:
[11451]363            return True
364        return False
365
[11641]366    def samePaymentMade(self, student, category, p_item, p_session):
367        for key in student['payments'].keys():
368            ticket = student['payments'][key]
369            if ticket.p_state == 'paid' and\
370               ticket.p_category == category and \
371               ticket.p_item == p_item and \
372               ticket.p_session == p_session:
373                  return True
374        return False
375
[9148]376    def setPaymentDetails(self, category, student,
[9151]377            previous_session, previous_level):
[8595]378        """Create Payment object and set the payment data of a student for
379        the payment category specified.
380
[7841]381        """
[8595]382        p_item = u''
383        amount = 0.0
[9148]384        if previous_session:
[9517]385            if previous_session < student['studycourse'].entry_session:
386                return _('The previous session must not fall below '
387                         'your entry session.'), None
388            if category == 'schoolfee':
389                # School fee is always paid for the following session
390                if previous_session > student['studycourse'].current_session:
391                    return _('This is not a previous session.'), None
392            else:
393                if previous_session > student['studycourse'].current_session - 1:
394                    return _('This is not a previous session.'), None
[9148]395            p_session = previous_session
396            p_level = previous_level
397            p_current = False
398        else:
399            p_session = student['studycourse'].current_session
400            p_level = student['studycourse'].current_level
401            p_current = True
[9519]402        academic_session = self._getSessionConfiguration(p_session)
403        if academic_session == None:
[8595]404            return _(u'Session configuration object is not available.'), None
[9521]405        # Determine fee.
[7150]406        if category == 'schoolfee':
[8595]407            try:
[8596]408                certificate = student['studycourse'].certificate
409                p_item = certificate.code
[8595]410            except (AttributeError, TypeError):
411                return _('Study course data are incomplete.'), None
[9148]412            if previous_session:
[9916]413                # Students can pay for previous sessions in all
414                # workflow states.  Fresh students are excluded by the
415                # update method of the PreviousPaymentAddFormPage.
[9148]416                if previous_level == 100:
417                    amount = getattr(certificate, 'school_fee_1', 0.0)
418                else:
419                    amount = getattr(certificate, 'school_fee_2', 0.0)
420            else:
421                if student.state == CLEARED:
422                    amount = getattr(certificate, 'school_fee_1', 0.0)
423                elif student.state == RETURNING:
[9916]424                    # In case of returning school fee payment the
425                    # payment session and level contain the values of
426                    # the session the student has paid for. Payment
427                    # session is always next session.
[9148]428                    p_session, p_level = self.getReturningData(student)
[9519]429                    academic_session = self._getSessionConfiguration(p_session)
430                    if academic_session == None:
[9916]431                        return _(
432                            u'Session configuration object is not available.'
433                            ), None
[9148]434                    amount = getattr(certificate, 'school_fee_2', 0.0)
435                elif student.is_postgrad and student.state == PAID:
[9916]436                    # Returning postgraduate students also pay for the
437                    # next session but their level always remains the
438                    # same.
[9148]439                    p_session += 1
[9519]440                    academic_session = self._getSessionConfiguration(p_session)
441                    if academic_session == None:
[9916]442                        return _(
443                            u'Session configuration object is not available.'
444                            ), None
[9148]445                    amount = getattr(certificate, 'school_fee_2', 0.0)
[7150]446        elif category == 'clearance':
[9178]447            try:
448                p_item = student['studycourse'].certificate.code
449            except (AttributeError, TypeError):
450                return _('Study course data are incomplete.'), None
[8595]451            amount = academic_session.clearance_fee
[7150]452        elif category == 'bed_allocation':
[8595]453            p_item = self.getAccommodationDetails(student)['bt']
454            amount = academic_session.booking_fee
[9423]455        elif category == 'hostel_maintenance':
[10681]456            amount = 0.0
[9429]457            bedticket = student['accommodation'].get(
458                str(student.current_session), None)
459            if bedticket:
460                p_item = bedticket.bed_coordinates
[10681]461                if bedticket.bed.__parent__.maint_fee > 0:
462                    amount = bedticket.bed.__parent__.maint_fee
463                else:
464                    # fallback
465                    amount = academic_session.maint_fee
[9429]466            else:
467                # Should not happen because this is already checked
468                # in the browser module, but anyway ...
469                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
470                p_item = trans(_('no bed allocated'), portal_language)
[10449]471        elif category == 'transcript':
472            amount = academic_session.transcript_fee
[13031]473        elif category == 'late_registration':
474            amount = academic_session.late_registration_fee
[8595]475        if amount in (0.0, None):
[9517]476            return _('Amount could not be determined.'), None
[11641]477        if self.samePaymentMade(student, category, p_item, p_session):
478            return _('This type of payment has already been made.'), None
[11451]479        if self._isPaymentDisabled(p_session, category, student):
480            return _('Payment temporarily disabled.'), None
[8708]481        payment = createObject(u'waeup.StudentOnlinePayment')
[8951]482        timestamp = ("%d" % int(time()*10000))[1:]
[8595]483        payment.p_id = "p%s" % timestamp
484        payment.p_category = category
485        payment.p_item = p_item
486        payment.p_session = p_session
487        payment.p_level = p_level
[9148]488        payment.p_current = p_current
[8595]489        payment.amount_auth = amount
490        return None, payment
[7019]491
[9868]492    def setBalanceDetails(self, category, student,
[9864]493            balance_session, balance_level, balance_amount):
494        """Create Payment object and set the payment data of a student for.
495
496        """
[9868]497        p_item = u'Balance'
[9864]498        p_session = balance_session
499        p_level = balance_level
500        p_current = False
501        amount = balance_amount
502        academic_session = self._getSessionConfiguration(p_session)
503        if academic_session == None:
504            return _(u'Session configuration object is not available.'), None
[9874]505        if amount in (0.0, None) or amount < 0:
506            return _('Amount must be greater than 0.'), None
[11641]507        if self.samePaymentMade(student, 'balance', p_item, p_session):
508            return _('This type of payment has already been made.'), None
[9864]509        payment = createObject(u'waeup.StudentOnlinePayment')
510        timestamp = ("%d" % int(time()*10000))[1:]
511        payment.p_id = "p%s" % timestamp
[9868]512        payment.p_category = category
[9864]513        payment.p_item = p_item
514        payment.p_session = p_session
515        payment.p_level = p_level
516        payment.p_current = p_current
517        payment.amount_auth = amount
518        return None, payment
519
[12896]520    def increaseMatricInteger(self, student):
521        """Increase counter for matric numbers.
522
523        This counter can be a centrally stored attribute or an attribute of
524        faculties, departments or certificates. In the base package the counter
525        is as an attribute of the site configuration object.
526        """
527        grok.getSite()['configuration'].next_matric_integer += 1
528        return
529
[11595]530    def constructMatricNumber(self, student):
[12896]531        """Fetch the matric number counter which fits the student and
532        construct the new matric number of the student.
533
[12902]534        In the base package the counter is returned which is as an attribute
535        of the site configuration object.
[12896]536        """
[11595]537        next_integer = grok.getSite()['configuration'].next_matric_integer
538        if next_integer == 0:
[11619]539            return _('Matriculation number cannot be set.'), None
540        return None, unicode(next_integer)
[11589]541
542    def setMatricNumber(self, student):
543        """Set matriculation number of student.
544
545        If the student's matric number is unset a new matric number is
[12896]546        constructed according to the matriculation number construction rules
547        defined in the constructMatricNumber method. The new matric number is
548        set, the students catalog updated. The corresponding matric number
549        counter is increased by one.
[11589]550
551        This method is tested but not used in the base package. It can
552        be used in custom packages by adding respective views
[12896]553        and by customizing increaseMatricInteger and constructMatricNumber
554        according to the university's matriculation number construction rules.
[11589]555
[12896]556        The method can be disabled by setting the counter to zero.
[11589]557        """
558        if student.matric_number is not None:
559            return _('Matriculation number already set.'), None
[11590]560        if student.certcode is None:
561            return _('No certificate assigned.'), None
[11619]562        error, matric_number = self.constructMatricNumber(student)
563        if error:
564            return error, None
[11589]565        try:
[11592]566            student.matric_number = matric_number
[11589]567        except MatNumNotInSource:
568            return _('Matriculation number exists.'), None
569        notify(grok.ObjectModifiedEvent(student))
[12896]570        self.increaseMatricInteger(student)
[11595]571        return None, matric_number
[11589]572
[7186]573    def getAccommodationDetails(self, student):
[9219]574        """Determine the accommodation data of a student.
[7841]575        """
[7150]576        d = {}
577        d['error'] = u''
[8685]578        hostels = grok.getSite()['hostels']
579        d['booking_session'] = hostels.accommodation_session
580        d['allowed_states'] = hostels.accommodation_states
[8688]581        d['startdate'] = hostels.startdate
582        d['enddate'] = hostels.enddate
583        d['expired'] = hostels.expired
[7150]584        # Determine bed type
585        studycourse = student['studycourse']
[7369]586        certificate = getattr(studycourse,'certificate',None)
[7150]587        entry_session = studycourse.entry_session
588        current_level = studycourse.current_level
[9187]589        if None in (entry_session, current_level, certificate):
590            return d
[7369]591        end_level = certificate.end_level
[9148]592        if current_level == 10:
593            bt = 'pr'
594        elif entry_session == grok.getSite()['hostels'].accommodation_session:
[7150]595            bt = 'fr'
596        elif current_level >= end_level:
597            bt = 'fi'
598        else:
599            bt = 're'
600        if student.sex == 'f':
601            sex = 'female'
602        else:
603            sex = 'male'
604        special_handling = 'regular'
605        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
606        return d
[7019]607
[7186]608    def selectBed(self, available_beds):
[7841]609        """Select a bed from a list of available beds.
610
611        In the base configuration we select the first bed found,
612        but can also randomize the selection if we like.
613        """
[7150]614        return available_beds[0]
615
[9981]616    def _admissionText(self, student, portal_language):
[9979]617        inst_name = grok.getSite()['configuration'].name
618        text = trans(_(
619            'This is to inform you that you have been provisionally'
620            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
621            portal_language)
622        return text
623
[10686]624    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
625                                 pre_text=None, post_text=None,):
[9191]626        """Render pdf admission letter.
627        """
628        if student is None:
629            return
630        style = getSampleStyleSheet()
[9949]631        creator = self.getPDFCreator(student)
[9979]632        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9191]633        data = []
634        doc_title = view.label
635        author = '%s (%s)' % (view.request.principal.title,
636                              view.request.principal.id)
[9944]637        footer_text = view.label.split('\n')
638        if len(footer_text) > 1:
639            # We can add a department in first line
640            footer_text = footer_text[1]
641        else:
642            # Only the first line is used for the footer
643            footer_text = footer_text[0]
[9191]644        if getattr(student, 'student_id', None) is not None:
645            footer_text = "%s - %s - " % (student.student_id, footer_text)
646
[10702]647        # Text before student data
[10686]648        if pre_text is None:
649            html = format_html(self._admissionText(student, portal_language))
650        else:
651            html = format_html(pre_text)
[11875]652        if html:
653            data.append(Paragraph(html, NOTE_STYLE))
654            data.append(Spacer(1, 20))
[9191]655
656        # Student data
[11550]657        data.append(render_student_data(view, student,
658                    omit_fields, lang=portal_language,
659                    slipname='admission_slip.pdf'))
[9191]660
[10702]661        # Text after student data
[9191]662        data.append(Spacer(1, 20))
[10686]663        if post_text is None:
664            datelist = student.history.messages[0].split()[0].split('-')
665            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
[10702]666            post_text = trans(_(
[10686]667                'Your Kofa student record was created on ${a}.',
668                mapping = {'a': creation_date}),
669                portal_language)
[10702]670        #html = format_html(post_text)
671        #data.append(Paragraph(html, NOTE_STYLE))
[9191]672
673        # Create pdf stream
674        view.response.setHeader(
675            'Content-Type', 'application/pdf')
676        pdf_stream = creator.create_pdf(
677            data, None, doc_title, author=author, footer=footer_text,
[10702]678            note=post_text)
[9191]679        return pdf_stream
680
[9949]681    def getPDFCreator(self, context):
682        """Get a pdf creator suitable for `context`.
683
684        The default implementation always returns the default creator.
685        """
686        return getUtility(IPDFCreator)
687
[8257]688    def renderPDF(self, view, filename='slip.pdf', student=None,
[9906]689                  studentview=None,
[10439]690                  tableheader=[], tabledata=[],
[9555]691                  note=None, signatures=None, sigs_in_footer=(),
[10250]692                  show_scans=True, topMargin=1.5,
693                  omit_fields=()):
[7841]694        """Render pdf slips for various pages.
695        """
[10261]696        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9916]697        # XXX: tell what the different parameters mean
[8112]698        style = getSampleStyleSheet()
[9949]699        creator = self.getPDFCreator(student)
[8112]700        data = []
701        doc_title = view.label
702        author = '%s (%s)' % (view.request.principal.title,
703                              view.request.principal.id)
[9913]704        footer_text = view.label.split('\n')
705        if len(footer_text) > 2:
706            # We can add a department in first line
707            footer_text = footer_text[1]
708        else:
[9917]709            # Only the first line is used for the footer
[9913]710            footer_text = footer_text[0]
[7714]711        if getattr(student, 'student_id', None) is not None:
[7310]712            footer_text = "%s - %s - " % (student.student_id, footer_text)
[7150]713
[7318]714        # Insert student data table
[7310]715        if student is not None:
[8112]716            bd_translation = trans(_('Base Data'), portal_language)
[9910]717            data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]718            data.append(render_student_data(
[11550]719                studentview, view.context, omit_fields, lang=portal_language,
720                slipname=filename))
[7304]721
[7318]722        # Insert widgets
[9191]723        if view.form_fields:
[9910]724            data.append(Paragraph(view.title, HEADING_STYLE))
[9191]725            separators = getattr(self, 'SEPARATORS_DICT', {})
726            table = creator.getWidgetsTable(
727                view.form_fields, view.context, None, lang=portal_language,
728                separators=separators)
729            data.append(table)
[7318]730
[8112]731        # Insert scanned docs
[9550]732        if show_scans:
733            data.extend(docs_as_flowables(view, portal_language))
[7318]734
[9452]735        # Insert history
[9910]736        if filename.startswith('clearance'):
[9452]737            hist_translation = trans(_('Workflow History'), portal_language)
[9910]738            data.append(Paragraph(hist_translation, HEADING_STYLE))
[9452]739            data.extend(creator.fromStringList(student.history.messages))
740
[10438]741        # Insert content tables (optionally on second page)
[10439]742        if hasattr(view, 'tabletitle'):
743            for i in range(len(view.tabletitle)):
744                if tabledata[i] and tableheader[i]:
745                    #data.append(PageBreak())
746                    #data.append(Spacer(1, 20))
747                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
748                    data.append(Spacer(1, 8))
749                    contenttable = render_table_data(tableheader[i],tabledata[i])
750                    data.append(contenttable)
[7318]751
[9010]752        # Insert signatures
[9965]753        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
754        # do not have a test for the following lines.
[9555]755        if signatures and not sigs_in_footer:
[9010]756            data.append(Spacer(1, 20))
[9966]757            # Render one signature table per signature to
758            # get date and signature in line.
759            for signature in signatures:
760                signaturetables = get_signature_tables(signature)
761                data.append(signaturetables[0])
[9010]762
[7150]763        view.response.setHeader(
764            'Content-Type', 'application/pdf')
[8112]765        try:
766            pdf_stream = creator.create_pdf(
[8257]767                data, None, doc_title, author=author, footer=footer_text,
[9948]768                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
[8112]769        except IOError:
770            view.flash('Error in image file.')
771            return view.redirect(view.url(view.context))
772        return pdf_stream
[7620]773
[10578]774    gpa_boundaries = ((1, 'Fail'),
775                      (1.5, 'Pass'),
776                      (2.4, '3rd Class'),
777                      (3.5, '2nd Class Lower'),
778                      (4.5, '2nd Class Upper'),
779                      (5, '1st Class'))
[10576]780
[10445]781    def getClassFromCGPA(self, gpa):
[10578]782        if gpa < self.gpa_boundaries[0][0]:
783            return 0, self.gpa_boundaries[0][1]
784        if gpa < self.gpa_boundaries[1][0]:
785            return 1, self.gpa_boundaries[1][1]
786        if gpa < self.gpa_boundaries[2][0]:
787            return 2, self.gpa_boundaries[2][1]
788        if gpa < self.gpa_boundaries[3][0]:
789            return 3, self.gpa_boundaries[3][1]
790        if gpa < self.gpa_boundaries[4][0]:
791            return 4, self.gpa_boundaries[4][1]
792        if gpa <= self.gpa_boundaries[5][0]:
793            return 5, self.gpa_boundaries[5][1]
794        return 'N/A'
[10445]795
[10250]796    def renderPDFTranscript(self, view, filename='transcript.pdf',
797                  student=None,
798                  studentview=None,
799                  note=None, signatures=None, sigs_in_footer=(),
800                  show_scans=True, topMargin=1.5,
801                  omit_fields=(),
802                  tableheader=None):
803        """Render pdf slips for transcript.
804        """
[10261]805        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[10250]806        # XXX: tell what the different parameters mean
807        style = getSampleStyleSheet()
808        creator = self.getPDFCreator(student)
809        data = []
810        doc_title = view.label
811        author = '%s (%s)' % (view.request.principal.title,
812                              view.request.principal.id)
813        footer_text = view.label.split('\n')
814        if len(footer_text) > 2:
815            # We can add a department in first line
816            footer_text = footer_text[1]
817        else:
818            # Only the first line is used for the footer
819            footer_text = footer_text[0]
820        if getattr(student, 'student_id', None) is not None:
821            footer_text = "%s - %s - " % (student.student_id, footer_text)
822
823        # Insert student data table
824        if student is not None:
825            #bd_translation = trans(_('Base Data'), portal_language)
826            #data.append(Paragraph(bd_translation, HEADING_STYLE))
[10261]827            data.append(render_student_data(
[11550]828                studentview, view.context,
829                omit_fields, lang=portal_language,
830                slipname=filename))
[10250]831
832        transcript_data = view.context.getTranscriptData()
833        levels_data = transcript_data[0]
834        gpa = transcript_data[1]
835
836        contextdata = []
[10261]837        f_label = trans(_('Course of Study:'), portal_language)
[10250]838        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10650]839        f_text = formatted_text(view.context.certificate.longtitle)
[10250]840        f_text = Paragraph(f_text, ENTRY1_STYLE)
841        contextdata.append([f_label,f_text])
842
[10261]843        f_label = trans(_('Faculty:'), portal_language)
[10250]844        f_label = Paragraph(f_label, ENTRY1_STYLE)
845        f_text = formatted_text(
[10650]846            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
[10250]847        f_text = Paragraph(f_text, ENTRY1_STYLE)
848        contextdata.append([f_label,f_text])
849
[10261]850        f_label = trans(_('Department:'), portal_language)
[10250]851        f_label = Paragraph(f_label, ENTRY1_STYLE)
852        f_text = formatted_text(
[10650]853            view.context.certificate.__parent__.__parent__.longtitle)
[10250]854        f_text = Paragraph(f_text, ENTRY1_STYLE)
855        contextdata.append([f_label,f_text])
856
[10261]857        f_label = trans(_('Entry Session:'), portal_language)
[10250]858        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]859        f_text = formatted_text(
860            view.session_dict.get(view.context.entry_session))
[10250]861        f_text = Paragraph(f_text, ENTRY1_STYLE)
862        contextdata.append([f_label,f_text])
863
[10261]864        f_label = trans(_('Entry Mode:'), portal_language)
[10250]865        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10256]866        f_text = formatted_text(view.studymode_dict.get(
867            view.context.entry_mode))
[10250]868        f_text = Paragraph(f_text, ENTRY1_STYLE)
869        contextdata.append([f_label,f_text])
870
[10262]871        f_label = trans(_('Cumulative GPA:'), portal_language)
[10250]872        f_label = Paragraph(f_label, ENTRY1_STYLE)
[10576]873        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
[10250]874        f_text = Paragraph(f_text, ENTRY1_STYLE)
875        contextdata.append([f_label,f_text])
876
877        contexttable = Table(contextdata,style=SLIP_STYLE)
878        data.append(contexttable)
879
880        transcripttables = render_transcript_data(
[10261]881            view, tableheader, levels_data, lang=portal_language)
[10250]882        data.extend(transcripttables)
883
884        # Insert signatures
885        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
886        # do not have a test for the following lines.
887        if signatures and not sigs_in_footer:
888            data.append(Spacer(1, 20))
889            # Render one signature table per signature to
890            # get date and signature in line.
891            for signature in signatures:
892                signaturetables = get_signature_tables(signature)
893                data.append(signaturetables[0])
894
895        view.response.setHeader(
896            'Content-Type', 'application/pdf')
897        try:
898            pdf_stream = creator.create_pdf(
899                data, None, doc_title, author=author, footer=footer_text,
900                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
901        except IOError:
[10261]902            view.flash(_('Error in image file.'))
[10250]903            return view.redirect(view.url(view.context))
904        return pdf_stream
905
[9830]906    def maxCredits(self, studylevel):
907        """Return maximum credits.
[9532]908
[9830]909        In some universities maximum credits is not constant, it
[12048]910        depends on the student's study level. Set maxCredits None or 0
911        in order to deactivate the limitation.
[9830]912        """
913        return 50
914
[9532]915    def maxCreditsExceeded(self, studylevel, course):
[9830]916        max_credits = self.maxCredits(studylevel)
917        if max_credits and \
918            studylevel.total_credits + course.credits > max_credits:
919            return max_credits
[9532]920        return 0
921
[9987]922    def getBedCoordinates(self, bedticket):
923        """Return bed coordinates.
924
925        This method can be used to customize the display_coordinates
926        property method.
927        """
928        return bedticket.bed_coordinates
929
[11772]930    def clearance_disabled_message(self, student):
931        try:
932            session_config = grok.getSite()[
933                'configuration'][str(student.current_session)]
934        except KeyError:
935            return _('Session configuration object is not available.')
936        if not session_config.clearance_enabled:
937            return _('Clearance is disabled for this session.')
938        return None
939
[7841]940    VERDICTS_DICT = {
[8820]941        '0': _('(not yet)'),
[7841]942        'A': 'Successful student',
943        'B': 'Student with carryover courses',
944        'C': 'Student on probation',
945        }
[8099]946
947    SEPARATORS_DICT = {
948        }
[8410]949
[10021]950    SKIP_UPLOAD_VIEWLETS = ()
951
[10803]952    PWCHANGE_STATES = (ADMITTED,)
[10706]953
[12104]954    #: A tuple containing all exporter names referring to students or
955    #: subobjects thereof.
956    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
957            'studentstudylevels', 'coursetickets',
[12971]958            'studentpayments', 'studentunpaidpayments',
959            'bedtickets', 'paymentsoverview',
[12104]960            'studylevelsoverview', 'combocard', 'bursary')
961
[12971]962    #: A tuple containing all exporter names needed for backing
963    #: up student data
964    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
965            'studentstudylevels', 'coursetickets',
966            'studentpayments', 'bedtickets')
967
[8410]968    #: A prefix used when generating new student ids. Each student id will
969    #: start with this string. The default is 'K' for ``Kofa``.
970    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.