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

Last change on this file since 13501 was 13501, checked in by Henrik Bettermann, 9 years ago

Do not try to determine maintenance fee if no bed is allocated.

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