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

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

After careful consideration: plural is more appropriate than singular.

A student section has another meaning: https://en.wikipedia.org/wiki/Student_section

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