source: main/waeup.aaue/trunk/src/waeup/aaue/students/browser.py @ 15458

Last change on this file since 15458 was 15457, checked in by Henrik Bettermann, 6 years ago

Show signatures for bridge.

  • Property svn:keywords set to Id
File size: 50.3 KB
Line 
1## $Id: browser.py 15457 2019-06-17 09:05:20Z henrik $
2##
3## Copyright (C) 2012 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##
18import grok
19import csv
20import textwrap
21import pytz
22from cStringIO import StringIO
23from datetime import datetime
24from zope.i18n import translate
25from zope.component import getUtility, queryUtility
26from zope.schema.interfaces import TooBig, TooSmall
27from zope.security import checkPermission
28from zope.catalog.interfaces import ICatalog
29from zope.formlib.textwidgets import BytesDisplayWidget
30from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
31from waeup.kofa.browser.layout import UtilityView, KofaEditFormPage, jsaction
32from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
33from waeup.kofa.interfaces import (
34    IKofaUtils, academic_sessions_vocab, ICSVExporter, IKofaObject)
35from waeup.kofa.students.interfaces import (
36    IStudentsUtils, IStudent, IStudentRequestPW, IStudentStudyLevel)
37from waeup.kofa.students.workflow import PAID, REGISTERED, RETURNING
38from waeup.kofa.students.browser import (
39    StartClearancePage,
40    StudentBasePDFFormPage,
41    CourseTicketAddFormPage,
42    StudyLevelDisplayFormPage,
43    StudyLevelManageFormPage,
44    StudyLevelEditFormPage,
45    ExportPDFTranscriptSlip,
46    ExportPDFAdmissionSlip,
47    BedTicketAddPage,
48    StudentFilesUploadPage,
49    PaymentsManageFormPage,
50    CourseTicketDisplayFormPage,
51    CourseTicketManageFormPage,
52    EditScoresPage,
53    ExportPDFScoresSlip,
54    StudyCourseTranscriptPage,
55    DownloadScoresView,
56    StudentRequestPasswordPage,
57    StudyCourseManageFormPage,
58    UnregisterCoursesView,
59    addCourseTicket,
60    emit_lock_message
61    )
62from kofacustom.nigeria.students.browser import (
63    NigeriaOnlinePaymentDisplayFormPage,
64    NigeriaOnlinePaymentAddFormPage,
65    NigeriaExportPDFPaymentSlip,
66    NigeriaExportPDFCourseRegistrationSlip,
67    NigeriaStudentPersonalDisplayFormPage,
68    NigeriaStudentPersonalEditFormPage,
69    NigeriaStudentPersonalManageFormPage,
70    NigeriaStudentClearanceDisplayFormPage,
71    NigeriaExportPDFClearanceSlip,
72    NigeriaStudentClearanceManageFormPage,
73    NigeriaStudentClearanceEditFormPage,
74    NigeriaAccommodationManageFormPage,
75    NigeriaStudentBaseDisplayFormPage,
76    NigeriaStudentBaseManageFormPage
77    )
78from waeup.aaue.students.interfaces import (
79    ICustomStudentOnlinePayment,
80    ICustomStudentStudyLevel,
81    ICustomStudent,
82    ICustomStudentPersonal,
83    ICustomStudentPersonalEdit,
84    ICustomUGStudentClearance,
85    ICustomUGStudentClearanceEdit,
86    ICustomPGStudentClearance,
87    ICustomCourseTicket,
88    ICustomStudentBase,
89    ICustomStudentStudyCourse)
90from waeup.aaue.interswitch.browser import gateway_net_amt
91from waeup.aaue.interfaces import MessageFactory as _
92
93grok.context(IKofaObject)  # Make IKofaObject the default context
94
95def translated_values(view):
96    """Translate course ticket attribute values to be displayed on
97    studylevel pages.
98    """
99    lang = view.request.cookies.get('kofa.language')
100    for value in view.context.values():
101        value._p_activate()
102        value_dict = dict([i for i in value.__dict__.items()])
103        value_dict['url'] = view.url(value)
104        value_dict['removable_by_student'] = value.removable_by_student
105        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
106            target_language=lang)
107        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
108            target_language=lang)
109        value_dict['outstanding'] = translate(str(value.outstanding), 'zope',
110            target_language=lang)
111        value_dict['automatic'] = translate(str(value.automatic), 'zope',
112            target_language=lang)
113        value_dict['grade'] = value.grade
114        value_dict['weight'] = value.weight
115        value_dict['course_category'] = value.course_category
116        value_dict['total_score'] = value.total_score
117        semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
118        value_dict['semester'] = semester_dict[
119            value.semester].replace('mester', 'm.')
120        # AAUE specific
121        value_dict['formatted_total_score'] = value.total_score
122        if getattr(value, 'imported_ts', None):
123            value_dict['formatted_total_score'] = "<strong>%s</strong>" % value.imported_ts
124        yield value_dict
125
126class CustomStudentBaseDisplayFormPage(NigeriaStudentBaseDisplayFormPage):
127    """ Page to display student base data
128    """
129    form_fields = grok.AutoFields(ICustomStudentBase).omit(
130        'password', 'suspended', 'suspended_comment', 'flash_notice')
131    form_fields[
132        'financial_clearance_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
133
134class CustomStudentBaseManageFormPage(NigeriaStudentBaseManageFormPage):
135    """ View to manage student base data
136    """
137    form_fields = grok.AutoFields(ICustomStudentBase).omit(
138        'student_id', 'adm_code', 'suspended',
139        'financially_cleared_by', 'financial_clearance_date')
140
141class CustomStudentPersonalDisplayFormPage(NigeriaStudentPersonalDisplayFormPage):
142    """ Page to display student personal data
143    """
144    form_fields = grok.AutoFields(ICustomStudentPersonal)
145    form_fields['perm_address'].custom_widget = BytesDisplayWidget
146    form_fields['father_address'].custom_widget = BytesDisplayWidget
147    form_fields['mother_address'].custom_widget = BytesDisplayWidget
148    form_fields['next_kin_address'].custom_widget = BytesDisplayWidget
149    form_fields[
150        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
151
152class CustomStudentPersonalEditFormPage(NigeriaStudentPersonalEditFormPage):
153    """ Page to edit personal data
154    """
155    form_fields = grok.AutoFields(ICustomStudentPersonalEdit).omit('personal_updated')
156
157class CustomStudentPersonalManageFormPage(NigeriaStudentPersonalManageFormPage):
158    """ Page to edit personal data
159    """
160    form_fields = grok.AutoFields(ICustomStudentPersonal)
161    form_fields['personal_updated'].for_display = True
162    form_fields[
163        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
164
165
166class ExportExaminationScheduleSlip(UtilityView, grok.View):
167    """Deliver a examination schedule slip.
168
169    This form page is available only in Uniben and AAUE.
170    """
171    grok.context(ICustomStudent)
172    grok.name('examination_schedule_slip.pdf')
173    grok.require('waeup.viewStudent')
174    prefix = 'form'
175
176    label = u'Examination Schedule Slip'
177
178    omit_fields = (
179        'suspended', 'phone', 'email',
180        'adm_code', 'suspended_comment',
181        'date_of_birth', 'current_level',
182        'current_mode',
183        'entry_session',
184        'flash_notice')
185
186    form_fields = []
187
188    @property
189    def note(self):
190        return """
191 <br /><br />
192 <strong>Instructions on CBT Venue Allocation Slip (VAS)</strong>
193 <br /><br />
194 You should login with your student id from Kofa and surname as password.
195 Download and print two copies of this slip and bring them to the
196 allocated CBT examination center.
197 The copies <strong>MUST</strong> be shown to the invigilators
198 before being admitted into the examination hall.
199 <br /><br />
200 How to start examination:<br /><br />
201  * Username:  "student id" from Kofa e.g E1000000<br />
202  * Password: "surname" as shown on this slip in capital letters<br />
203  * Click the course and click "start exam".
204 <br /><br />
205 <strong>WARNING:</strong> Electronic devices (phones, tablets, laptops etc.)
206 are not allowed in the examination hall. Any electronics seized will not
207 be returned. Any student caught charging his/her mobile phone at the CBT
208 centers will be penalized and the exam of such a student will be cancelled.
209 Bags and any foreign materials are not allowed at the venue of
210 the CBT exams. Any omission/other complaints should be reported to the CBT
211 committee through the HoD before the date of examination.
212 <br /><br />
213 Your examination date, time and venue is scheduled as follows:
214 <br /><br />
215 <strong>%s</strong>
216""" % self.context.flash_notice
217        return
218
219
220    def update(self):
221        if not self.context.flash_notice \
222            or not 'exam' in self.context.flash_notice.lower():
223            self.flash(_('Forbidden'), type="warning")
224            self.redirect(self.url(self.context))
225
226    def render(self):
227        studentview = StudentBasePDFFormPage(self.context.student,
228            self.request, self.omit_fields)
229        students_utils = getUtility(IStudentsUtils)
230        return students_utils.renderPDF(
231            self, 'examination_schedule_slip',
232            self.context.student, studentview,
233            omit_fields=self.omit_fields,
234            note=self.note)
235
236class CustomStudentClearanceDisplayFormPage(NigeriaStudentClearanceDisplayFormPage):
237    """ Page to display student clearance data
238    """
239
240    @property
241    def form_fields(self):
242        if self.context.is_postgrad:
243            form_fields = grok.AutoFields(
244                ICustomPGStudentClearance).omit('clearance_locked')
245        else:
246            form_fields = grok.AutoFields(
247                ICustomUGStudentClearance).omit('clearance_locked')
248        if not getattr(self.context, 'officer_comment'):
249            form_fields = form_fields.omit('officer_comment')
250        else:
251            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
252        form_fields = form_fields.omit('def_adm')
253        return form_fields
254
255class CustomStudentClearanceManageFormPage(NigeriaStudentClearanceManageFormPage):
256    """ Page to edit student clearance data
257    """
258
259    @property
260    def form_fields(self):
261        if self.context.is_postgrad:
262            form_fields = grok.AutoFields(
263                ICustomPGStudentClearance).omit('clr_code')
264        else:
265            form_fields = grok.AutoFields(
266                ICustomUGStudentClearance).omit('clr_code')
267        form_fields = form_fields.omit('def_adm')
268        return form_fields
269
270class CustomStudentClearanceEditFormPage(NigeriaStudentClearanceEditFormPage):
271    """ View to edit student clearance data by student
272    """
273
274    @property
275    def form_fields(self):
276        if self.context.is_postgrad:
277            form_fields = grok.AutoFields(ICustomPGStudentClearance).omit(
278            'clearance_locked', 'nysc_location', 'clr_code', 'officer_comment',
279            'physical_clearance_date')
280        else:
281            form_fields = grok.AutoFields(ICustomUGStudentClearanceEdit).omit(
282            'clearance_locked', 'clr_code', 'officer_comment',
283            'physical_clearance_date', 'date_of_birth', 'nationality', 'lga')
284        form_fields = form_fields.omit('def_adm')
285        return form_fields
286
287class CustomStartClearancePage(StartClearancePage):
288    with_ac = False
289
290    @property
291    def all_required_fields_filled(self):
292        if not self.context.email:
293            return _("Email address is missing."), 'edit_base'
294        if not self.context.phone:
295            return _("Phone number is missing."), 'edit_base'
296        if not self.context.father_name:
297            return _("Personal data form is not properly filled."), 'edit_personal'
298        return
299
300class CustomOnlinePaymentDisplayFormPage(NigeriaOnlinePaymentDisplayFormPage):
301    """ Page to view an online payment ticket
302    """
303    grok.context(ICustomStudentOnlinePayment)
304    form_fields = grok.AutoFields(ICustomStudentOnlinePayment).omit(
305        'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item')
306    form_fields[
307        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
308    form_fields[
309        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
310
311class CustomPaymentsManageFormPage(PaymentsManageFormPage):
312    """ Page to manage the student payments.
313
314    This manage form page is for both students and students officers.
315    """
316    @property
317    def manage_payments_allowed(self):
318        return checkPermission('waeup.manageStudent', self.context)
319
320class CustomExportPDFPaymentSlip(NigeriaExportPDFPaymentSlip):
321    """Deliver a PDF slip of the context.
322    """
323    grok.context(ICustomStudentOnlinePayment)
324    form_fields = grok.AutoFields(ICustomStudentOnlinePayment).omit(
325        'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item')
326    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
327    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
328
329    @property
330    def note(self):
331        p_session = self.context.p_session
332        try:
333            academic_session = grok.getSite()['configuration'][str(p_session)]
334        except KeyError:
335            academic_session = None
336        text =  '\n\n The Amount Authorized is inclusive of: '
337        if self.context.p_category in ('schoolfee_incl', 'schoolfee_1') \
338            and academic_session:
339            #welfare_fee = gateway_net_amt(academic_session.welfare_fee)
340            #union_fee = gateway_net_amt(academic_session.union_fee)
341            if self.context.student.entry_session == 2016 \
342                and self.context.student.entry_mode == 'ug_ft' \
343                and self.context.p_session == 2016:
344                # Add student id card fee to first school fee payment.
345
346                ## Attention: The payment slip does not contain any information
347                ## whether the fee was added or not.
348                ## We can only draw conclusions from from the student's entry
349                ## session whether the fee had been included.
350                #id_card_fee = gateway_net_amt(academic_session.id_card_fee)
351                #text += ('School Fee, '
352                #         '%s Naira Student ID Card Fee, '
353                #         '%s Naira Student Union Dues, '
354                #         '%s Naira Student Welfare Assurance Fee and '
355                #         % (id_card_fee, union_fee, welfare_fee))
356
357                text += ('School Fee, '
358                         'Student ID Card Fee, '
359                         'Student Union Dues, '
360                         'Sports Fee, '
361                         'Library Levy, '
362                         'Student Welfare Assurance Fee and ')
363            else:
364
365                #text += ('School Fee, '
366                #         '%s Naira Student Union Dues, '
367                #         '%s Naira Student Welfare Assurance Fee and '
368                #         % (union_fee, welfare_fee))
369
370                text += ('School Fee, '
371                         'Student Union Dues, '
372                         'Sports Development Fee, '
373                         'Library Development Levy, '
374                         'Student Welfare Assurance Fee and ')
375        elif self.context.p_category in (
376            'clearance_incl', 'clearance_medical_incl') and academic_session:
377
378            #matric_gown_fee = gateway_net_amt(academic_session.matric_gown_fee)
379            #lapel_fee = gateway_net_amt(academic_session.lapel_fee)
380            #text += ('Acceptance Fee, '
381            #         '%s Naira Matriculation Gown Fee, '
382            #         '%s Naira Lapel/File Fee and '
383            #         % (matric_gown_fee, lapel_fee))
384
385            text += ('Acceptance Fee, '
386                     'Matriculation Gown Fee, '
387                     'Lapel/File Fee and ')
388
389        #return text + '250.0 Naira Transaction Charge.'
390
391        return text + 'Transaction Charge.'
392
393class CustomStudyCourseManageFormPage(StudyCourseManageFormPage):
394    """ Page to edit the student study course data
395    """
396    grok.context(ICustomStudentStudyCourse)
397
398    @property
399    def form_fields(self):
400        if self.context.is_postgrad:
401            form_fields = grok.AutoFields(ICustomStudentStudyCourse).omit(
402                'previous_verdict')
403        else:
404            form_fields = grok.AutoFields(ICustomStudentStudyCourse)
405        form_fields['imported_cgpa'].for_display = True
406        return form_fields
407
408#class CustomUnregisterCoursesView(UnregisterCoursesView):
409#    """Unregister course list by student
410#    """
411#    grok.context(ICustomStudentStudyLevel)
412
413#    def update(self):
414#        if not self.context.__parent__.is_current:
415#            emit_lock_message(self)
416#            return
417#        try:
418#            deadline = grok.getSite()['configuration'][
419#                str(self.context.level_session)].coursereg_deadline
420#        except (TypeError, KeyError):
421#            deadline = None
422#        # In AAUE fresh students are allowed to "unregister their course"
423#        # aside the deadline
424#        if deadline and not self.context.student.is_fresh \
425#            and deadline < datetime.now(pytz.utc):
426#            self.flash(_(
427#                "Course registration has ended. "
428#                "Unregistration is disabled."), type="warning")
429#        elif str(self.context.__parent__.current_level) != self.context.__name__:
430#            self.flash(_('This is not your current level.'), type="danger")
431#        elif self.context.student.state == REGISTERED:
432#            IWorkflowInfo(self.context.student).fireTransition('reset7')
433#            message = _('Course list has been unregistered.')
434#            self.flash(message)
435#        else:
436#            self.flash(_('You are in the wrong state.'), type="warning")
437#        self.redirect(self.url(self.context))
438#        return
439
440class CustomStudyLevelDisplayFormPage(StudyLevelDisplayFormPage):
441    """ Page to display student study levels
442    """
443    grok.context(ICustomStudentStudyLevel)
444    form_fields = grok.AutoFields(ICustomStudentStudyLevel).omit(
445        'total_credits', 'gpa', 'level', 'imported_gpa', 'imported_cgpa')
446    form_fields[
447        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
448
449    @property
450    def translated_values(self):
451        return translated_values(self)
452
453    @property
454    def show_results(self):
455        isStudent = getattr(
456            self.request.principal, 'user_type', None) == 'student'
457        try:
458            show_results = grok.getSite()[
459                'configuration'][str(self.context.level_session)].show_results
460        except KeyError:
461            return False
462        if isStudent and self.context.student.current_mode not in show_results:
463            return False
464        #if isStudent and self.context.student.state != RETURNING \
465        #    and self.context.student.current_level == self.context.level:
466        #    return False
467        return True
468
469class CustomStudyLevelManageFormPage(StudyLevelManageFormPage):
470    """ Page to edit the student study level data
471    """
472    grok.context(ICustomStudentStudyLevel)
473
474    form_fields = grok.AutoFields(ICustomStudentStudyLevel).omit(
475        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level',
476        'total_credits_s1', 'total_credits_s2')
477
478    form_fields['imported_gpa'].for_display = True
479    form_fields['imported_cgpa'].for_display = True
480
481class CustomStudyLevelEditFormPage(StudyLevelEditFormPage):
482    """ Page to edit the student study level data by students
483    """
484    grok.context(ICustomStudentStudyLevel)
485
486class StudyLevelRepairFormPage(KofaEditFormPage):
487    """ Page to repair the student study level data by students
488    """
489    grok.context(IStudentStudyLevel)
490    grok.name('repair')
491    grok.require('waeup.editStudyLevel')
492    grok.template('studylevelrepairpage')
493    pnav = 4
494    placeholder = _('Enter valid course code')
495
496    def update(self, ADD=None, course=None):
497        if not self.context.__parent__.is_current \
498            or self.context.student.studycourse_locked:
499            emit_lock_message(self)
500            return
501        try:
502            studylevel_repair_enabled = grok.getSite()['configuration'][
503                str(self.context.level_session)].studylevel_repair_enabled
504        except KeyError:
505            emit_lock_message(self)
506            return
507        if not studylevel_repair_enabled:
508            emit_lock_message(self)
509            return
510        super(StudyLevelRepairFormPage, self).update()
511        if ADD is not None:
512            if not course:
513                self.flash(_('No valid course code entered.'), type="warning")
514                return
515            cat = queryUtility(ICatalog, name='courses_catalog')
516            result = cat.searchResults(code=(course, course))
517            if len(result) != 1:
518                self.flash(_('Course not found.'), type="warning")
519                return
520            course = list(result)[0]
521            addCourseTicket(self, course)
522        return
523
524    @property
525    def label(self):
526        # Here we know that the cookie has been set
527        lang = self.request.cookies.get('kofa.language')
528        level_title = translate(self.context.level_title, 'waeup.kofa',
529            target_language=lang)
530        return _('Repair course list of ${a}',
531            mapping = {'a':level_title})
532
533    @property
534    def translated_values(self):
535        return translated_values(self)
536
537    def _delCourseTicket(self, **data):
538        form = self.request.form
539        if 'val_id' in form:
540            child_id = form['val_id']
541        else:
542            self.flash(_('No ticket selected.'), type="warning")
543            self.redirect(self.url(self.context, '@@edit'))
544            return
545        if not isinstance(child_id, list):
546            child_id = [child_id]
547        deleted = []
548        for id in child_id:
549            # Students are not allowed to remove core tickets
550            if id in self.context and \
551                self.context[id].removable_by_student:
552                del self.context[id]
553                deleted.append(id)
554        if len(deleted):
555            self.flash(_('Successfully removed: ${a}',
556                mapping = {'a':', '.join(deleted)}))
557            self.context.writeLogMessage(
558                self,'removed: %s at %s' %
559                (', '.join(deleted), self.context.level))
560        self.redirect(self.url(self.context, u'@@repair'))
561        return
562
563    @jsaction(_('Remove selected tickets'))
564    def delCourseTicket(self, **data):
565        self._delCourseTicket(**data)
566        return
567
568class CustomExportPDFCourseRegistrationSlip(
569    NigeriaExportPDFCourseRegistrationSlip):
570    """Deliver a PDF slip of the context.
571    """
572    grok.context(ICustomStudentStudyLevel)
573    form_fields = grok.AutoFields(ICustomStudentStudyLevel).omit(
574        'level_session', 'level_verdict',
575        'validated_by', 'validation_date', 'gpa', 'level',
576        'imported_gpa', 'imported_cgpa')
577
578    omit_fields = ('password', 'suspended', 'suspended_comment',
579        'phone', 'adm_code', 'sex', 'email', 'date_of_birth',
580        'department', 'current_mode', 'current_level', 'flash_notice',
581        'transcript_remark')
582
583    @property
584    def show_results(self):
585        isStudent = getattr(
586            self.request.principal, 'user_type', None) == 'student'
587        try:
588            show_results = grok.getSite()[
589                'configuration'][str(self.context.level_session)].show_results
590        except KeyError:
591            return False
592        if isStudent and self.context.student.current_mode not in show_results:
593            return False
594        if isStudent and self.context.student.state != RETURNING \
595            and self.context.student.current_level == self.context.level:
596            return False
597        return True
598
599    def update(self):
600        if self.context.student.state != REGISTERED \
601            and self.context.student.current_level == self.context.level:
602            self.flash(_('Forbidden'), type="warning")
603            self.redirect(self.url(self.context))
604            return
605
606    @property
607    def label(self):
608        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
609        lang = self.request.cookies.get('kofa.language', portal_language)
610        level_title = translate(self.context.level_title, 'waeup.kofa',
611            target_language=lang)
612        line0 = ''
613        if self.context.student.is_postgrad:
614            line0 = 'SCHOOL OF POSTGRADUATE STUDIES\n'
615        elif self.context.student.current_mode.endswith('_pt'):
616            line0 = 'DIRECTORATE OF PART-TIME DEGREE PROGRAMMES\n'
617        line1 = translate(_('Course Registration Slip'),
618            target_language=portal_language) \
619            + ' %s' % level_title
620        line2 = translate(_('Session'),
621            target_language=portal_language) \
622            + ' %s' % self.context.getSessionString
623        return '%s%s\n%s' % (line0, line1, line2)
624
625    @property
626    def title(self):
627        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
628        return translate(_('Units Registered'), target_language=portal_language)
629
630    def _signatures(self):
631        if self.context.student.current_mode.endswith('_pt') \
632            or self.context.student.current_mode == 'found':
633            return (
634                [('I have selected the course on the advise of my Head of '
635                 'Department. <br>', _('Student\'s Signature'), '<br>')],
636                [('This student has satisfied the department\'s requirements. '
637                 'I recommend to approve the course registration. <br>',
638                 _('Head of Department\'s Signature'), '<br>')],
639                [('' , _('Deputy Registrar\'s Signature'), '<br>')],
640                [('', _('Director\'s Signature'))]
641                )
642        if self.context.student.current_mode in (
643            'de_ft', 'ug_ft', 'dp_ft', 'transfer', 'bridge'):
644            return ([_('Academic Adviser\'s Signature'),
645                _('Faculty Officer\'s Signature'),
646                _('Student\'s Signature')],)
647
648        if self.context.student.current_mode in ('special_pg_ft', 'special_pg_pt'):
649            return (
650                [('I declare that all items of information supplied above are correct:' ,
651                    _('Student\'s Signature'), '<br>')],
652                [('We approved the above registration:',
653                    _('Major Supervisor (Name / Signature)'), '')],
654                [('', _('Co-Supervisor (Name / Signature)'), '')],
655                [('', _('Head of Department'), '<br>')],
656                [('The student has satisfied the conditions for renewal of '
657                  'registration for graduate school programme in this university:',
658                  _('Secretary <br /> (School of Postgraduate Studies)'), '')],
659                [('', _('Dean <br /> (School of Postgraduate Studies)'), '')],
660                )
661        return None
662
663
664    def render(self):
665        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
666        Sem = translate(_('Sem.'), target_language=portal_language)
667        Code = translate(_('Code'), target_language=portal_language)
668        Title = translate(_('Title'), target_language=portal_language)
669        Cred = translate(_('Cred.'), target_language=portal_language)
670        CC = translate(_('Cat.'), target_language=portal_language)
671        if self.show_results:
672            TotalScore = translate(_('Total Score'), target_language=portal_language)
673            #CA = translate(_('CA'), target_language=portal_language)
674            Grade = translate(_('Grade'), target_language=portal_language)
675        Signature = translate(_('Lecturer\'s Signature'), 'waeup.aaue',
676            target_language=portal_language)
677        studentview = StudentBasePDFFormPage(self.context.student,
678            self.request, self.omit_fields)
679        students_utils = getUtility(IStudentsUtils)
680
681        tabledata = []
682        tableheader = []
683        contenttitle = []
684        for i in range(1,7):
685            tabledata.append(sorted(
686                [value for value in self.context.values() if value.semester == i],
687                key=lambda value: str(value.semester) + value.code))
688            if self.show_results:
689                tableheader.append([(Code,'code', 2.0),
690                                   (Title,'title', 7),
691                                   (Cred, 'credits', 1.4),
692                                   (CC, 'course_category', 1.2),
693                                   (TotalScore, 'total_score', 1.4),
694                                   #(CA, 'ca', 1.4),
695                                   (Grade, 'grade', 1.4),
696                                   (Signature, 'dummy', 3),
697                                   ])
698            else:
699                tableheader.append([(Code,'code', 2.0),
700                                   (Title,'title', 7),
701                                   (Cred, 'credits', 1.5),
702                                   (CC, 'course_category', 1.2),
703                                   (Signature, 'dummy', 3),
704                                   ])
705        if len(self.label.split('\n')) == 3:
706            topMargin = 1.9
707        elif len(self.label.split('\n')) == 2:
708            topMargin = 1.7
709        else:
710            topMargin = 1.5
711        return students_utils.renderPDF(
712            self, 'course_registration_slip.pdf',
713            self.context.student, studentview,
714            tableheader=tableheader,
715            tabledata=tabledata,
716            signatures=self._signatures(),
717            topMargin=topMargin,
718            omit_fields=self.omit_fields
719            )
720
721class CustomStudyCourseTranscriptPage(StudyCourseTranscriptPage):
722    """ Page to display the student's transcript.
723    """
724    grok.require('waeup.viewStudent')
725
726class CustomExportPDFTranscriptSlip(ExportPDFTranscriptSlip):
727    """Deliver a PDF slip of the context.
728    """
729#    grok.require('waeup.viewStudent')
730
731    note = _("""
732<br /><br /><br /><br />
733<font size='10'>
734<strong>Note:</strong> This copy is subject to correction for typographical errors and ratification by the departmental board.
735</font>
736""")
737
738    def _sigsInFooter(self):
739        return []
740
741    def _signatures(self):
742        return ([(
743            'O.O OHIKHENA (Manupa)<br />Principal Asst. Registrar<br /> '
744            'Exams, Records and Data Processing Division <br /> For: Registrar')],)
745
746    def render(self):
747        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
748        Term = translate(_('Sem.'), target_language=portal_language)
749        Code = translate(_('Code'), target_language=portal_language)
750        Title = translate(_('Title'), target_language=portal_language)
751        Cred = translate(_('Credits'), target_language=portal_language)
752        Score = translate(_('Score'), target_language=portal_language)
753        Grade = translate(_('Grade'), target_language=portal_language)
754        studentview = StudentBasePDFFormPage(self.context.student,
755            self.request, self.omit_fields)
756        students_utils = getUtility(IStudentsUtils)
757
758        tableheader = [(Code,'code', 2.5),
759                         (Title,'title', 7),
760                         (Term, 'semester', 1.5),
761                         (Cred, 'credits', 1.5),
762                         (Score, 'total_score', 1.5),
763                         (Grade, 'grade', 1.5),
764                         ]
765
766        pdfstream = students_utils.renderPDFTranscript(
767            self, 'transcript.pdf',
768            self.context.student, studentview,
769            omit_fields=self.omit_fields,
770            tableheader=tableheader,
771            signatures=self._signatures(),
772            sigs_in_footer=self._sigsInFooter(),
773            digital_sigs=self._digital_sigs(),
774            save_file=self._save_file(),
775            )
776        if not pdfstream:
777            self.redirect(self.url(self.context.student))
778            return
779        return pdfstream
780
781class CustomExportPDFAdmissionSlip(ExportPDFAdmissionSlip):
782    """Deliver a PDF Admission slip.
783    """
784
785    @property
786    def label(self):
787        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
788        return translate(_('e-Admission Slip \n'),
789            target_language=portal_language) \
790            + ' %s' % self.context.display_fullname
791
792class CustomExportPDFClearanceSlip(NigeriaExportPDFClearanceSlip):
793    """Deliver a PDF slip of the context.
794    """
795
796    @property
797    def label(self):
798        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
799        return translate(_('Verification/Clearance Slip\n'),
800            target_language=portal_language) \
801            + ' %s' % self.context.display_fullname
802
803    @property
804    def form_fields(self):
805        if self.context.is_postgrad:
806            form_fields = grok.AutoFields(
807                ICustomPGStudentClearance).omit('clearance_locked')
808        else:
809            form_fields = grok.AutoFields(
810                ICustomUGStudentClearance).omit('clearance_locked')
811        if not getattr(self.context, 'officer_comment'):
812            form_fields = form_fields.omit('officer_comment')
813        form_fields = form_fields.omit('def_adm')
814        return form_fields
815
816class StudentGetMatricNumberPage(UtilityView, grok.View):
817    """ Construct and set the matriculation number.
818    """
819    grok.context(IStudent)
820    grok.name('get_matric_number')
821    grok.require('waeup.viewStudent')
822
823    def update(self):
824        students_utils = getUtility(IStudentsUtils)
825        msg, mnumber = students_utils.setMatricNumber(self.context)
826        if msg:
827            self.flash(msg, type="danger")
828        else:
829            self.flash(_('Matriculation number %s assigned.' % mnumber))
830            self.context.writeLogMessage(self, '%s assigned' % mnumber)
831        self.redirect(self.url(self.context))
832        return
833
834    def render(self):
835        return
836
837class ExportPDFMatricNumberSlip(UtilityView, grok.View):
838    """Deliver a PDF notification slip.
839    """
840    grok.context(ICustomStudent)
841    grok.name('matric_number_slip.pdf')
842    grok.require('waeup.viewStudent')
843    prefix = 'form'
844
845    form_fields = grok.AutoFields(ICustomStudent).select(
846        'student_id', 'matric_number')
847    omit_fields = ('date_of_birth', 'current_level', 'flash_notice')
848
849    @property
850    def title(self):
851        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
852        return translate(_('Matriculation Number'), 'waeup.kofa',
853            target_language=portal_language)
854
855    @property
856    def label(self):
857        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
858        return translate(_('Matriculation Number Slip\n'),
859            target_language=portal_language) \
860            + ' %s' % self.context.display_fullname
861
862    def render(self):
863        if self.context.state not in (PAID,) or not self.context.is_fresh \
864            or not self.context.matric_number:
865            self.flash('Not allowed.', type="danger")
866            self.redirect(self.url(self.context))
867            return
868        students_utils = getUtility(IStudentsUtils)
869        pre_text = _('Congratulations! Your acceptance fee and school fees ' +
870                     'payments have been received and your matriculation ' +
871                     'number generated with details as follows.')
872        return students_utils.renderPDFAdmissionLetter(self,
873            self.context.student, omit_fields=self.omit_fields,
874            pre_text=pre_text, post_text='')
875
876class ExportPersonalDataSlip(UtilityView, grok.View):
877    """Deliver a PDF notification slip.
878    """
879    grok.context(ICustomStudent)
880    grok.name('personal_data_slip.pdf')
881    grok.require('waeup.viewStudent')
882    prefix = 'form'
883    note = None
884
885    form_fields = grok.AutoFields(ICustomStudentPersonal).omit('personal_updated')
886    omit_fields = ('suspended', 'suspended_comment', 'adm_code',
887                   'certificate', 'flash_notice')
888
889    @property
890    def title(self):
891        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
892        return translate(_('Personal Data'), 'waeup.kofa',
893            target_language=portal_language)
894
895    @property
896    def label(self):
897        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
898        return translate(_('Personal Data Slip\n'),
899            target_language=portal_language) \
900            + ' %s' % self.context.display_fullname
901
902    def render(self):
903        studentview = StudentBasePDFFormPage(self.context.student,
904            self.request, self.omit_fields)
905        students_utils = getUtility(IStudentsUtils)
906        return students_utils.renderPDF(self, 'personal_data_slip.pdf',
907            self.context.student, studentview, note=self.note,
908            omit_fields=self.omit_fields)
909
910class CustomAccommodationManageFormPage(NigeriaAccommodationManageFormPage):
911    """ Page to manage bed tickets.
912    This manage form page is for both students and students officers.
913    """
914    with_hostel_selection = True
915
916class CustomBedTicketAddPage(BedTicketAddPage):
917    with_ac = False
918
919class CustomStudentFilesUploadPage(StudentFilesUploadPage):
920    """ View to upload files by student. Inherit from same class in
921    base package, not from kofacustom.nigeria which
922    requires that no application slip exists.
923    """
924
925class CustomCourseTicketDisplayFormPage(CourseTicketDisplayFormPage):
926    """ Page to display course tickets
927    """
928
929    @property
930    def show_results(self):
931        isStudent = getattr(
932            self.request.principal, 'user_type', None) == 'student'
933        if isStudent:
934            return False
935        return True
936
937    @property
938    def form_fields(self):
939        if self.show_results:
940            return grok.AutoFields(ICustomCourseTicket)
941        else:
942            return grok.AutoFields(ICustomCourseTicket).omit('score').omit('ca')
943
944class CustomCourseTicketManageFormPage(CourseTicketManageFormPage):
945    """ Page to manage course tickets
946    """
947    form_fields = grok.AutoFields(ICustomCourseTicket)
948    form_fields['title'].for_display = True
949    form_fields['fcode'].for_display = True
950    form_fields['dcode'].for_display = True
951    form_fields['semester'].for_display = True
952    form_fields['passmark'].for_display = True
953    form_fields['credits'].for_display = True
954    form_fields['mandatory'].for_display = False
955    form_fields['automatic'].for_display = True
956    form_fields['carry_over'].for_display = True
957    form_fields['ticket_session'].for_display = True
958    form_fields['imported_ts'].for_display = True
959
960class CustomEditScoresPage(EditScoresPage):
961    """Page that filters and lists students.
962    """
963    grok.template('editscorespage')
964
965    def _searchCatalog(self, session):
966        cat = queryUtility(ICatalog, name='coursetickets_catalog')
967        coursetickets = cat.searchResults(
968            session=(session, session),
969            code=(self.context.code, self.context.code)
970            )
971        try:
972            score_editing_enabled = grok.getSite()[
973                'configuration'][str(session)].score_editing_enabled
974        except KeyError:
975            return []
976        coursetickets_list = [courseticket for courseticket in coursetickets
977            if courseticket.student.current_mode in score_editing_enabled]
978        return coursetickets_list
979
980    def _extract_uploadfile(self, uploadfile):
981        """Get a mapping of student-ids to scores.
982
983        The mapping is constructed by reading contents from `uploadfile`.
984
985        We expect uploadfile to be a regular CSV file with columns
986        ``student_id``, ``score``, ``imported_ts``
987        and ``ca`` (other cols are ignored).
988        """
989        result = dict()
990        data = StringIO(uploadfile.read())  # ensure we have something seekable
991        reader = csv.DictReader(data)
992        for row in reader:
993            if not ('student_id' in row and 'score' in row and 'ca' in row and
994                'imported_ts' in row):
995                continue
996            result[row['student_id']] = (
997                row['score'], row['ca'], row['imported_ts'])
998        return result
999
1000    def _update_scores(self, form):
1001        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1002        error = ''
1003        if 'UPDATE_FILE' in form:
1004            if form['uploadfile']:
1005                try:
1006                    formvals = self._extract_uploadfile(form['uploadfile'])
1007                except:
1008                    self.flash(
1009                        _('Uploaded file contains illegal data. Ignored'),
1010                        type="danger")
1011                    return False
1012            else:
1013                self.flash(
1014                    _('No file provided.'), type="danger")
1015                return False
1016        else:
1017            formvals = dict(zip(form['sids'], zip(
1018                form['scores'], form['cas'], form['imported_tss'])))
1019        for ticket in self.editable_tickets:
1020            ticket_error = False
1021            score = ticket.score
1022            ca = ticket.ca
1023            imported_ts = ticket.imported_ts
1024            sid = ticket.student.student_id
1025            if formvals[sid][0] == '':
1026                score = None
1027            if formvals[sid][1] == '':
1028                ca = None
1029            if formvals[sid][2] == '':
1030                imported_ts = None
1031            try:
1032                if formvals[sid][0]:
1033                    score = int(formvals[sid][0])
1034                if formvals[sid][1]:
1035                    ca = int(formvals[sid][1])
1036                if formvals[sid][2]:
1037                    imported_ts = int(formvals[sid][2])
1038            except ValueError:
1039                error += '%s, ' % ticket.student.display_fullname
1040                ticket_error = True
1041            if not ticket_error and ticket.score != score:
1042                try:
1043                    ticket.score = score
1044                except TooBig:
1045                    error += '%s, ' % ticket.student.display_fullname
1046                    ticket_error = True
1047                    pass
1048                ticket.student.__parent__.logger.info(
1049                    '%s - %s %s/%s score updated (%s)' %
1050                    (ob_class, ticket.student.student_id,
1051                     ticket.level, ticket.code, score))
1052            if not ticket_error and ticket.ca != ca:
1053                try:
1054                    ticket.ca = ca
1055                except TooBig:
1056                    error += '%s, ' % ticket.student.display_fullname
1057                    pass
1058                ticket.student.__parent__.logger.info(
1059                    '%s - %s %s/%s ca updated (%s)' %
1060                    (ob_class, ticket.student.student_id,
1061                     ticket.level, ticket.code, ca))
1062            if not ticket_error and ticket.imported_ts != imported_ts:
1063                try:
1064                    ticket.imported_ts = imported_ts
1065                except TooBig:
1066                    error += '%s, ' % ticket.student.display_fullname
1067                    pass
1068                ticket.student.__parent__.logger.info(
1069                    '%s - %s %s/%s imported_ts updated (%s)' %
1070                    (ob_class, ticket.student.student_id,
1071                     ticket.level, ticket.code, imported_ts))
1072        if error:
1073            self.flash(_('Error: Score(s), CA(s) and Imported TS(s) of %s have not be updated. '
1074              % error.strip(', ')), type="danger")
1075        return True
1076
1077class EditPreviousSessionScoresPage(CustomEditScoresPage):
1078
1079    grok.name('edit_prev_scores')
1080
1081    def update(self,  *args, **kw):
1082        form = self.request.form
1083        self.current_academic_session = grok.getSite()[
1084            'configuration'].current_academic_session
1085        if self.context.__parent__.__parent__.score_editing_disabled:
1086            self.flash(_('Score editing disabled.'), type="warning")
1087            self.redirect(self.url(self.context))
1088            return
1089        if not self.current_academic_session:
1090            self.flash(_('Current academic session not set.'), type="warning")
1091            self.redirect(self.url(self.context))
1092            return
1093        previous_session = self.current_academic_session - 1
1094        self.session_title = academic_sessions_vocab.getTerm(
1095            previous_session).title
1096        self.tickets = self._searchCatalog(previous_session)
1097        if not self.tickets:
1098            self.flash(_('No student found.'), type="warning")
1099            self.redirect(self.url(self.context))
1100            return
1101        self.editable_tickets = [
1102            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
1103        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
1104            return
1105        if not self.editable_tickets:
1106            return
1107        success = self._update_scores(form)
1108        if success:
1109            self.flash(_('You successfully updated course results.'))
1110        return
1111
1112class CustomExportPDFScoresSlip(ExportPDFScoresSlip):
1113    """Deliver a PDF slip of course tickets for a lecturer.
1114    """
1115
1116    note = u'\nUpgraded scores are with asterisks.'
1117
1118    def data(self, session):
1119        #site = grok.getSite()
1120        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1121        coursetickets = cat.searchResults(
1122            session=(session, session),
1123            code=(self.context.code, self.context.code)
1124            )
1125        # Apply filter
1126        try:
1127            score_editing_enabled = grok.getSite()[
1128                'configuration'][str(session)].score_editing_enabled
1129            if checkPermission('waeup.manageAcademics', self.context):
1130                score_editing_enabled = True
1131            coursetickets_filtered = [courseticket
1132                for courseticket in coursetickets
1133                if (checkPermission('waeup.manageAcademics', self.context)
1134                    or (courseticket.student.current_mode in
1135                        score_editing_enabled))
1136                and courseticket.total_score is not None
1137                and courseticket.__parent__.__parent__.is_current]
1138        except KeyError:
1139            coursetickets_filtered = coursetickets
1140        # In AAUE only editable tickets can be printed
1141        editable_tickets = [
1142            ticket for ticket in coursetickets_filtered
1143            if ticket.editable_by_lecturer]
1144        header = [[_(''),
1145                   _('Student Id'),
1146                   _('Matric No.'),
1147                   #_('Reg. No.'),
1148                   #_('Fullname'),
1149                   #_('Status'),
1150                   #_('Course of\nStudies'),
1151                   _('Department'),
1152                   _('Level'),
1153                   _(' CA  '),
1154                   _('Exam\nScore'),
1155                   _('Total '),
1156                   _('Grade'),
1157                   ],]
1158        sorted_tickets = sorted(editable_tickets,
1159            key=lambda ticket: ticket.student.depcode
1160                               + ticket.student.faccode
1161                               + ticket.student.matric_number)
1162        no = 1
1163        tickets = []
1164        passed = 0
1165        failed = 0
1166        with_ca = False
1167        # In AAUE only editable tickets can be printed
1168        for ticket in sorted_tickets:
1169            if ticket.ca > 0:
1170                with_ca = True
1171            total = ticket.total_score
1172            if getattr(ticket, 'imported_ts', None):
1173                total = "**%s**" % ticket.imported_ts
1174            grade = ticket._getGradeWeightFromScore[0]
1175            if grade in ('F', '-'):
1176                failed += 1
1177            else:
1178                passed += 1
1179            fullname = textwrap.fill(ticket.student.display_fullname, 30)
1180            #deptitle = site['faculties'][ticket.student.faccode][
1181            #    ticket.student.depcode].longtitle
1182            row = [str(no),
1183                  ticket.student.student_id,
1184                  ticket.student.matric_number,
1185                  #ticket.student.reg_number,
1186                  #fullname,
1187                  #ticket.student.translated_state,
1188                  #ticket.student.certcode,
1189                  ticket.student.faccode + ' / ' + ticket.student.depcode,
1190                  ticket.level,
1191                  ticket.ca,
1192                  ticket.score,
1193                  total,
1194                  grade,
1195                  ]
1196            tickets.append(row)
1197            no += 1
1198        total = passed + failed
1199        passed_perc = 0
1200        failed_perc = 0
1201        if total:
1202            passed_perc = round(100.0 * passed / total)
1203            failed_perc = round(100.0 * failed / total)
1204        dep = self.context.__parent__.__parent__.longtitle
1205        fac = self.context.__parent__.__parent__.__parent__.longtitle
1206        # remove CA column if not necessary
1207        if not with_ca:
1208            header = [[_(''),
1209                       _('Student Id'),
1210                       _('Matric No.'),
1211                       #_('Reg. No.'),
1212                       #_('Fullname'),
1213                       #_('Status'),
1214                       #_('Course of\nStudies'),
1215                       _('Department'),
1216                       _('Level'),
1217                       #_(' CA  '),
1218                       _('Exam\nScore'),
1219                       _('Total '),
1220                       _('Grade'),
1221                       ],]
1222            for ticket in tickets:
1223                del(ticket[5])
1224        return header + tickets, [
1225            dep, fac, total, passed, passed_perc, failed, failed_perc]
1226
1227    def render(self):
1228        session = grok.getSite()['configuration'].current_academic_session
1229        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
1230                     if i['local_role'] == 'waeup.local.Lecturer']
1231        lecturers =  ', '.join(lecturers)
1232        students_utils = getUtility(IStudentsUtils)
1233        # only orientation is different
1234        return students_utils.renderPDFCourseticketsOverview(
1235            self, 'coursetickets',
1236            session, self.data(session), lecturers, '', 45, self.note)
1237
1238class DownloadPreviousSessionScoresView(DownloadScoresView):
1239    """View that exports scores.
1240    """
1241    grok.name('download_prev_scores')
1242
1243    def update(self):
1244        self.current_academic_session = grok.getSite()[
1245            'configuration'].current_academic_session
1246        if self.context.__parent__.__parent__.score_editing_disabled:
1247            self.flash(_('Score editing disabled.'), type="warning")
1248            self.redirect(self.url(self.context))
1249            return
1250        if not self.current_academic_session:
1251            self.flash(_('Current academic session not set.'), type="warning")
1252            self.redirect(self.url(self.context))
1253            return
1254        site = grok.getSite()
1255        exporter = getUtility(ICSVExporter, name='lecturer')
1256        self.csv = exporter.export_filtered(site, filepath=None,
1257                                 catalog='coursetickets',
1258                                 session=self.current_academic_session-1,
1259                                 level=None,
1260                                 code=self.context.code)
1261        return
1262
1263class AlumniRequestPasswordPage(StudentRequestPasswordPage):
1264    """Captcha'd request password page for students.
1265    """
1266    grok.name('alumni_requestpw')
1267    grok.require('waeup.Anonymous')
1268    grok.template('alumni_requestpw')
1269    form_fields = grok.AutoFields(IStudentRequestPW).select(
1270        'lastname','number','email')
1271    label = _('Search student record and send password for first-time login')
1272
1273    def _redirect_no_student(self):
1274        self.flash(_('No student record found.'), type="warning")
1275        self.redirect(self.application_url() + '/applicants/trans2017/register')
1276        return
Note: See TracBrowser for help on using the repository browser.