source: main/waeup.kofa/branches/uli-stud-utils-cleanup/src/waeup/kofa/students/utils.py @ 13606

Last change on this file since 13606 was 11912, checked in by uli, 10 years ago

Remove trash.

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