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

Last change on this file since 14248 was 14213, checked in by Henrik Bettermann, 8 years ago

Remove restrictions for adding balance payment tickets.

  • Property svn:keywords set to Id
File size: 43.7 KB
Line 
1## $Id: utils.py 14213 2016-10-11 08:58:17Z 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 students 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
59FONT_SIZE = 10
60FONT_COLOR = 'black'
61
62def trans(text, lang):
63    # shortcut
64    return translate(text, 'waeup.kofa', target_language=lang)
65
66def formatted_text(text, color=FONT_COLOR, lang='en'):
67    """Turn `text`, `color` and `size` into an HTML snippet.
68
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
75    Finally, a br tag is added if widgets contain div tags
76    which are not supported by reportlab.
77
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)
85    if text == 'None':
86        text = ''
87    # Very long matriculation numbers need to be wrapped
88    if text.find(' ') == -1 and len(text.split('/')) > 6:
89        text = '/'.join(text.split('/')[:5]) + \
90            '/ ' + '/'.join(text.split('/')[5:])
91    # Mainly for boolean values we need our customized
92    # localisation of the zope domain
93    text = translate(text, 'zope', target_language=lang)
94    text = text.replace('</div>', '<br /></div>')
95    tag1 = u'<font color="%s">' % (color)
96    return tag1 + u'%s</font>' % text
97
98def generate_student_id():
99    students = grok.getSite()['students']
100    new_id = students.unique_student_id
101    return new_id
102
103def set_up_widgets(view, ignore_request=False):
104    view.adapters = {}
105    view.widgets = setUpEditWidgets(
106        view.form_fields, view.prefix, view.context, view.request,
107        adapters=view.adapters, for_display=True,
108        ignore_request=ignore_request
109        )
110
111def render_student_data(studentview, context, omit_fields=(),
112                        lang='en', slipname=None):
113    """Render student table for an existing frame.
114    """
115    width, height = A4
116    set_up_widgets(studentview, ignore_request=True)
117    data_left = []
118    data_middle = []
119    style = getSampleStyleSheet()
120    img = getUtility(IExtFileStore).getFileByContext(
121        studentview.context, attr='passport.jpg')
122    if img is None:
123        from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH
124        img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb')
125    doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound')
126    data_left.append([doc_img])
127    #data.append([Spacer(1, 12)])
128
129    f_label = trans(_('Name:'), lang)
130    f_label = Paragraph(f_label, ENTRY1_STYLE)
131    f_text = formatted_text(studentview.context.display_fullname)
132    f_text = Paragraph(f_text, ENTRY1_STYLE)
133    data_middle.append([f_label,f_text])
134
135    for widget in studentview.widgets:
136        if 'name' in widget.name:
137            continue
138        f_label = translate(
139            widget.label.strip(), 'waeup.kofa',
140            target_language=lang)
141        f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE)
142        f_text = formatted_text(widget(), lang=lang)
143        f_text = Paragraph(f_text, ENTRY1_STYLE)
144        data_middle.append([f_label,f_text])
145
146    if getattr(studentview.context, 'certcode', None):
147        if not 'certificate' in omit_fields:
148            f_label = trans(_('Study Course:'), lang)
149            f_label = Paragraph(f_label, ENTRY1_STYLE)
150            f_text = formatted_text(
151                studentview.context['studycourse'].certificate.longtitle)
152            f_text = Paragraph(f_text, ENTRY1_STYLE)
153            data_middle.append([f_label,f_text])
154        if not 'department' in omit_fields:
155            f_label = trans(_('Department:'), lang)
156            f_label = Paragraph(f_label, ENTRY1_STYLE)
157            f_text = formatted_text(
158                studentview.context[
159                'studycourse'].certificate.__parent__.__parent__.longtitle,
160                )
161            f_text = Paragraph(f_text, ENTRY1_STYLE)
162            data_middle.append([f_label,f_text])
163        if not 'faculty' in omit_fields:
164            f_label = trans(_('Faculty:'), lang)
165            f_label = Paragraph(f_label, ENTRY1_STYLE)
166            f_text = formatted_text(
167                studentview.context[
168                'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle,
169                )
170            f_text = Paragraph(f_text, ENTRY1_STYLE)
171            data_middle.append([f_label,f_text])
172        if not 'current_mode' in omit_fields:
173            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
174            sm = studymodes_dict[studentview.context.current_mode]
175            f_label = trans(_('Study Mode:'), lang)
176            f_label = Paragraph(f_label, ENTRY1_STYLE)
177            f_text = formatted_text(sm)
178            f_text = Paragraph(f_text, ENTRY1_STYLE)
179            data_middle.append([f_label,f_text])
180        if not 'entry_session' in omit_fields:
181            f_label = trans(_('Entry Session:'), lang)
182            f_label = Paragraph(f_label, ENTRY1_STYLE)
183            entry_session = studentview.context.entry_session
184            entry_session = academic_sessions_vocab.getTerm(entry_session).title
185            f_text = formatted_text(entry_session)
186            f_text = Paragraph(f_text, ENTRY1_STYLE)
187            data_middle.append([f_label,f_text])
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)
198            data_middle.append([f_label,f_text])
199        if not 'date_of_birth' in omit_fields:
200            f_label = trans(_('Date of Birth:'), lang)
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)
209            data_middle.append([f_label,f_text])
210
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)]]
215        table_right = Table(data_right,style=SLIP_STYLE)
216    else:
217        table_right = None
218
219    table_left = Table(data_left,style=SLIP_STYLE)
220    table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm])
221    table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE)
222    return table
223
224def render_table_data(tableheader, tabledata, lang='en'):
225    """Render children table for an existing frame.
226    """
227    data = []
228    #data.append([Spacer(1, 12)])
229    line = []
230    style = getSampleStyleSheet()
231    for element in tableheader:
232        field = '<strong>%s</strong>' % formatted_text(element[0], lang=lang)
233        field = Paragraph(field, style["Normal"])
234        line.append(field)
235    data.append(line)
236    for ticket in tabledata:
237        line = []
238        for element in tableheader:
239              field = formatted_text(getattr(ticket,element[1],u' '))
240              field = Paragraph(field, style["Normal"])
241              line.append(field)
242        data.append(line)
243    table = Table(data,colWidths=[
244        element[2]*cm for element in tableheader], style=CONTENT_STYLE)
245    return table
246
247def render_transcript_data(view, tableheader, levels_data, lang='en'):
248    """Render children table for an existing frame.
249    """
250    data = []
251    style = getSampleStyleSheet()
252    for level in levels_data:
253        level_obj = level['level']
254        tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3']
255        headerline = []
256        tabledata = []
257        subheader = '%s %s, %s %s' % (
258            trans(_('Session'), lang),
259            view.session_dict[level_obj.level_session],
260            trans(_('Level'), lang),
261            view.level_dict[level_obj.level])
262        data.append(Paragraph(subheader, HEADING_STYLE))
263        for element in tableheader:
264            field = '<strong>%s</strong>' % formatted_text(element[0])
265            field = Paragraph(field, style["Normal"])
266            headerline.append(field)
267        tabledata.append(headerline)
268        for ticket in tickets:
269            ticketline = []
270            for element in tableheader:
271                  field = formatted_text(getattr(ticket,element[1],u' '))
272                  field = Paragraph(field, style["Normal"])
273                  ticketline.append(field)
274            tabledata.append(ticketline)
275        table = Table(tabledata,colWidths=[
276            element[2]*cm for element in tableheader], style=CONTENT_STYLE)
277        data.append(table)
278        sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), level['sgpa'])
279        data.append(Paragraph(sgpa, style["Normal"]))
280    return data
281
282def docs_as_flowables(view, lang='en'):
283    """Create reportlab flowables out of scanned docs.
284    """
285    # XXX: fix circular import problem
286    from waeup.kofa.browser.fileviewlets import FileManager
287    from waeup.kofa.browser import DEFAULT_IMAGE_PATH
288    style = getSampleStyleSheet()
289    data = []
290
291    # Collect viewlets
292    fm = FileManager(view.context, view.request, view)
293    fm.update()
294    if fm.viewlets:
295        sc_translation = trans(_('Scanned Documents'), lang)
296        data.append(Paragraph(sc_translation, HEADING_STYLE))
297        # Insert list of scanned documents
298        table_data = []
299        for viewlet in fm.viewlets:
300            if viewlet.file_exists:
301                # Show viewlet only if file exists
302                f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE)
303                img_path = getattr(getUtility(IExtFileStore).getFileByContext(
304                    view.context, attr=viewlet.download_name), 'name', None)
305                #f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE)
306                if img_path is None:
307                    pass
308                elif not img_path[-4:] in ('.jpg', '.JPG'):
309                    # reportlab requires jpg images, I think.
310                    f_text = Paragraph('%s (not displayable)' % (
311                        viewlet.title,), ENTRY1_STYLE)
312                else:
313                    f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound')
314                table_data.append([f_label, f_text])
315        if table_data:
316            # safety belt; empty tables lead to problems.
317            data.append(Table(table_data, style=SLIP_STYLE))
318    return data
319
320class StudentsUtils(grok.GlobalUtility):
321    """A collection of methods subject to customization.
322    """
323    grok.implements(IStudentsUtils)
324
325    def getReturningData(self, student):
326        """ Define what happens after school fee payment
327        depending on the student's senate verdict.
328        In the base configuration current level is always increased
329        by 100 no matter which verdict has been assigned.
330        """
331        new_level = student['studycourse'].current_level + 100
332        new_session = student['studycourse'].current_session + 1
333        return new_session, new_level
334
335    def setReturningData(self, student):
336        """ Define what happens after school fee payment
337        depending on the student's senate verdict.
338        This method folllows the same algorithm as `getReturningData` but
339        it also sets the new values.
340        """
341        new_session, new_level = self.getReturningData(student)
342        try:
343            student['studycourse'].current_level = new_level
344        except ConstraintNotSatisfied:
345            # Do not change level if level exceeds the
346            # certificate's end_level.
347            pass
348        student['studycourse'].current_session = new_session
349        verdict = student['studycourse'].current_verdict
350        student['studycourse'].current_verdict = '0'
351        student['studycourse'].previous_verdict = verdict
352        return
353
354    def _getSessionConfiguration(self, session):
355        try:
356            return grok.getSite()['configuration'][str(session)]
357        except KeyError:
358            return None
359
360    def _isPaymentDisabled(self, p_session, category, student):
361        academic_session = self._getSessionConfiguration(p_session)
362        if category == 'schoolfee' and \
363            'sf_all' in academic_session.payment_disabled:
364            return True
365        return False
366
367    def samePaymentMade(self, student, category, p_item, p_session):
368        for key in student['payments'].keys():
369            ticket = student['payments'][key]
370            if ticket.p_state == 'paid' and\
371               ticket.p_category == category and \
372               ticket.p_item == p_item and \
373               ticket.p_session == p_session:
374                  return True
375        return False
376
377    def setPaymentDetails(self, category, student,
378            previous_session, previous_level):
379        """Create a payment ticket and set the payment data of a
380        student for the payment category specified.
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 is not None and bedticket.bed is not None:
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                return _(u'No bed allocated.'), None
468        elif category == 'transcript':
469            amount = academic_session.transcript_fee
470        elif category == 'transfer':
471            amount = academic_session.transfer_fee
472        elif category == 'late_registration':
473            amount = academic_session.late_registration_fee
474        if amount in (0.0, None):
475            return _('Amount could not be determined.'), None
476        if self.samePaymentMade(student, category, p_item, p_session):
477            return _('This type of payment has already been made.'), None
478        if self._isPaymentDisabled(p_session, category, student):
479            return _('This category of payments has been disabled.'), None
480        payment = createObject(u'waeup.StudentOnlinePayment')
481        timestamp = ("%d" % int(time()*10000))[1:]
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
487        payment.p_current = p_current
488        payment.amount_auth = amount
489        return None, payment
490
491    def setBalanceDetails(self, category, student,
492            balance_session, balance_level, balance_amount):
493        """Create a balance payment ticket and set the payment data
494        as selected by the student.
495        """
496        p_item = u'Balance'
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
504        if amount in (0.0, None) or amount < 0:
505            return _('Amount must be greater than 0.'), None
506        payment = createObject(u'waeup.StudentOnlinePayment')
507        timestamp = ("%d" % int(time()*10000))[1:]
508        payment.p_id = "p%s" % timestamp
509        payment.p_category = category
510        payment.p_item = p_item
511        payment.p_session = p_session
512        payment.p_level = p_level
513        payment.p_current = p_current
514        payment.amount_auth = amount
515        return None, payment
516
517    def increaseMatricInteger(self, student):
518        """Increase counter for matric numbers.
519        This counter can be a centrally stored attribute or an attribute of
520        faculties, departments or certificates. In the base package the counter
521        is as an attribute of the site configuration container.
522        """
523        grok.getSite()['configuration'].next_matric_integer += 1
524        return
525
526    def constructMatricNumber(self, student):
527        """Fetch the matric number counter which fits the student and
528        construct the new matric number of the student.
529        In the base package the counter is returned which is as an attribute
530        of the site configuration container.
531        """
532        next_integer = grok.getSite()['configuration'].next_matric_integer
533        if next_integer == 0:
534            return _('Matriculation number cannot be set.'), None
535        return None, unicode(next_integer)
536
537    def setMatricNumber(self, student):
538        """Set matriculation number of student. If the student's matric number
539        is unset a new matric number is
540        constructed according to the matriculation number construction rules
541        defined in the `constructMatricNumber` method. The new matric number is
542        set, the students catalog updated. The corresponding matric number
543        counter is increased by one.
544
545        This method is tested but not used in the base package. It can
546        be used in custom packages by adding respective views
547        and by customizing `increaseMatricInteger` and `constructMatricNumber`
548        according to the university's matriculation number construction rules.
549
550        The method can be disabled by setting the counter to zero.
551        """
552        if student.matric_number is not None:
553            return _('Matriculation number already set.'), None
554        if student.certcode is None:
555            return _('No certificate assigned.'), None
556        error, matric_number = self.constructMatricNumber(student)
557        if error:
558            return error, None
559        try:
560            student.matric_number = matric_number
561        except MatNumNotInSource:
562            return _('Matriculation number %s exists.' % matric_number), None
563        notify(grok.ObjectModifiedEvent(student))
564        self.increaseMatricInteger(student)
565        return None, matric_number
566
567    def getAccommodationDetails(self, student):
568        """Determine the accommodation data of a student.
569        """
570        d = {}
571        d['error'] = u''
572        hostels = grok.getSite()['hostels']
573        d['booking_session'] = hostels.accommodation_session
574        d['allowed_states'] = hostels.accommodation_states
575        d['startdate'] = hostels.startdate
576        d['enddate'] = hostels.enddate
577        d['expired'] = hostels.expired
578        # Determine bed type
579        studycourse = student['studycourse']
580        certificate = getattr(studycourse,'certificate',None)
581        entry_session = studycourse.entry_session
582        current_level = studycourse.current_level
583        if None in (entry_session, current_level, certificate):
584            return d
585        end_level = certificate.end_level
586        if current_level == 10:
587            bt = 'pr'
588        elif entry_session == grok.getSite()['hostels'].accommodation_session:
589            bt = 'fr'
590        elif current_level >= end_level:
591            bt = 'fi'
592        else:
593            bt = 're'
594        if student.sex == 'f':
595            sex = 'female'
596        else:
597            sex = 'male'
598        special_handling = 'regular'
599        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
600        return d
601
602    def checkAccommodationRequirements(self, student, acc_details):
603        if acc_details.get('expired', False):
604            startdate = acc_details.get('startdate')
605            enddate = acc_details.get('enddate')
606            if startdate and enddate:
607                tz = getUtility(IKofaUtils).tzinfo
608                startdate = to_timezone(
609                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
610                enddate = to_timezone(
611                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
612                return _("Outside booking period: ${a} - ${b}",
613                         mapping = {'a': startdate, 'b': enddate})
614            else:
615                return _("Outside booking period.")
616        if not acc_details.get('bt'):
617            return _("Your data are incomplete.")
618        if not student.state in acc_details['allowed_states']:
619            return _("You are in the wrong registration state.")
620        if student['studycourse'].current_session != acc_details[
621            'booking_session']:
622            return _('Your current session does not '
623                     'match accommodation session.')
624        if str(acc_details['booking_session']) in student['accommodation'].keys():
625            return _('You already booked a bed space in '
626                     'current accommodation session.')
627        return
628
629    def selectBed(self, available_beds, desired_hostel=None):
630        """Select a bed from a filtered list of available beds.
631        In the base configuration beds are sorted by the sort id
632        of the hostel and the bed number. The first bed found in
633        this sorted list is taken.
634        """
635        sorted_beds = sorted(available_beds,
636                key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number)
637        if desired_hostel:
638            # Filter desired hostel beds
639            filtered_beds = [bed for bed in sorted_beds
640                             if bed.bed_id.startswith(desired_hostel)]
641            if not filtered_beds:
642                return
643            return filtered_beds[0]
644        return sorted_beds[0]
645
646    def _admissionText(self, student, portal_language):
647        inst_name = grok.getSite()['configuration'].name
648        text = trans(_(
649            'This is to inform you that you have been provisionally'
650            ' admitted into ${a} as follows:', mapping = {'a': inst_name}),
651            portal_language)
652        return text
653
654    def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(),
655                                 pre_text=None, post_text=None,):
656        """Render pdf admission letter.
657        """
658        if student is None:
659            return
660        style = getSampleStyleSheet()
661        creator = self.getPDFCreator(student)
662        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
663        data = []
664        doc_title = view.label
665        author = '%s (%s)' % (view.request.principal.title,
666                              view.request.principal.id)
667        footer_text = view.label.split('\n')
668        if len(footer_text) > 1:
669            # We can add a department in first line
670            footer_text = footer_text[1]
671        else:
672            # Only the first line is used for the footer
673            footer_text = footer_text[0]
674        if getattr(student, 'student_id', None) is not None:
675            footer_text = "%s - %s - " % (student.student_id, footer_text)
676
677        # Text before student data
678        if pre_text is None:
679            html = format_html(self._admissionText(student, portal_language))
680        else:
681            html = format_html(pre_text)
682        if html:
683            data.append(Paragraph(html, NOTE_STYLE))
684            data.append(Spacer(1, 20))
685
686        # Student data
687        data.append(render_student_data(view, student,
688                    omit_fields, lang=portal_language,
689                    slipname='admission_slip.pdf'))
690
691        # Text after student data
692        data.append(Spacer(1, 20))
693        if post_text is None:
694            datelist = student.history.messages[0].split()[0].split('-')
695            creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0])
696            post_text = trans(_(
697                'Your Kofa student record was created on ${a}.',
698                mapping = {'a': creation_date}),
699                portal_language)
700        #html = format_html(post_text)
701        #data.append(Paragraph(html, NOTE_STYLE))
702
703        # Create pdf stream
704        view.response.setHeader(
705            'Content-Type', 'application/pdf')
706        pdf_stream = creator.create_pdf(
707            data, None, doc_title, author=author, footer=footer_text,
708            note=post_text)
709        return pdf_stream
710
711    def getPDFCreator(self, context):
712        """Get a pdf creator suitable for `context`.
713        The default implementation always returns the default creator.
714        """
715        return getUtility(IPDFCreator)
716
717    def renderPDF(self, view, filename='slip.pdf', student=None,
718                  studentview=None,
719                  tableheader=[], tabledata=[],
720                  note=None, signatures=None, sigs_in_footer=(),
721                  show_scans=True, topMargin=1.5,
722                  omit_fields=()):
723        """Render pdf slips for various pages (also some pages
724        in the applicants module).
725        """
726        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
727        # XXX: tell what the different parameters mean
728        style = getSampleStyleSheet()
729        creator = self.getPDFCreator(student)
730        data = []
731        doc_title = view.label
732        author = '%s (%s)' % (view.request.principal.title,
733                              view.request.principal.id)
734        footer_text = view.label.split('\n')
735        if len(footer_text) > 1:
736            # We can add a department in first line, second line is used
737            footer_text = footer_text[1]
738        else:
739            # Only the first line is used for the footer
740            footer_text = footer_text[0]
741        if getattr(student, 'student_id', None) is not None:
742            footer_text = "%s - %s - " % (student.student_id, footer_text)
743
744        # Insert student data table
745        if student is not None:
746            bd_translation = trans(_('Base Data'), portal_language)
747            data.append(Paragraph(bd_translation, HEADING_STYLE))
748            data.append(render_student_data(
749                studentview, view.context, omit_fields, lang=portal_language,
750                slipname=filename))
751
752        # Insert widgets
753        if view.form_fields:
754            data.append(Paragraph(view.title, HEADING_STYLE))
755            separators = getattr(self, 'SEPARATORS_DICT', {})
756            table = creator.getWidgetsTable(
757                view.form_fields, view.context, None, lang=portal_language,
758                separators=separators)
759            data.append(table)
760
761        # Insert scanned docs
762        if show_scans:
763            data.extend(docs_as_flowables(view, portal_language))
764
765        # Insert history
766        if filename.startswith('clearance'):
767            hist_translation = trans(_('Workflow History'), portal_language)
768            data.append(Paragraph(hist_translation, HEADING_STYLE))
769            data.extend(creator.fromStringList(student.history.messages))
770
771        # Insert content tables (optionally on second page)
772        if hasattr(view, 'tabletitle'):
773            for i in range(len(view.tabletitle)):
774                if tabledata[i] and tableheader[i]:
775                    #data.append(PageBreak())
776                    #data.append(Spacer(1, 20))
777                    data.append(Paragraph(view.tabletitle[i], HEADING_STYLE))
778                    data.append(Spacer(1, 8))
779                    contenttable = render_table_data(tableheader[i],tabledata[i])
780                    data.append(contenttable)
781
782        # Insert signatures
783        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
784        # do not have a test for the following lines.
785        if signatures and not sigs_in_footer:
786            data.append(Spacer(1, 20))
787            # Render one signature table per signature to
788            # get date and signature in line.
789            for signature in signatures:
790                signaturetables = get_signature_tables(signature)
791                data.append(signaturetables[0])
792
793        view.response.setHeader(
794            'Content-Type', 'application/pdf')
795        try:
796            pdf_stream = creator.create_pdf(
797                data, None, doc_title, author=author, footer=footer_text,
798                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
799        except IOError:
800            view.flash('Error in image file.')
801            return view.redirect(view.url(view.context))
802        return pdf_stream
803
804    gpa_boundaries = ((1, 'Fail'),
805                      (1.5, 'Pass'),
806                      (2.4, '3rd Class'),
807                      (3.5, '2nd Class Lower'),
808                      (4.5, '2nd Class Upper'),
809                      (5, '1st Class'))
810
811    def getClassFromCGPA(self, gpa):
812        if gpa < self.gpa_boundaries[0][0]:
813            return 0, self.gpa_boundaries[0][1]
814        if gpa < self.gpa_boundaries[1][0]:
815            return 1, self.gpa_boundaries[1][1]
816        if gpa < self.gpa_boundaries[2][0]:
817            return 2, self.gpa_boundaries[2][1]
818        if gpa < self.gpa_boundaries[3][0]:
819            return 3, self.gpa_boundaries[3][1]
820        if gpa < self.gpa_boundaries[4][0]:
821            return 4, self.gpa_boundaries[4][1]
822        if gpa <= self.gpa_boundaries[5][0]:
823            return 5, self.gpa_boundaries[5][1]
824        return 'N/A'
825
826    def getDegreeClassNumber(self, level_obj):
827        """Get degree class number (used for SessionResultsPresentation
828        reports).
829        """
830        return self.getClassFromCGPA(level_obj.cumulative_params[0])[0]
831
832    def renderPDFTranscript(self, view, filename='transcript.pdf',
833                  student=None,
834                  studentview=None,
835                  note=None, signatures=None, sigs_in_footer=(),
836                  show_scans=True, topMargin=1.5,
837                  omit_fields=(),
838                  tableheader=None):
839        """Render pdf slips for transcripts.
840        """
841        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
842        # XXX: tell what the different parameters mean
843        style = getSampleStyleSheet()
844        creator = self.getPDFCreator(student)
845        data = []
846        doc_title = view.label
847        author = '%s (%s)' % (view.request.principal.title,
848                              view.request.principal.id)
849        footer_text = view.label.split('\n')
850        if len(footer_text) > 2:
851            # We can add a department in first line
852            footer_text = footer_text[1]
853        else:
854            # Only the first line is used for the footer
855            footer_text = footer_text[0]
856        if getattr(student, 'student_id', None) is not None:
857            footer_text = "%s - %s - " % (student.student_id, footer_text)
858
859        # Insert student data table
860        if student is not None:
861            #bd_translation = trans(_('Base Data'), portal_language)
862            #data.append(Paragraph(bd_translation, HEADING_STYLE))
863            data.append(render_student_data(
864                studentview, view.context,
865                omit_fields, lang=portal_language,
866                slipname=filename))
867
868        transcript_data = view.context.getTranscriptData()
869        levels_data = transcript_data[0]
870        gpa = transcript_data[1]
871
872        contextdata = []
873        f_label = trans(_('Course of Study:'), portal_language)
874        f_label = Paragraph(f_label, ENTRY1_STYLE)
875        f_text = formatted_text(view.context.certificate.longtitle)
876        f_text = Paragraph(f_text, ENTRY1_STYLE)
877        contextdata.append([f_label,f_text])
878
879        f_label = trans(_('Faculty:'), portal_language)
880        f_label = Paragraph(f_label, ENTRY1_STYLE)
881        f_text = formatted_text(
882            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
883        f_text = Paragraph(f_text, ENTRY1_STYLE)
884        contextdata.append([f_label,f_text])
885
886        f_label = trans(_('Department:'), portal_language)
887        f_label = Paragraph(f_label, ENTRY1_STYLE)
888        f_text = formatted_text(
889            view.context.certificate.__parent__.__parent__.longtitle)
890        f_text = Paragraph(f_text, ENTRY1_STYLE)
891        contextdata.append([f_label,f_text])
892
893        f_label = trans(_('Entry Session:'), portal_language)
894        f_label = Paragraph(f_label, ENTRY1_STYLE)
895        f_text = formatted_text(
896            view.session_dict.get(view.context.entry_session))
897        f_text = Paragraph(f_text, ENTRY1_STYLE)
898        contextdata.append([f_label,f_text])
899
900        f_label = trans(_('Entry Mode:'), portal_language)
901        f_label = Paragraph(f_label, ENTRY1_STYLE)
902        f_text = formatted_text(view.studymode_dict.get(
903            view.context.entry_mode))
904        f_text = Paragraph(f_text, ENTRY1_STYLE)
905        contextdata.append([f_label,f_text])
906
907        f_label = trans(_('Cumulative GPA:'), portal_language)
908        f_label = Paragraph(f_label, ENTRY1_STYLE)
909        f_text = formatted_text('%s (%s)' % (gpa, self.getClassFromCGPA(gpa)[1]))
910        f_text = Paragraph(f_text, ENTRY1_STYLE)
911        contextdata.append([f_label,f_text])
912
913        contexttable = Table(contextdata,style=SLIP_STYLE)
914        data.append(contexttable)
915
916        transcripttables = render_transcript_data(
917            view, tableheader, levels_data, lang=portal_language)
918        data.extend(transcripttables)
919
920        # Insert signatures
921        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
922        # do not have a test for the following lines.
923        if signatures and not sigs_in_footer:
924            data.append(Spacer(1, 20))
925            # Render one signature table per signature to
926            # get date and signature in line.
927            for signature in signatures:
928                signaturetables = get_signature_tables(signature)
929                data.append(signaturetables[0])
930
931        view.response.setHeader(
932            'Content-Type', 'application/pdf')
933        try:
934            pdf_stream = creator.create_pdf(
935                data, None, doc_title, author=author, footer=footer_text,
936                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
937        except IOError:
938            view.flash(_('Error in image file.'))
939            return view.redirect(view.url(view.context))
940        return pdf_stream
941
942    def renderPDFCourseticketsOverview(
943            self, view, session, table_data, lecturers):
944        filename = 'coursetickets_%s_%s_%s.pdf' % (
945            view.context.code, session, view.request.principal.id)
946        session = academic_sessions_vocab.getTerm(session).title
947        creator = getUtility(IPDFCreator, name='landscape')
948        style = getSampleStyleSheet()
949        pdf_data = [Paragraph(
950            translate(_('<b>Lecturer(s): ${a}</b>',
951                      mapping = {'a':lecturers})), style["Normal"]),]
952        pdf_data += [Paragraph(
953            translate(_('<b>Credits: ${a}</b>',
954                      mapping = {'a':view.context.credits})), style["Normal"]),]
955        pdf_data.append(Spacer(1, 20))
956        pdf_data += [Table(table_data, style=CONTENT_STYLE)]
957        doc_title = translate(_('${a} (${b}) - Academic Session ${d}',
958            mapping = {'a':view.context.title,
959                       'b':view.context.code,
960                       'd':session}))
961        author = '%s (%s)' % (view.request.principal.title,
962                              view.request.principal.id)
963        view.response.setHeader(
964            'Content-Type', 'application/pdf')
965        view.response.setHeader(
966            'Content-Disposition:', 'attachment; filename="%s' % filename)
967        pdf_stream = creator.create_pdf(
968            pdf_data, None, doc_title, author, doc_title + ' -'
969            )
970        return pdf_stream
971
972    def maxCredits(self, studylevel):
973        """Return maximum credits.
974        At some universities maximum credits is not constant, it
975        depends on the student's study level. Set `maxCredits` None or 0
976        in order to deactivate the limitation.
977        """
978        return 50
979
980    def maxCreditsExceeded(self, studylevel, course):
981        max_credits = self.maxCredits(studylevel)
982        if max_credits and \
983            studylevel.total_credits + course.credits > max_credits:
984            return max_credits
985        return 0
986
987    def getBedCoordinates(self, bedticket):
988        """Return descriptive bed coordinates.
989        This method can be used to customize the `display_coordinates`
990        property method in order to  display a
991        customary description of the bed space.
992        """
993        return bedticket.bed_coordinates
994
995    def clearance_disabled_message(self, student):
996        try:
997            session_config = grok.getSite()[
998                'configuration'][str(student.current_session)]
999        except KeyError:
1000            return _('Session configuration object is not available.')
1001        if not session_config.clearance_enabled:
1002            return _('Clearance is disabled for this session.')
1003        return None
1004
1005    #: A dictionary which maps widget names to headlines. The headline
1006    #: is rendered in forms and on pdf slips above the respective
1007    #: display or input widget. There are no separating headlines
1008    #: in the base package.
1009    SEPARATORS_DICT = {}
1010
1011    #: A tuple containing names of file upload viewlets which are not shown
1012    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
1013    #: in the base package. This attribute makes only sense, if intermediate
1014    #: custom packages are being used, like we do for all Nigerian portals.
1015    SKIP_UPLOAD_VIEWLETS = ()
1016
1017    #: A tuple containing the names of registration states in which changing of
1018    #: passport pictures is allowed.
1019    PORTRAIT_CHANGE_STATES = (ADMITTED,)
1020
1021    #: A tuple containing all exporter names referring to students or
1022    #: subobjects thereof.
1023    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
1024            'studentstudylevels', 'coursetickets',
1025            'studentpayments', 'studentunpaidpayments',
1026            'bedtickets', 'paymentsoverview',
1027            'studylevelsoverview', 'combocard', 'bursary')
1028
1029    #: A tuple containing all exporter names needed for backing
1030    #: up student data
1031    STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses',
1032            'studentstudylevels', 'coursetickets',
1033            'studentpayments', 'bedtickets')
1034
1035    #: A prefix used when generating new student ids. Each student id will
1036    #: start with this string. The default is 'K' for Kofa.
1037    STUDENT_ID_PREFIX = u'K'
Note: See TracBrowser for help on using the repository browser.