## $Id: utils.py 9864 2013-01-11 17:19:38Z henrik $ ## ## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## """General helper functions and utilities for the student section. """ import grok from time import time from reportlab.lib import colors from reportlab.lib.units import cm from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet from reportlab.platypus import Paragraph, Image, Table, Spacer from zope.component import getUtility, createObject from zope.formlib.form import setUpEditWidgets from zope.i18n import translate from waeup.kofa.interfaces import ( IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED, academic_sessions_vocab) from waeup.kofa.interfaces import MessageFactory as _ from waeup.kofa.students.interfaces import IStudentsUtils SLIP_STYLE = [ ('VALIGN',(0,0),(-1,-1),'TOP'), #('FONT', (0,0), (-1,-1), 'Helvetica', 11), ] CONTENT_STYLE = [ ('VALIGN',(0,0),(-1,-1),'TOP'), #('FONT', (0,0), (-1,-1), 'Helvetica', 8), #('TEXTCOLOR',(0,0),(-1,0),colors.white), ('BACKGROUND',(0,0),(-1,0),colors.black), ] FONT_SIZE = 10 FONT_COLOR = 'black' def formatted_label(color=FONT_COLOR, size=FONT_SIZE): tag1 ='' % (color, size) return tag1 + '%s:' def trans(text, lang): # shortcut return translate(text, 'waeup.kofa', target_language=lang) def formatted_text(text, color=FONT_COLOR, size=FONT_SIZE): """Turn `text`, `color` and `size` into an HTML snippet. The snippet is suitable for use with reportlab and generating PDFs. Wraps the `text` into a ```` tag with passed attributes. Also non-strings are converted. Raw strings are expected to be utf-8 encoded (usually the case for widgets etc.). Finally, a br tag is added if widgets contain div tags which are not supported by reportlab. The returned snippet is unicode type. """ try: # In unit tests IKofaUtils has not been registered portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE except: portal_language = 'en' if not isinstance(text, unicode): if isinstance(text, basestring): text = text.decode('utf-8') else: text = unicode(text) if text == 'None': text = '' # Mainly for boolean values we need our customized # localisation of the zope domain text = translate(text, 'zope', target_language=portal_language) text = text.replace('', '
') tag1 = u'' % (color, size) return tag1 + u'%s' % text def generate_student_id(): students = grok.getSite()['students'] new_id = students.unique_student_id return new_id def set_up_widgets(view, ignore_request=False): view.adapters = {} view.widgets = setUpEditWidgets( view.form_fields, view.prefix, view.context, view.request, adapters=view.adapters, for_display=True, ignore_request=ignore_request ) def render_student_data(studentview): """Render student table for an existing frame. """ width, height = A4 set_up_widgets(studentview, ignore_request=True) data_left = [] data_right = [] style = getSampleStyleSheet() img = getUtility(IExtFileStore).getFileByContext( studentview.context, attr='passport.jpg') if img is None: from waeup.kofa.browser import DEFAULT_PASSPORT_IMAGE_PATH img = open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb') doc_img = Image(img.name, width=4*cm, height=4*cm, kind='bound') data_left.append([doc_img]) #data.append([Spacer(1, 12)]) portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE f_label = formatted_label(size=12) % _('Name') f_label = Paragraph(f_label, style["Normal"]) f_text = formatted_text(studentview.context.display_fullname, size=12) f_text = Paragraph(f_text, style["Normal"]) data_right.append([f_label,f_text]) for widget in studentview.widgets: if 'name' in widget.name: continue f_label = formatted_label(size=12) % translate( widget.label.strip(), 'waeup.kofa', target_language=portal_language) f_label = Paragraph(f_label, style["Normal"]) f_text = formatted_text(widget(), size=12) f_text = Paragraph(f_text, style["Normal"]) data_right.append([f_label,f_text]) if getattr(studentview.context, 'certcode', None): f_label = formatted_label(size=12) % _('Study Course') f_label = Paragraph(f_label, style["Normal"]) f_text = formatted_text( studentview.context['studycourse'].certificate.longtitle(), size=12) f_text = Paragraph(f_text, style["Normal"]) data_right.append([f_label,f_text]) f_label = formatted_label(size=12) % _('Department') f_label = Paragraph(f_label, style["Normal"]) f_text = formatted_text( studentview.context[ 'studycourse'].certificate.__parent__.__parent__.longtitle(), size=12) f_text = Paragraph(f_text, style["Normal"]) data_right.append([f_label,f_text]) f_label = formatted_label(size=12) % _('Faculty') f_label = Paragraph(f_label, style["Normal"]) f_text = formatted_text( studentview.context[ 'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle(), size=12) f_text = Paragraph(f_text, style["Normal"]) data_right.append([f_label,f_text]) f_label = formatted_label(size=12) % _('Entry Session') f_label = Paragraph(f_label, style["Normal"]) entry_session = studentview.context['studycourse'].entry_session entry_session = academic_sessions_vocab.getTerm(entry_session).title f_text = formatted_text(entry_session, size=12) f_text = Paragraph(f_text, style["Normal"]) data_right.append([f_label,f_text]) table_left = Table(data_left,style=SLIP_STYLE) table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm]) table = Table([[table_left, table_right],],style=SLIP_STYLE) return table def render_table_data(tableheader,tabledata): """Render children table for an existing frame. """ data = [] #data.append([Spacer(1, 12)]) line = [] style = getSampleStyleSheet() for element in tableheader: field = formatted_text(element[0], color='white') field = Paragraph(field, style["Normal"]) line.append(field) data.append(line) for ticket in tabledata: line = [] for element in tableheader: field = formatted_text(getattr(ticket,element[1],u' ')) field = Paragraph(field, style["Normal"]) line.append(field) data.append(line) table = Table(data,colWidths=[ element[2]*cm for element in tableheader], style=CONTENT_STYLE) return table def get_signature_table(signatures, lang='en'): """Return a reportlab table containing signature fields (with date). """ style = getSampleStyleSheet() space_width = 0.4 # width in cm of space between signatures table_width = 16.0 # supposed width of signature table in cms # width of signature cells in cm... sig_col_width = table_width - ((len(signatures) - 1) * space_width) sig_col_width = sig_col_width / len(signatures) data = [] col_widths = [] # widths of columns sig_style = [ ('VALIGN',(0,-1),(-1,-1),'TOP'), ('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12), ('BOTTOMPADDING', (0,0), (-1,0), 36), ('TOPPADDING', (0,-1), (-1,-1), 0), ] for num, elem in enumerate(signatures): # draw a line above each signature cell (not: empty cells in between) sig_style.append( ('LINEABOVE', (num*2,-1), (num*2, -1), 1, colors.black)) row = [] for signature in signatures: row.append(trans(_('Date:'), lang)) row.append('') if len(signatures) > 1: col_widths.extend([sig_col_width*cm, space_width*cm]) else: col_widths.extend([sig_col_width/2*cm, sig_col_width/2*cm]) row.append('') # empty spaceholder on right data.append(row[:-1]) data.extend(([''],)*3) # insert 3 empty rows... row = [] for signature in signatures: row.append(Paragraph(trans(signature, lang), style["Normal"])) row.append('') data.append(row[:-1]) table = Table(data, style=sig_style, repeatRows=len(data), colWidths=col_widths) return table def docs_as_flowables(view, lang='en'): """Create reportlab flowables out of scanned docs. """ # XXX: fix circular import problem from waeup.kofa.students.viewlets import FileManager from waeup.kofa.browser import DEFAULT_IMAGE_PATH from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE style = getSampleStyleSheet() data = [] # Collect viewlets fm = FileManager(view.context, view.request, view) fm.update() if fm.viewlets: sc_translation = trans(_('Scanned Documents'), lang) data.append(Paragraph(sc_translation, style["Heading3"])) # Insert list of scanned documents table_data = [] for viewlet in fm.viewlets: f_label = Paragraph(trans(viewlet.label, lang), ENTRY1_STYLE) img_path = getattr(getUtility(IExtFileStore).getFileByContext( view.context, attr=viewlet.download_name), 'name', None) f_text = Paragraph(trans(_('(not provided)'),lang), ENTRY1_STYLE) if img_path is None: pass elif not img_path[-4:] in ('.jpg', '.JPG'): # reportlab requires jpg images, I think. f_text = Paragraph('%s (not displayable)' % ( viewlet.title,), ENTRY1_STYLE) else: f_text = Image(img_path, width=2*cm, height=1*cm, kind='bound') table_data.append([f_label, f_text]) if table_data: # safety belt; empty tables lead to problems. data.append(Table(table_data, style=SLIP_STYLE)) return data class StudentsUtils(grok.GlobalUtility): """A collection of methods subject to customization. """ grok.implements(IStudentsUtils) def getReturningData(self, student): """ Define what happens after school fee payment depending on the student's senate verdict. In the base configuration current level is always increased by 100 no matter which verdict has been assigned. """ new_level = student['studycourse'].current_level + 100 new_session = student['studycourse'].current_session + 1 return new_session, new_level def setReturningData(self, student): """ Define what happens after school fee payment depending on the student's senate verdict. This method folllows the same algorithm as getReturningData but it also sets the new values. """ new_session, new_level = self.getReturningData(student) student['studycourse'].current_level = new_level student['studycourse'].current_session = new_session verdict = student['studycourse'].current_verdict student['studycourse'].current_verdict = '0' student['studycourse'].previous_verdict = verdict return def _getSessionConfiguration(self, session): try: return grok.getSite()['configuration'][str(session)] except KeyError: return None def setPaymentDetails(self, category, student, previous_session, previous_level): """Create Payment object and set the payment data of a student for the payment category specified. """ p_item = u'' amount = 0.0 if previous_session: if previous_session < student['studycourse'].entry_session: return _('The previous session must not fall below ' 'your entry session.'), None if category == 'schoolfee': # School fee is always paid for the following session if previous_session > student['studycourse'].current_session: return _('This is not a previous session.'), None else: if previous_session > student['studycourse'].current_session - 1: return _('This is not a previous session.'), None p_session = previous_session p_level = previous_level p_current = False else: p_session = student['studycourse'].current_session p_level = student['studycourse'].current_level p_current = True academic_session = self._getSessionConfiguration(p_session) if academic_session == None: return _(u'Session configuration object is not available.'), None # Determine fee. if category == 'schoolfee': try: certificate = student['studycourse'].certificate p_item = certificate.code except (AttributeError, TypeError): return _('Study course data are incomplete.'), None if previous_session: # Students can pay for previous sessions in all workflow states. # Fresh students are excluded by the update method of the # PreviousPaymentAddFormPage. if previous_level == 100: amount = getattr(certificate, 'school_fee_1', 0.0) else: amount = getattr(certificate, 'school_fee_2', 0.0) else: if student.state == CLEARED: amount = getattr(certificate, 'school_fee_1', 0.0) elif student.state == RETURNING: # In case of returning school fee payment the payment session # and level contain the values of the session the student # has paid for. Payment session is always next session. p_session, p_level = self.getReturningData(student) academic_session = self._getSessionConfiguration(p_session) if academic_session == None: return _(u'Session configuration object is not available.'), None amount = getattr(certificate, 'school_fee_2', 0.0) elif student.is_postgrad and student.state == PAID: # Returning postgraduate students also pay for the next session # but their level always remains the same. p_session += 1 academic_session = self._getSessionConfiguration(p_session) if academic_session == None: return _(u'Session configuration object is not available.'), None amount = getattr(certificate, 'school_fee_2', 0.0) elif category == 'clearance': try: p_item = student['studycourse'].certificate.code except (AttributeError, TypeError): return _('Study course data are incomplete.'), None amount = academic_session.clearance_fee elif category == 'bed_allocation': p_item = self.getAccommodationDetails(student)['bt'] amount = academic_session.booking_fee elif category == 'hostel_maintenance': amount = academic_session.maint_fee bedticket = student['accommodation'].get( str(student.current_session), None) if bedticket: p_item = bedticket.bed_coordinates else: # Should not happen because this is already checked # in the browser module, but anyway ... portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE p_item = trans(_('no bed allocated'), portal_language) if amount in (0.0, None): return _('Amount could not be determined.'), None for key in student['payments'].keys(): ticket = student['payments'][key] if ticket.p_state == 'paid' and\ ticket.p_category == category and \ ticket.p_item == p_item and \ ticket.p_session == p_session: return _('This type of payment has already been made.'), None payment = createObject(u'waeup.StudentOnlinePayment') timestamp = ("%d" % int(time()*10000))[1:] payment.p_id = "p%s" % timestamp payment.p_category = category payment.p_item = p_item payment.p_session = p_session payment.p_level = p_level payment.p_current = p_current payment.amount_auth = amount return None, payment def setBalanceDetails(self, balance_item, student, balance_session, balance_level, balance_amount): """Create Payment object and set the payment data of a student for. """ p_item = getUtility(IKofaUtils).BALANCE_PAYMENT_ITEMS[balance_item] p_item = unicode(p_item) p_session = balance_session p_level = balance_level p_current = False amount = balance_amount academic_session = self._getSessionConfiguration(p_session) if academic_session == None: return _(u'Session configuration object is not available.'), None if amount in (0.0, None): return _('Amount could not be determined.'), None for key in student['payments'].keys(): ticket = student['payments'][key] if ticket.p_state == 'paid' and\ ticket.p_category == 'balance' and \ ticket.p_item == p_item and \ ticket.p_session == p_session: return _('This type of payment has already been made.'), None payment = createObject(u'waeup.StudentOnlinePayment') timestamp = ("%d" % int(time()*10000))[1:] payment.p_id = "p%s" % timestamp payment.p_category = 'balance' payment.p_item = p_item payment.p_session = p_session payment.p_level = p_level payment.p_current = p_current payment.amount_auth = amount return None, payment def getAccommodationDetails(self, student): """Determine the accommodation data of a student. """ d = {} d['error'] = u'' hostels = grok.getSite()['hostels'] d['booking_session'] = hostels.accommodation_session d['allowed_states'] = hostels.accommodation_states d['startdate'] = hostels.startdate d['enddate'] = hostels.enddate d['expired'] = hostels.expired # Determine bed type studycourse = student['studycourse'] certificate = getattr(studycourse,'certificate',None) entry_session = studycourse.entry_session current_level = studycourse.current_level if None in (entry_session, current_level, certificate): return d end_level = certificate.end_level if current_level == 10: bt = 'pr' elif entry_session == grok.getSite()['hostels'].accommodation_session: bt = 'fr' elif current_level >= end_level: bt = 'fi' else: bt = 're' if student.sex == 'f': sex = 'female' else: sex = 'male' special_handling = 'regular' d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt) return d def selectBed(self, available_beds): """Select a bed from a list of available beds. In the base configuration we select the first bed found, but can also randomize the selection if we like. """ return available_beds[0] def renderPDFAdmissionLetter(self, view, student=None): """Render pdf admission letter. """ # XXX: we have to fix the import problems here. from waeup.kofa.browser.interfaces import IPDFCreator from waeup.kofa.browser.pdf import format_html, NOTE_STYLE if student is None: return style = getSampleStyleSheet() creator = getUtility(IPDFCreator) data = [] doc_title = view.label author = '%s (%s)' % (view.request.principal.title, view.request.principal.id) footer_text = view.label if getattr(student, 'student_id', None) is not None: footer_text = "%s - %s - " % (student.student_id, footer_text) # Admission text portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE inst_name = grok.getSite()['configuration'].name text = trans(_( 'This is to inform you that you have been provisionally' ' admitted into ${a} as follows:', mapping = {'a': inst_name}), portal_language) html = format_html(text) data.append(Paragraph(html, NOTE_STYLE)) data.append(Spacer(1, 20)) # Student data data.append(render_student_data(view)) # Insert history data.append(Spacer(1, 20)) datelist = student.history.messages[0].split()[0].split('-') creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0]) text = trans(_( 'Your Kofa student record was created on ${a}.', mapping = {'a': creation_date}), portal_language) html = format_html(text) data.append(Paragraph(html, NOTE_STYLE)) # Create pdf stream view.response.setHeader( 'Content-Type', 'application/pdf') pdf_stream = creator.create_pdf( data, None, doc_title, author=author, footer=footer_text, note=None) return pdf_stream def renderPDF(self, view, filename='slip.pdf', student=None, studentview=None, tableheader=None, tabledata=None, note=None, signatures=None, sigs_in_footer=(), show_scans=True): """Render pdf slips for various pages. """ # XXX: we have to fix the import problems here. from waeup.kofa.browser.interfaces import IPDFCreator from waeup.kofa.browser.pdf import NORMAL_STYLE, ENTRY1_STYLE style = getSampleStyleSheet() creator = getUtility(IPDFCreator) data = [] doc_title = view.label author = '%s (%s)' % (view.request.principal.title, view.request.principal.id) footer_text = view.label if getattr(student, 'student_id', None) is not None: footer_text = "%s - %s - " % (student.student_id, footer_text) # Insert student data table portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE if student is not None: bd_translation = trans(_('Base Data'), portal_language) data.append(Paragraph(bd_translation, style["Heading3"])) data.append(render_student_data(studentview)) # Insert widgets if view.form_fields: data.append(Paragraph(view.title, style["Heading3"])) portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE separators = getattr(self, 'SEPARATORS_DICT', {}) table = creator.getWidgetsTable( view.form_fields, view.context, None, lang=portal_language, separators=separators) data.append(table) # Insert scanned docs if show_scans: data.extend(docs_as_flowables(view, portal_language)) # Insert history if filename.startswith('clearance') or filename.startswith('course'): hist_translation = trans(_('Workflow History'), portal_language) data.append(Paragraph(hist_translation, style["Heading3"])) data.extend(creator.fromStringList(student.history.messages)) # Insert content table (optionally on second page) if tabledata and tableheader: #data.append(PageBreak()) data.append(Spacer(1, 20)) data.append(Paragraph(view.content_title, style["Heading3"])) contenttable = render_table_data(tableheader,tabledata) data.append(contenttable) # Insert signatures if signatures and not sigs_in_footer: data.append(Spacer(1, 20)) signaturetable = get_signature_table(signatures) data.append(signaturetable) view.response.setHeader( 'Content-Type', 'application/pdf') try: pdf_stream = creator.create_pdf( data, None, doc_title, author=author, footer=footer_text, note=note, sigs_in_footer=sigs_in_footer) except IOError: view.flash('Error in image file.') return view.redirect(view.url(view.context)) return pdf_stream def maxCredits(self, studylevel): """Return maximum credits. In some universities maximum credits is not constant, it depends on the student's study level. """ return 50 def maxCreditsExceeded(self, studylevel, course): max_credits = self.maxCredits(studylevel) if max_credits and \ studylevel.total_credits + course.credits > max_credits: return max_credits return 0 VERDICTS_DICT = { '0': _('(not yet)'), 'A': 'Successful student', 'B': 'Student with carryover courses', 'C': 'Student on probation', } SEPARATORS_DICT = { } #: A prefix used when generating new student ids. Each student id will #: start with this string. The default is 'K' for ``Kofa``. STUDENT_ID_PREFIX = u'K'