## $Id: utils.py 14473 2017-01-27 15:53:33Z 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 students 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 reportlab.platypus.doctemplate import LayoutError from zope.event import notify from zope.schema.interfaces import ConstraintNotSatisfied 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 from waeup.kofa.students.workflow import ADMITTED from waeup.kofa.students.vocabularies import StudyLevelSource, MatNumNotInSource from waeup.kofa.browser.pdf import ( ENTRY1_STYLE, format_html, NOTE_STYLE, HEADING_STYLE, get_signature_tables, get_qrcode) from waeup.kofa.browser.interfaces import IPDFCreator from waeup.kofa.utils.helpers import to_timezone 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), ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), ('BOX', (0,0), (-1,-1), 1, colors.black), ] FONT_SIZE = 10 FONT_COLOR = 'black' def trans(text, lang): # shortcut return translate(text, 'waeup.kofa', target_language=lang) def formatted_text(text, color=FONT_COLOR, lang='en'): """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. """ if not isinstance(text, unicode): if isinstance(text, basestring): text = text.decode('utf-8') else: text = unicode(text) if text == 'None': text = '' # Very long matriculation numbers need to be wrapped if text.find(' ') == -1 and len(text.split('/')) > 6: text = '/'.join(text.split('/')[:5]) + \ '/ ' + '/'.join(text.split('/')[5:]) # Mainly for boolean values we need our customized # localisation of the zope domain text = translate(text, 'zope', target_language=lang) text = text.replace('', '
') tag1 = u'' % (color) 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, context, omit_fields=(), lang='en', slipname=None, no_passport=False): """Render student table for an existing frame. """ width, height = A4 set_up_widgets(studentview, ignore_request=True) data_left = [] data_middle = [] 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)]) f_label = trans(_('Name:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text(studentview.context.display_fullname) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) for widget in studentview.widgets: if 'name' in widget.name: continue f_label = translate( widget.label.strip(), 'waeup.kofa', target_language=lang) f_label = Paragraph('%s:' % f_label, ENTRY1_STYLE) f_text = formatted_text(widget(), lang=lang) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) if getattr(studentview.context, 'certcode', None): if not 'certificate' in omit_fields: f_label = trans(_('Study Course:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text( studentview.context['studycourse'].certificate.longtitle) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) if not 'department' in omit_fields: f_label = trans(_('Department:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text( studentview.context[ 'studycourse'].certificate.__parent__.__parent__.longtitle, ) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) if not 'faculty' in omit_fields: f_label = trans(_('Faculty:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text( studentview.context[ 'studycourse'].certificate.__parent__.__parent__.__parent__.longtitle, ) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) if not 'current_mode' in omit_fields: studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT sm = studymodes_dict[studentview.context.current_mode] f_label = trans(_('Study Mode:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text(sm) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) if not 'entry_session' in omit_fields: f_label = trans(_('Entry Session:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) entry_session = studentview.context.entry_session entry_session = academic_sessions_vocab.getTerm(entry_session).title f_text = formatted_text(entry_session) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) # Requested by Uniben, does not really make sense if not 'current_level' in omit_fields: f_label = trans(_('Current Level:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) current_level = studentview.context['studycourse'].current_level studylevelsource = StudyLevelSource().factory current_level = studylevelsource.getTitle( studentview.context, current_level) f_text = formatted_text(current_level) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) if not 'date_of_birth' in omit_fields: f_label = trans(_('Date of Birth:'), lang) f_label = Paragraph(f_label, ENTRY1_STYLE) date_of_birth = studentview.context.date_of_birth tz = getUtility(IKofaUtils).tzinfo date_of_birth = to_timezone(date_of_birth, tz) if date_of_birth is not None: date_of_birth = date_of_birth.strftime("%d/%m/%Y") f_text = formatted_text(date_of_birth) f_text = Paragraph(f_text, ENTRY1_STYLE) data_middle.append([f_label,f_text]) if no_passport: table = Table(data_middle,style=SLIP_STYLE) table.hAlign = 'LEFT' return table # append QR code to the right if slipname: url = studentview.url(context, slipname) data_right = [[get_qrcode(url, width=70.0)]] table_right = Table(data_right,style=SLIP_STYLE) else: table_right = None table_left = Table(data_left,style=SLIP_STYLE) table_middle = Table(data_middle,style=SLIP_STYLE, colWidths=[5*cm, 5*cm]) table = Table([[table_left, table_middle, table_right],],style=SLIP_STYLE) return table def render_table_data(tableheader, tabledata, lang='en'): """Render children table for an existing frame. """ data = [] #data.append([Spacer(1, 12)]) line = [] style = getSampleStyleSheet() for element in tableheader: field = '%s' % formatted_text(element[0], lang=lang) 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 render_transcript_data(view, tableheader, levels_data, lang='en'): """Render children table for an existing frame. """ data = [] style = getSampleStyleSheet() format_float = getUtility(IKofaUtils).format_float for level in levels_data: level_obj = level['level'] tickets = level['tickets_1'] + level['tickets_2'] + level['tickets_3'] headerline = [] tabledata = [] subheader = '%s %s, %s %s' % ( trans(_('Session'), lang), view.session_dict[level_obj.level_session], trans(_('Level'), lang), view.level_dict[level_obj.level]) data.append(Paragraph(subheader, HEADING_STYLE)) for element in tableheader: field = '%s' % formatted_text(element[0]) field = Paragraph(field, style["Normal"]) headerline.append(field) tabledata.append(headerline) for ticket in tickets: ticketline = [] for element in tableheader: field = formatted_text(getattr(ticket,element[1],u' ')) field = Paragraph(field, style["Normal"]) ticketline.append(field) tabledata.append(ticketline) table = Table(tabledata,colWidths=[ element[2]*cm for element in tableheader], style=CONTENT_STYLE) data.append(table) sgpa = format_float(level['sgpa'], 2) sgpa = '%s: %s' % (trans('Sessional GPA (rectified)', lang), sgpa) #sgpa = '%s: %.2f' % (trans('Sessional GPA (rectified)', lang), level['sgpa']) data.append(Paragraph(sgpa, style["Normal"])) return data def docs_as_flowables(view, lang='en'): """Create reportlab flowables out of scanned docs. """ # XXX: fix circular import problem from waeup.kofa.browser.fileviewlets import FileManager from waeup.kofa.browser import DEFAULT_IMAGE_PATH 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, HEADING_STYLE)) # Insert list of scanned documents table_data = [] for viewlet in fm.viewlets: if viewlet.file_exists: # Show viewlet only if file exists 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) try: student['studycourse'].current_level = new_level except ConstraintNotSatisfied: # Do not change level if level exceeds the # certificate's end_level. pass 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 _isPaymentDisabled(self, p_session, category, student): academic_session = self._getSessionConfiguration(p_session) if category == 'schoolfee' and \ 'sf_all' in academic_session.payment_disabled: return True return False def samePaymentMade(self, student, category, p_item, p_session): 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 True return False def setPaymentDetails(self, category, student, previous_session, previous_level): """Create a payment ticket 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 = 0.0 bedticket = student['accommodation'].get( str(student.current_session), None) if bedticket is not None and bedticket.bed is not None: p_item = bedticket.bed_coordinates if bedticket.bed.__parent__.maint_fee > 0: amount = bedticket.bed.__parent__.maint_fee else: # fallback amount = academic_session.maint_fee else: return _(u'No bed allocated.'), None elif category == 'transcript': amount = academic_session.transcript_fee elif category == 'transfer': amount = academic_session.transfer_fee elif category == 'late_registration': amount = academic_session.late_registration_fee if amount in (0.0, None): return _('Amount could not be determined.'), None if self.samePaymentMade(student, category, p_item, p_session): return _('This type of payment has already been made.'), None if self._isPaymentDisabled(p_session, category, student): return _('This category of payments has been disabled.'), 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, category, student, balance_session, balance_level, balance_amount): """Create a balance payment ticket and set the payment data as selected by the student. """ p_item = u'Balance' 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) or amount < 0: return _('Amount must be greater than 0.'), 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 increaseMatricInteger(self, student): """Increase counter for matric numbers. This counter can be a centrally stored attribute or an attribute of faculties, departments or certificates. In the base package the counter is as an attribute of the site configuration container. """ grok.getSite()['configuration'].next_matric_integer += 1 return def constructMatricNumber(self, student): """Fetch the matric number counter which fits the student and construct the new matric number of the student. In the base package the counter is returned which is as an attribute of the site configuration container. """ next_integer = grok.getSite()['configuration'].next_matric_integer if next_integer == 0: return _('Matriculation number cannot be set.'), None return None, unicode(next_integer) def setMatricNumber(self, student): """Set matriculation number of student. If the student's matric number is unset a new matric number is constructed according to the matriculation number construction rules defined in the `constructMatricNumber` method. The new matric number is set, the students catalog updated. The corresponding matric number counter is increased by one. This method is tested but not used in the base package. It can be used in custom packages by adding respective views and by customizing `increaseMatricInteger` and `constructMatricNumber` according to the university's matriculation number construction rules. The method can be disabled by setting the counter to zero. """ if student.matric_number is not None: return _('Matriculation number already set.'), None if student.certcode is None: return _('No certificate assigned.'), None error, matric_number = self.constructMatricNumber(student) if error: return error, None try: student.matric_number = matric_number except MatNumNotInSource: return _('Matriculation number %s exists.' % matric_number), None notify(grok.ObjectModifiedEvent(student)) self.increaseMatricInteger(student) return None, matric_number 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 checkAccommodationRequirements(self, student, acc_details): if acc_details.get('expired', False): startdate = acc_details.get('startdate') enddate = acc_details.get('enddate') if startdate and enddate: tz = getUtility(IKofaUtils).tzinfo startdate = to_timezone( startdate, tz).strftime("%d/%m/%Y %H:%M:%S") enddate = to_timezone( enddate, tz).strftime("%d/%m/%Y %H:%M:%S") return _("Outside booking period: ${a} - ${b}", mapping = {'a': startdate, 'b': enddate}) else: return _("Outside booking period.") if not acc_details.get('bt'): return _("Your data are incomplete.") if not student.state in acc_details['allowed_states']: return _("You are in the wrong registration state.") if student['studycourse'].current_session != acc_details[ 'booking_session']: return _('Your current session does not ' 'match accommodation session.') if str(acc_details['booking_session']) in student['accommodation'].keys(): return _('You already booked a bed space in ' 'current accommodation session.') return def selectBed(self, available_beds, desired_hostel=None): """Select a bed from a filtered list of available beds. In the base configuration beds are sorted by the sort id of the hostel and the bed number. The first bed found in this sorted list is taken. """ sorted_beds = sorted(available_beds, key=lambda bed: 1000 * bed.__parent__.sort_id + bed.bed_number) if desired_hostel: # Filter desired hostel beds filtered_beds = [bed for bed in sorted_beds if bed.bed_id.startswith(desired_hostel)] if not filtered_beds: return return filtered_beds[0] return sorted_beds[0] def _admissionText(self, student, 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) return text def renderPDFAdmissionLetter(self, view, student=None, omit_fields=(), pre_text=None, post_text=None,): """Render pdf admission letter. """ if student is None: return style = getSampleStyleSheet() creator = self.getPDFCreator(student) portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE data = [] doc_title = view.label author = '%s (%s)' % (view.request.principal.title, view.request.principal.id) footer_text = view.label.split('\n') if len(footer_text) > 1: # We can add a department in first line footer_text = footer_text[1] else: # Only the first line is used for the footer footer_text = footer_text[0] if getattr(student, 'student_id', None) is not None: footer_text = "%s - %s - " % (student.student_id, footer_text) # Text before student data if pre_text is None: html = format_html(self._admissionText(student, portal_language)) else: html = format_html(pre_text) if html: data.append(Paragraph(html, NOTE_STYLE)) data.append(Spacer(1, 20)) # Student data data.append(render_student_data(view, student, omit_fields, lang=portal_language, slipname='admission_slip.pdf')) # Text after student data data.append(Spacer(1, 20)) if post_text is None: datelist = student.history.messages[0].split()[0].split('-') creation_date = u'%s/%s/%s' % (datelist[2], datelist[1], datelist[0]) post_text = trans(_( 'Your Kofa student record was created on ${a}.', mapping = {'a': creation_date}), portal_language) #html = format_html(post_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=post_text) return pdf_stream def getPDFCreator(self, context): """Get a pdf creator suitable for `context`. The default implementation always returns the default creator. """ return getUtility(IPDFCreator) def renderPDF(self, view, filename='slip.pdf', student=None, studentview=None, tableheader=[], tabledata=[], note=None, signatures=None, sigs_in_footer=(), show_scans=True, topMargin=1.5, omit_fields=()): """Render pdf slips for various pages (also some pages in the applicants module). """ portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE # XXX: tell what the different parameters mean style = getSampleStyleSheet() creator = self.getPDFCreator(student) data = [] doc_title = view.label author = '%s (%s)' % (view.request.principal.title, view.request.principal.id) footer_text = view.label.split('\n') if len(footer_text) > 1: # We can add a department in first line, second line is used footer_text = footer_text[1] else: # Only the first line is used for the footer footer_text = footer_text[0] if getattr(student, 'student_id', None) is not None: footer_text = "%s - %s - " % (student.student_id, footer_text) # Insert student data table if student is not None: bd_translation = trans(_('Base Data'), portal_language) data.append(Paragraph(bd_translation, HEADING_STYLE)) data.append(render_student_data( studentview, view.context, omit_fields, lang=portal_language, slipname=filename)) # Insert widgets if view.form_fields: data.append(Paragraph(view.title, HEADING_STYLE)) 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'): hist_translation = trans(_('Workflow History'), portal_language) data.append(Paragraph(hist_translation, HEADING_STYLE)) data.extend(creator.fromStringList(student.history.messages)) # Insert content tables (optionally on second page) if hasattr(view, 'tabletitle'): for i in range(len(view.tabletitle)): if tabledata[i] and tableheader[i]: #data.append(PageBreak()) #data.append(Spacer(1, 20)) data.append(Paragraph(view.tabletitle[i], HEADING_STYLE)) data.append(Spacer(1, 8)) contenttable = render_table_data(tableheader[i],tabledata[i]) data.append(contenttable) # Insert signatures # XXX: We are using only sigs_in_footer in waeup.kofa, so we # do not have a test for the following lines. if signatures and not sigs_in_footer: data.append(Spacer(1, 20)) # Render one signature table per signature to # get date and signature in line. for signature in signatures: signaturetables = get_signature_tables(signature) data.append(signaturetables[0]) 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, topMargin=topMargin) except IOError: view.flash('Error in image file.') return view.redirect(view.url(view.context)) except LayoutError, err: view.flash( 'PDF file could not be created. Reportlab error message: %s' % escape(err.message), type="danger") return view.redirect(view.url(view.context)) return pdf_stream gpa_boundaries = ((1, 'Fail'), (1.5, 'Pass'), (2.4, '3rd Class'), (3.5, '2nd Class Lower'), (4.5, '2nd Class Upper'), (5, '1st Class')) def getClassFromCGPA(self, gpa, student): """Determine the class of degree. In some custom packages this class depends on e.g. the entry session of the student. In the base package, it does not. """ if gpa < self.gpa_boundaries[0][0]: return 0, self.gpa_boundaries[0][1] if gpa < self.gpa_boundaries[1][0]: return 1, self.gpa_boundaries[1][1] if gpa < self.gpa_boundaries[2][0]: return 2, self.gpa_boundaries[2][1] if gpa < self.gpa_boundaries[3][0]: return 3, self.gpa_boundaries[3][1] if gpa < self.gpa_boundaries[4][0]: return 4, self.gpa_boundaries[4][1] if gpa <= self.gpa_boundaries[5][0]: return 5, self.gpa_boundaries[5][1] return 'N/A' def getDegreeClassNumber(self, level_obj): """Get degree class number (used for SessionResultsPresentation reports). """ if level_obj.gpa_params[1] == 0: # No credits weighted return 6 return self.getClassFromCGPA( level_obj.cumulative_params[0], level_obj.student)[0] def renderPDFTranscript(self, view, filename='transcript.pdf', student=None, studentview=None, note=None, signatures=None, sigs_in_footer=(), show_scans=True, topMargin=1.5, omit_fields=(), tableheader=None, no_passport=False): """Render pdf slips for transcripts. """ portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE # XXX: tell what the different parameters mean style = getSampleStyleSheet() creator = self.getPDFCreator(student) data = [] doc_title = view.label author = '%s (%s)' % (view.request.principal.title, view.request.principal.id) footer_text = view.label.split('\n') if len(footer_text) > 2: # We can add a department in first line footer_text = footer_text[1] else: # Only the first line is used for the footer footer_text = footer_text[0] if getattr(student, 'student_id', None) is not None: footer_text = "%s - %s - " % (student.student_id, footer_text) # Insert student data table if student is not None: #bd_translation = trans(_('Base Data'), portal_language) #data.append(Paragraph(bd_translation, HEADING_STYLE)) data.append(render_student_data( studentview, view.context, omit_fields, lang=portal_language, slipname=filename, no_passport=no_passport)) transcript_data = view.context.getTranscriptData() levels_data = transcript_data[0] contextdata = [] f_label = trans(_('Course of Study:'), portal_language) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text(view.context.certificate.longtitle) f_text = Paragraph(f_text, ENTRY1_STYLE) contextdata.append([f_label,f_text]) f_label = trans(_('Faculty:'), portal_language) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text( view.context.certificate.__parent__.__parent__.__parent__.longtitle) f_text = Paragraph(f_text, ENTRY1_STYLE) contextdata.append([f_label,f_text]) f_label = trans(_('Department:'), portal_language) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text( view.context.certificate.__parent__.__parent__.longtitle) f_text = Paragraph(f_text, ENTRY1_STYLE) contextdata.append([f_label,f_text]) f_label = trans(_('Entry Session:'), portal_language) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text( view.session_dict.get(view.context.entry_session)) f_text = Paragraph(f_text, ENTRY1_STYLE) contextdata.append([f_label,f_text]) f_label = trans(_('Entry Mode:'), portal_language) f_label = Paragraph(f_label, ENTRY1_STYLE) f_text = formatted_text(view.studymode_dict.get( view.context.entry_mode)) f_text = Paragraph(f_text, ENTRY1_STYLE) contextdata.append([f_label,f_text]) f_label = trans(_('Cumulative GPA:'), portal_language) f_label = Paragraph(f_label, ENTRY1_STYLE) format_float = getUtility(IKofaUtils).format_float cgpa = format_float(transcript_data[1], 3) f_text = formatted_text('%s (%s)' % ( cgpa, self.getClassFromCGPA(transcript_data[1], student)[1])) f_text = Paragraph(f_text, ENTRY1_STYLE) contextdata.append([f_label,f_text]) contexttable = Table(contextdata,style=SLIP_STYLE) data.append(contexttable) transcripttables = render_transcript_data( view, tableheader, levels_data, lang=portal_language) data.extend(transcripttables) # Insert signatures # XXX: We are using only sigs_in_footer in waeup.kofa, so we # do not have a test for the following lines. if signatures and not sigs_in_footer: data.append(Spacer(1, 20)) # Render one signature table per signature to # get date and signature in line. for signature in signatures: signaturetables = get_signature_tables(signature) data.append(signaturetables[0]) 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, topMargin=topMargin) except IOError: view.flash(_('Error in image file.')) return view.redirect(view.url(view.context)) return pdf_stream def renderPDFCourseticketsOverview( self, view, session, data, lecturers): filename = 'coursetickets_%s_%s_%s.pdf' % ( view.context.code, session, view.request.principal.id) session = academic_sessions_vocab.getTerm(session).title creator = getUtility(IPDFCreator, name='landscape') style = getSampleStyleSheet() pdf_data = [Paragraph( translate(_('Lecturer(s): ${a}', mapping = {'a':lecturers})), style["Normal"]),] pdf_data += [Paragraph( translate(_('Credits: ${a}', mapping = {'a':view.context.credits})), style["Normal"]),] # Not used in base package. if data[1]: pdf_data += [Paragraph( translate(_('Total Students: ${a}', mapping = {'a':data[1][0]})), style["Normal"]),] pdf_data += [Paragraph( translate(_('Total Pass: ${a} (${b}%)', mapping = {'a':data[1][1],'b':data[1][2]})), style["Normal"]),] pdf_data += [Paragraph( translate(_('Total Fail: ${a} (${b}%)', mapping = {'a':data[1][3],'b':data[1][4]})), style["Normal"]),] pdf_data.append(Spacer(1, 20)) pdf_data += [Table(data[0], style=CONTENT_STYLE)] doc_title = translate(_('${a} (${b}) - Academic Session ${d}', mapping = {'a':view.context.title, 'b':view.context.code, 'd':session})) author = '%s (%s)' % (view.request.principal.title, view.request.principal.id) view.response.setHeader( 'Content-Type', 'application/pdf') view.response.setHeader( 'Content-Disposition:', 'attachment; filename="%s' % filename) pdf_stream = creator.create_pdf( pdf_data, None, doc_title, author, doc_title + ' -' ) return pdf_stream def maxCredits(self, studylevel): """Return maximum credits. At some universities maximum credits is not constant, it depends on the student's study level. Set `maxCredits` None or 0 in order to deactivate the limitation. """ 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 def getBedCoordinates(self, bedticket): """Return descriptive bed coordinates. This method can be used to customize the `display_coordinates` property method in order to display a customary description of the bed space. """ return bedticket.bed_coordinates def clearance_disabled_message(self, student): try: session_config = grok.getSite()[ 'configuration'][str(student.current_session)] except KeyError: return _('Session configuration object is not available.') if not session_config.clearance_enabled: return _('Clearance is disabled for this session.') return None #: A dictionary which maps widget names to headlines. The headline #: is rendered in forms and on pdf slips above the respective #: display or input widget. There are no separating headlines #: in the base package. SEPARATORS_DICT = {} #: A tuple containing names of file upload viewlets which are not shown #: on the `StudentClearanceManageFormPage`. Nothing is being skipped #: in the base package. This attribute makes only sense, if intermediate #: custom packages are being used, like we do for all Nigerian portals. SKIP_UPLOAD_VIEWLETS = () #: A tuple containing the names of registration states in which changing of #: passport pictures is allowed. PORTRAIT_CHANGE_STATES = (ADMITTED,) #: A tuple containing all exporter names referring to students or #: subobjects thereof. STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses', 'studentstudylevels', 'coursetickets', 'studentpayments', 'studentunpaidpayments', 'bedtickets', 'paymentsoverview', 'studylevelsoverview', 'combocard', 'bursary') #: A tuple containing all exporter names needed for backing #: up student data STUDENT_BACKUP_EXPORTER_NAMES = ('students', 'studentstudycourses', 'studentstudylevels', 'coursetickets', 'studentpayments', 'bedtickets') #: 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'