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

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

Add StudentsUtils.increaseMatricInteger method which allows to use various
matric number counters when computing the matric number.

  • Property svn:keywords set to Id
File size: 39.2 KB
Line 
1## $Id: utils.py 12896 2015-05-01 05:45:41Z 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##
18"""General helper functions and utilities for the student section.
19"""
20import grok
21from time import time
22from reportlab.lib import colors
23from reportlab.lib.units import cm
24from reportlab.lib.pagesizes import A4
25from reportlab.lib.styles import getSampleStyleSheet
26from reportlab.platypus import Paragraph, Image, Table, Spacer
27from zope.event import notify
28from zope.schema.interfaces import ConstraintNotSatisfied
29from zope.component import getUtility, createObject
30from zope.formlib.form import setUpEditWidgets
31from zope.i18n import translate
32from waeup.kofa.interfaces import (
33    IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED,
34    academic_sessions_vocab)
35from waeup.kofa.interfaces import MessageFactory as _
36from waeup.kofa.students.interfaces import IStudentsUtils
37from waeup.kofa.students.workflow import ADMITTED
38from waeup.kofa.students.vocabularies import StudyLevelSource, MatNumNotInSource
39from waeup.kofa.browser.pdf import (
40    ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE,
41    get_signature_tables, get_qrcode)
42from waeup.kofa.browser.interfaces import IPDFCreator
43from waeup.kofa.utils.helpers import to_timezone
44
45SLIP_STYLE = [
46    ('VALIGN',(0,0),(-1,-1),'TOP'),
47    #('FONT', (0,0), (-1,-1), 'Helvetica', 11),
48    ]
49
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),
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
58    ]
59
60FONT_SIZE = 10
61FONT_COLOR = 'black'
62
63def trans(text, lang):
64    # shortcut
65    return translate(text, 'waeup.kofa', target_language=lang)
66
67def formatted_text(text, color=FONT_COLOR, lang='en'):
68    """Turn `text`, `color` and `size` into an HTML snippet.
69
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
76    Finally, a br tag is added if widgets contain div tags
77    which are not supported by reportlab.
78
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)
86    if text == 'None':
87        text = ''
88    # Mainly for boolean values we need our customized
89    # localisation of the zope domain
90    text = translate(text, 'zope', target_language=lang)
91    text = text.replace('</div>', '<br /></div>')
92    tag1 = u'<font color="%s">' % (color)
93    return tag1 + u'%s</font>' % text
94
95def generate_student_id():
96    students = grok.getSite()['students']
97    new_id = students.unique_student_id
98    return new_id
99
100def set_up_widgets(view, ignore_request=False):
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
108def render_student_data(studentview, context, omit_fields=(),
109                        lang='en', slipname=None):
110    """Render student table for an existing frame.
111    """
112    width, height = A4
113    set_up_widgets(studentview, ignore_request=True)
114    data_left = []
115    data_middle = []
116    style = getSampleStyleSheet()
117    img = getUtility(IExtFileStore).getFileByContext(
118        studentview.context, attr='passport.jpg')
119    if img is None:
120        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
121        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
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)])
125
126    f_label = trans(_('Name:'), lang)
127    f_label = Paragraph(f_label, ENTRY1_STYLE)
128    f_text = formatted_text(studentview.context.display_fullname)
129    f_text = Paragraph(f_text, ENTRY1_STYLE)
130    data_middle.append([f_label,f_text])
131
132    for widget in studentview.widgets:
133        if 'name' in widget.name:
134            continue
135        f_label = translate(
136            widget.label.strip(), 'waeup.kofa',
137            target_language=lang)
138        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
139        f_text = formatted_text(widget(), lang=lang)
140        f_text = Paragraph(f_text, ENTRY1_STYLE)
141        data_middle.append([f_label,f_text])
142
143    if getattr(studentview.context, 'certcode', None):
144        if not 'certificate' in omit_fields:
145            f_label = trans(_('Study Course:'), lang)
146            f_label = Paragraph(f_label, ENTRY1_STYLE)
147            f_text = formatted_text(
148                studentview.context['studycourse'].certificate.longtitle)
149            f_text = Paragraph(f_text, ENTRY1_STYLE)
150            data_middle.append([f_label,f_text])
151        if not 'department' in omit_fields:
152            f_label = trans(_('Department:'), lang)
153            f_label = Paragraph(f_label, ENTRY1_STYLE)
154            f_text = formatted_text(
155                studentview.context[
156                'studycourse'].certificate.__parent__.__parent__.longtitle,
157                )
158            f_text = Paragraph(f_text, ENTRY1_STYLE)
159            data_middle.append([f_label,f_text])
160        if not 'faculty' in omit_fields:
161            f_label = trans(_('Faculty:'), lang)
162            f_label = Paragraph(f_label, ENTRY1_STYLE)
163            f_text = formatted_text(
164                studentview.context[
165                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
166                )
167            f_text = Paragraph(f_text, ENTRY1_STYLE)
168            data_middle.append([f_label,f_text])
169        if not 'current_mode' in omit_fields:
170            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
171            sm = studymodes_dict[studentview.context.current_mode]
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)
176            data_middle.append([f_label,f_text])
177        if not 'entry_session' in omit_fields:
178            f_label = trans(_('Entry Session:'), lang)
179            f_label = Paragraph(f_label, ENTRY1_STYLE)
180            entry_session = studentview.context.entry_session
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)
184            data_middle.append([f_label,f_text])
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)
195            data_middle.append([f_label,f_text])
196        if not 'date_of_birth' in omit_fields:
197            f_label = trans(_('Date of Birth:'), lang)
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)
206            data_middle.append([f_label,f_text])
207
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
216    table_left = Table(data_left,style=SLIP_STYLE)
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)
219    return table
220
221def render_table_data(tableheader, tabledata, lang='en'):
222    """Render children table for an existing frame.
223    """
224    data = []
225    #data.append([Spacer(1, 12)])
226    line = []
227    style = getSampleStyleSheet()
228    for element in tableheader:
229        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
230        field = Paragraph(field, style["Normal"])
231        line.append(field)
232    data.append(line)
233    for ticket in tabledata:
234        line = []
235        for element in tableheader:
236              field = formatted_text(getattr(ticket,element[1],u' '))
237              field = Paragraph(field, style["Normal"])
238              line.append(field)
239        data.append(line)
240    table = Table(data,colWidths=[
241        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
242    return table
243
244def render_transcript_data(view, tableheader, levels_data, lang='en'):
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']
251        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
252        headerline = []
253        tabledata = []
254        subheader = '%s %s, %s %s' % (
255            trans(_('Session'), lang),
256            view.session_dict[level_obj.level_session],
257            trans(_('Level'), lang),
258            view.level_dict[level_obj.level])
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"])
263            headerline.append(field)
264        tabledata.append(headerline)
265        for ticket in tickets:
266            ticketline = []
267            for element in tableheader:
268                  field = formatted_text(getattr(ticket,element[1],u' '))
269                  field = Paragraph(field, style["Normal"])
270                  ticketline.append(field)
271            tabledata.append(ticketline)
272        table = Table(tabledata,colWidths=[
273            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
274        data.append(table)
275        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
276        data.append(Paragraph(sgpa, style["Normal"]))
277    return data
278
279def docs_as_flowables(view, lang='en'):
280    """Create reportlab flowables out of scanned docs.
281    """
282    # XXX: fix circular import problem
283    from waeup.kofa.browser.fileviewlets import FileManager
284    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
285    style = getSampleStyleSheet()
286    data = []
287
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)
293        data.append(Paragraph(sc_translation, HEADING_STYLE))
294        # Insert list of scanned documents
295        table_data = []
296        for viewlet in fm.viewlets:
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])
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
317class StudentsUtils(grok.GlobalUtility):
318    """A collection of methods subject to customization.
319    """
320    grok.implements(IStudentsUtils)
321
322    def getReturningData(self, student):
323        """ Define what happens after school fee payment
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        """
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):
334        """ Define what happens after school fee payment
335        depending on the student's senate verdict.
336
337        This method folllows the same algorithm as getReturningData but
338        it also sets the new values.
339        """
340        new_session, new_level = self.getReturningData(student)
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
347        student['studycourse'].current_session = new_session
348        verdict = student['studycourse'].current_verdict
349        student['studycourse'].current_verdict = '0'
350        student['studycourse'].previous_verdict = verdict
351        return
352
353    def _getSessionConfiguration(self, session):
354        try:
355            return grok.getSite()['configuration'][str(session)]
356        except KeyError:
357            return None
358
359    def _isPaymentDisabled(self, p_session, category, student):
360        academic_session = self._getSessionConfiguration(p_session)
361        if category == 'schoolfee' and \
362            'sf_all' in academic_session.payment_disabled:
363            return True
364        return False
365
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
376    def setPaymentDetails(self, category, student,
377            previous_session, previous_level):
378        """Create Payment object and set the payment data of a student for
379        the payment category specified.
380
381        """
382        p_item = u''
383        amount = 0.0
384        if previous_session:
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
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
402        academic_session = self._getSessionConfiguration(p_session)
403        if academic_session == None:
404            return _(u'Session configuration object is not available.'), None
405        # Determine fee.
406        if category == 'schoolfee':
407            try:
408                certificate = student['studycourse'].certificate
409                p_item = certificate.code
410            except (AttributeError, TypeError):
411                return _('Study course data are incomplete.'), None
412            if previous_session:
413                # Students can pay for previous sessions in all
414                # workflow states.  Fresh students are excluded by the
415                # update method of the PreviousPaymentAddFormPage.
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:
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.
428                    p_session, p_level = self.getReturningData(student)
429                    academic_session = self._getSessionConfiguration(p_session)
430                    if academic_session == None:
431                        return _(
432                            u'Session configuration object is not available.'
433                            ), None
434                    amount = getattr(certificate, 'school_fee_2', 0.0)
435                elif student.is_postgrad and student.state == PAID:
436                    # Returning postgraduate students also pay for the
437                    # next session but their level always remains the
438                    # same.
439                    p_session += 1
440                    academic_session = self._getSessionConfiguration(p_session)
441                    if academic_session == None:
442                        return _(
443                            u'Session configuration object is not available.'
444                            ), None
445                    amount = getattr(certificate, 'school_fee_2', 0.0)
446        elif category == 'clearance':
447            try:
448                p_item = student['studycourse'].certificate.code
449            except (AttributeError, TypeError):
450                return _('Study course data are incomplete.'), None
451            amount = academic_session.clearance_fee
452        elif category == 'bed_allocation':
453            p_item = self.getAccommodationDetails(student)['bt']
454            amount = academic_session.booking_fee
455        elif category == 'hostel_maintenance':
456            amount = 0.0
457            bedticket = student['accommodation'].get(
458                str(student.current_session), None)
459            if bedticket:
460                p_item = bedticket.bed_coordinates
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
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)
471        elif category == 'transcript':
472            amount = academic_session.transcript_fee
473        if amount in (0.0, None):
474            return _('Amount could not be determined.'), None
475        if self.samePaymentMade(student, category, p_item, p_session):
476            return _('This type of payment has already been made.'), None
477        if self._isPaymentDisabled(p_session, category, student):
478            return _('Payment temporarily disabled.'), None
479        payment = createObject(u'waeup.StudentOnlinePayment')
480        timestamp = ("%d" % int(time()*10000))[1:]
481        payment.p_id = "p%s" % timestamp
482        payment.p_category = category
483        payment.p_item = p_item
484        payment.p_session = p_session
485        payment.p_level = p_level
486        payment.p_current = p_current
487        payment.amount_auth = amount
488        return None, payment
489
490    def setBalanceDetails(self, category, student,
491            balance_session, balance_level, balance_amount):
492        """Create Payment object and set the payment data of a student for.
493
494        """
495        p_item = u'Balance'
496        p_session = balance_session
497        p_level = balance_level
498        p_current = False
499        amount = balance_amount
500        academic_session = self._getSessionConfiguration(p_session)
501        if academic_session == None:
502            return _(u'Session configuration object is not available.'), None
503        if amount in (0.0, None) or amount < 0:
504            return _('Amount must be greater than 0.'), None
505        if self.samePaymentMade(student, 'balance', p_item, p_session):
506            return _('This type of payment has already been made.'), None
507        payment = createObject(u'waeup.StudentOnlinePayment')
508        timestamp = ("%d" % int(time()*10000))[1:]
509        payment.p_id = "p%s" % timestamp
510        payment.p_category = category
511        payment.p_item = p_item
512        payment.p_session = p_session
513        payment.p_level = p_level
514        payment.p_current = p_current
515        payment.amount_auth = amount
516        return None, payment
517
518    def increaseMatricInteger(self, student):
519        """Increase counter for matric numbers.
520
521        This counter can be a centrally stored attribute or an attribute of
522        faculties, departments or certificates. In the base package the counter
523        is as an attribute of the site configuration object.
524        """
525        grok.getSite()['configuration'].next_matric_integer += 1
526        return
527
528    def constructMatricNumber(self, student):
529        """Fetch the matric number counter which fits the student and
530        construct the new matric number of the student.
531
532        In the base package the counter value is returned.
533        """
534        next_integer = grok.getSite()['configuration'].next_matric_integer
535        if next_integer == 0:
536            return _('Matriculation number cannot be set.'), None
537        return None, unicode(next_integer)
538
539    def setMatricNumber(self, student):
540        """Set matriculation number of student.
541
542        If the student's matric number is unset a new matric number is
543        constructed according to the matriculation number construction rules
544        defined in the constructMatricNumber method. The new matric number is
545        set, the students catalog updated. The corresponding matric number
546        counter is increased by one.
547
548        This method is tested but not used in the base package. It can
549        be used in custom packages by adding respective views
550        and by customizing increaseMatricInteger and constructMatricNumber
551        according to the university's matriculation number construction rules.
552
553        The method can be disabled by setting the counter to zero.
554        """
555        if student.matric_number is not None:
556            return _('Matriculation number already set.'), None
557        if student.certcode is None:
558            return _('No certificate assigned.'), None
559        error, matric_number = self.constructMatricNumber(student)
560        if error:
561            return error, None
562        try:
563            student.matric_number = matric_number
564        except MatNumNotInSource:
565            return _('Matriculation number exists.'), None
566        notify(grok.ObjectModifiedEvent(student))
567        self.increaseMatricInteger(student)
568        return None, matric_number
569
570    def getAccommodationDetails(self, student):
571        """Determine the accommodation data of a student.
572        """
573        d = {}
574        d['error'] = u''
575        hostels = grok.getSite()['hostels']
576        d['booking_session'] = hostels.accommodation_session
577        d['allowed_states'] = hostels.accommodation_states
578        d['startdate'] = hostels.startdate
579        d['enddate'] = hostels.enddate
580        d['expired'] = hostels.expired
581        # Determine bed type
582        studycourse = student['studycourse']
583        certificate = getattr(studycourse,'certificate',None)
584        entry_session = studycourse.entry_session
585        current_level = studycourse.current_level
586        if None in (entry_session, current_level, certificate):
587            return d
588        end_level = certificate.end_level
589        if current_level == 10:
590            bt = 'pr'
591        elif entry_session == grok.getSite()['hostels'].accommodation_session:
592            bt = 'fr'
593        elif current_level >= end_level:
594            bt = 'fi'
595        else:
596            bt = 're'
597        if student.sex == 'f':
598            sex = 'female'
599        else:
600            sex = 'male'
601        special_handling = 'regular'
602        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
603        return d
604
605    def selectBed(self, available_beds):
606        """Select a bed from a list of available beds.
607
608        In the base configuration we select the first bed found,
609        but can also randomize the selection if we like.
610        """
611        return available_beds[0]
612
613    def _admissionText(self, student, portal_language):
614        inst_name = grok.getSite()['configuration'].name
615        text = trans(_(
616            'This is to inform you that you have been provisionally'
617            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
618            portal_language)
619        return text
620
621    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
622                                 pre_text=None, post_text=None,):
623        """Render pdf admission letter.
624        """
625        if student is None:
626            return
627        style = getSampleStyleSheet()
628        creator = self.getPDFCreator(student)
629        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
630        data = []
631        doc_title = view.label
632        author = '%s (%s)' % (view.request.principal.title,
633                              view.request.principal.id)
634        footer_text = view.label.split('\n')
635        if len(footer_text) > 1:
636            # We can add a department in first line
637            footer_text = footer_text[1]
638        else:
639            # Only the first line is used for the footer
640            footer_text = footer_text[0]
641        if getattr(student, 'student_id', None) is not None:
642            footer_text = "%s - %s - " % (student.student_id, footer_text)
643
644        # Text before student data
645        if pre_text is None:
646            html = format_html(self._admissionText(student, portal_language))
647        else:
648            html = format_html(pre_text)
649        if html:
650            data.append(Paragraph(html, NOTE_STYLE))
651            data.append(Spacer(1, 20))
652
653        # Student data
654        data.append(render_student_data(view, student,
655                    omit_fields, lang=portal_language,
656                    slipname='admission_slip.pdf'))
657
658        # Text after student data
659        data.append(Spacer(1, 20))
660        if post_text is None:
661            datelist = student.history.messages[0].split()[0].split('-')
662            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
663            post_text = trans(_(
664                'Your Kofa student record was created on ${a}.',
665                mapping = {'a': creation_date}),
666                portal_language)
667        #html = format_html(post_text)
668        #data.append(Paragraph(html, NOTE_STYLE))
669
670        # Create pdf stream
671        view.response.setHeader(
672            'Content-Type', 'application/pdf')
673        pdf_stream = creator.create_pdf(
674            data, None, doc_title, author=author, footer=footer_text,
675            note=post_text)
676        return pdf_stream
677
678    def getPDFCreator(self, context):
679        """Get a pdf creator suitable for `context`.
680
681        The default implementation always returns the default creator.
682        """
683        return getUtility(IPDFCreator)
684
685    def renderPDF(self, view, filename='slip.pdf', student=None,
686                  studentview=None,
687                  tableheader=[], tabledata=[],
688                  note=None, signatures=None, sigs_in_footer=(),
689                  show_scans=True, topMargin=1.5,
690                  omit_fields=()):
691        """Render pdf slips for various pages.
692        """
693        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
694        # XXX: tell what the different parameters mean
695        style = getSampleStyleSheet()
696        creator = self.getPDFCreator(student)
697        data = []
698        doc_title = view.label
699        author = '%s (%s)' % (view.request.principal.title,
700                              view.request.principal.id)
701        footer_text = view.label.split('\n')
702        if len(footer_text) > 2:
703            # We can add a department in first line
704            footer_text = footer_text[1]
705        else:
706            # Only the first line is used for the footer
707            footer_text = footer_text[0]
708        if getattr(student, 'student_id', None) is not None:
709            footer_text = "%s - %s - " % (student.student_id, footer_text)
710
711        # Insert student data table
712        if student is not None:
713            bd_translation = trans(_('Base Data'), portal_language)
714            data.append(Paragraph(bd_translation, HEADING_STYLE))
715            data.append(render_student_data(
716                studentview, view.context, omit_fields, lang=portal_language,
717                slipname=filename))
718
719        # Insert widgets
720        if view.form_fields:
721            data.append(Paragraph(view.title, HEADING_STYLE))
722            separators = getattr(self, 'SEPARATORS_DICT', {})
723            table = creator.getWidgetsTable(
724                view.form_fields, view.context, None, lang=portal_language,
725                separators=separators)
726            data.append(table)
727
728        # Insert scanned docs
729        if show_scans:
730            data.extend(docs_as_flowables(view, portal_language))
731
732        # Insert history
733        if filename.startswith('clearance'):
734            hist_translation = trans(_('Workflow History'), portal_language)
735            data.append(Paragraph(hist_translation, HEADING_STYLE))
736            data.extend(creator.fromStringList(student.history.messages))
737
738        # Insert content tables (optionally on second page)
739        if hasattr(view, 'tabletitle'):
740            for i in range(len(view.tabletitle)):
741                if tabledata[i] and tableheader[i]:
742                    #data.append(PageBreak())
743                    #data.append(Spacer(1, 20))
744                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
745                    data.append(Spacer(1, 8))
746                    contenttable = render_table_data(tableheader[i],tabledata[i])
747                    data.append(contenttable)
748
749        # Insert signatures
750        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
751        # do not have a test for the following lines.
752        if signatures and not sigs_in_footer:
753            data.append(Spacer(1, 20))
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])
759
760        view.response.setHeader(
761            'Content-Type', 'application/pdf')
762        try:
763            pdf_stream = creator.create_pdf(
764                data, None, doc_title, author=author, footer=footer_text,
765                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
766        except IOError:
767            view.flash('Error in image file.')
768            return view.redirect(view.url(view.context))
769        return pdf_stream
770
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'))
777
778    def getClassFromCGPA(self, gpa):
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'
792
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        """
802        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
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))
824            data.append(render_student_data(
825                studentview, view.context,
826                omit_fields, lang=portal_language,
827                slipname=filename))
828
829        transcript_data = view.context.getTranscriptData()
830        levels_data = transcript_data[0]
831        gpa = transcript_data[1]
832
833        contextdata = []
834        f_label = trans(_('Course of Study:'), portal_language)
835        f_label = Paragraph(f_label, ENTRY1_STYLE)
836        f_text = formatted_text(view.context.certificate.longtitle)
837        f_text = Paragraph(f_text, ENTRY1_STYLE)
838        contextdata.append([f_label,f_text])
839
840        f_label = trans(_('Faculty:'), portal_language)
841        f_label = Paragraph(f_label, ENTRY1_STYLE)
842        f_text = formatted_text(
843            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
844        f_text = Paragraph(f_text, ENTRY1_STYLE)
845        contextdata.append([f_label,f_text])
846
847        f_label = trans(_('Department:'), portal_language)
848        f_label = Paragraph(f_label, ENTRY1_STYLE)
849        f_text = formatted_text(
850            view.context.certificate.__parent__.__parent__.longtitle)
851        f_text = Paragraph(f_text, ENTRY1_STYLE)
852        contextdata.append([f_label,f_text])
853
854        f_label = trans(_('Entry Session:'), portal_language)
855        f_label = Paragraph(f_label, ENTRY1_STYLE)
856        f_text = formatted_text(
857            view.session_dict.get(view.context.entry_session))
858        f_text = Paragraph(f_text, ENTRY1_STYLE)
859        contextdata.append([f_label,f_text])
860
861        f_label = trans(_('Entry Mode:'), portal_language)
862        f_label = Paragraph(f_label, ENTRY1_STYLE)
863        f_text = formatted_text(view.studymode_dict.get(
864            view.context.entry_mode))
865        f_text = Paragraph(f_text, ENTRY1_STYLE)
866        contextdata.append([f_label,f_text])
867
868        f_label = trans(_('Cumulative GPA:'), portal_language)
869        f_label = Paragraph(f_label, ENTRY1_STYLE)
870        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
871        f_text = Paragraph(f_text, ENTRY1_STYLE)
872        contextdata.append([f_label,f_text])
873
874        contexttable = Table(contextdata,style=SLIP_STYLE)
875        data.append(contexttable)
876
877        transcripttables = render_transcript_data(
878            view, tableheader, levels_data, lang=portal_language)
879        data.extend(transcripttables)
880
881        # Insert signatures
882        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
883        # do not have a test for the following lines.
884        if signatures and not sigs_in_footer:
885            data.append(Spacer(1, 20))
886            # Render one signature table per signature to
887            # get date and signature in line.
888            for signature in signatures:
889                signaturetables = get_signature_tables(signature)
890                data.append(signaturetables[0])
891
892        view.response.setHeader(
893            'Content-Type', 'application/pdf')
894        try:
895            pdf_stream = creator.create_pdf(
896                data, None, doc_title, author=author, footer=footer_text,
897                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
898        except IOError:
899            view.flash(_('Error in image file.'))
900            return view.redirect(view.url(view.context))
901        return pdf_stream
902
903    def maxCredits(self, studylevel):
904        """Return maximum credits.
905
906        In some universities maximum credits is not constant, it
907        depends on the student's study level. Set maxCredits None or 0
908        in order to deactivate the limitation.
909        """
910        return 50
911
912    def maxCreditsExceeded(self, studylevel, course):
913        max_credits = self.maxCredits(studylevel)
914        if max_credits and \
915            studylevel.total_credits + course.credits > max_credits:
916            return max_credits
917        return 0
918
919    def getBedCoordinates(self, bedticket):
920        """Return bed coordinates.
921
922        This method can be used to customize the display_coordinates
923        property method.
924        """
925        return bedticket.bed_coordinates
926
927    def clearance_disabled_message(self, student):
928        try:
929            session_config = grok.getSite()[
930                'configuration'][str(student.current_session)]
931        except KeyError:
932            return _('Session configuration object is not available.')
933        if not session_config.clearance_enabled:
934            return _('Clearance is disabled for this session.')
935        return None
936
937    VERDICTS_DICT = {
938        '0': _('(not yet)'),
939        'A': 'Successful student',
940        'B': 'Student with carryover courses',
941        'C': 'Student on probation',
942        }
943
944    SEPARATORS_DICT = {
945        }
946
947    SKIP_UPLOAD_VIEWLETS = ()
948
949    PWCHANGE_STATES = (ADMITTED,)
950
951    #: A tuple containing all exporter names referring to students or
952    #: subobjects thereof.
953    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
954            'studentstudylevels', 'coursetickets',
955            'studentpayments', 'bedtickets', 'paymentsoverview',
956            'studylevelsoverview', 'combocard', 'bursary')
957
958    #: A prefix used when generating new student ids. Each student id will
959    #: start with this string. The default is 'K' for ``Kofa``.
960    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.