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

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

Implement course list repair page.

  • Property svn:keywords set to Id
File size: 50.2 KB
Line 
1## $Id: browser.py 15330 2019-02-14 15:55:27Z 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 CustomOnlinePaymentAddFormPage(NigeriaOnlinePaymentAddFormPage):
312    """ Page to add an online payment ticket
313    """
314    form_fields = grok.AutoFields(ICustomStudentOnlinePayment).select(
315        'p_category')
316
317    ALUMNI_PAYMENT_CATS =  {
318        'transcript_local': 'Transcript Fee Local',
319        'transcript_inter': 'Transcript Fee International',
320        }
321
322    REDUCED_PAYMENT_CATS =  {
323        'clearance': 'Acceptance Fee',
324        'schoolfee': 'School Fee',
325        }
326
327    IJMBE_PAYMENT_CATS =  {
328        'clearance': 'Acceptance Fee',
329        'schoolfee': 'School Fee',
330        'schoolfee_1': 'School Fee (1st instalment)',
331        'schoolfee_2': 'School Fee (2nd instalment)',
332        }
333
334    @property
335    def selectable_categories(self):
336        if 'alumni' in self.application_url():
337            return self.ALUMNI_PAYMENT_CATS.items()
338        if self.context.student.current_mode in (
339            'special_pg_ft', 'special_pg_pt', 'found'):
340            return self.REDUCED_PAYMENT_CATS.items()
341        if self.context.student.current_mode == 'ijmbe':
342            return sorted(self.IJMBE_PAYMENT_CATS.items())
343        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
344        return sorted(categories.items())
345
346class CustomPaymentsManageFormPage(PaymentsManageFormPage):
347    """ Page to manage the student payments.
348
349    This manage form page is for both students and students officers.
350    """
351    @property
352    def manage_payments_allowed(self):
353        return checkPermission('waeup.manageStudent', self.context)
354
355class CustomExportPDFPaymentSlip(NigeriaExportPDFPaymentSlip):
356    """Deliver a PDF slip of the context.
357    """
358    grok.context(ICustomStudentOnlinePayment)
359    form_fields = grok.AutoFields(ICustomStudentOnlinePayment).omit(
360        'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item')
361    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
362    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
363
364    @property
365    def note(self):
366        p_session = self.context.p_session
367        try:
368            academic_session = grok.getSite()['configuration'][str(p_session)]
369        except KeyError:
370            academic_session = None
371        text =  '\n\n The Amount Authorized is inclusive of: '
372        if self.context.p_category in ('schoolfee_incl', 'schoolfee_1') \
373            and academic_session:
374            #welfare_fee = gateway_net_amt(academic_session.welfare_fee)
375            #union_fee = gateway_net_amt(academic_session.union_fee)
376            if self.context.student.entry_session == 2016 \
377                and self.context.student.entry_mode == 'ug_ft' \
378                and self.context.p_session == 2016:
379                # Add student id card fee to first school fee payment.
380
381                ## Attention: The payment slip does not contain any information
382                ## whether the fee was added or not.
383                ## We can only draw conclusions from from the student's entry
384                ## session whether the fee had been included.
385                #id_card_fee = gateway_net_amt(academic_session.id_card_fee)
386                #text += ('School Fee, '
387                #         '%s Naira Student ID Card Fee, '
388                #         '%s Naira Student Union Dues, '
389                #         '%s Naira Student Welfare Assurance Fee and '
390                #         % (id_card_fee, union_fee, welfare_fee))
391
392                text += ('School Fee, '
393                         'Student ID Card Fee, '
394                         'Student Union Dues, '
395                         'Sports Fee, '
396                         'Library Levy, '
397                         'Student Welfare Assurance Fee and ')
398            else:
399
400                #text += ('School Fee, '
401                #         '%s Naira Student Union Dues, '
402                #         '%s Naira Student Welfare Assurance Fee and '
403                #         % (union_fee, welfare_fee))
404
405                text += ('School Fee, '
406                         'Student Union Dues, '
407                         'Sports Development Fee, '
408                         'Library Development Levy, '
409                         'Student Welfare Assurance Fee and ')
410        elif self.context.p_category in (
411            'clearance_incl', 'clearance_medical_incl') and academic_session:
412
413            #matric_gown_fee = gateway_net_amt(academic_session.matric_gown_fee)
414            #lapel_fee = gateway_net_amt(academic_session.lapel_fee)
415            #text += ('Acceptance Fee, '
416            #         '%s Naira Matriculation Gown Fee, '
417            #         '%s Naira Lapel/File Fee and '
418            #         % (matric_gown_fee, lapel_fee))
419
420            text += ('Acceptance Fee, '
421                     'Matriculation Gown Fee, '
422                     'Lapel/File Fee and ')
423
424        #return text + '250.0 Naira Transaction Charge.'
425
426        return text + 'Transaction Charge.'
427
428class CustomStudyCourseManageFormPage(StudyCourseManageFormPage):
429    """ Page to edit the student study course data
430    """
431    grok.context(ICustomStudentStudyCourse)
432
433    @property
434    def form_fields(self):
435        if self.context.is_postgrad:
436            form_fields = grok.AutoFields(ICustomStudentStudyCourse).omit(
437                'previous_verdict')
438        else:
439            form_fields = grok.AutoFields(ICustomStudentStudyCourse)
440        form_fields['imported_cgpa'].for_display = True
441        return form_fields
442
443class CustomUnregisterCoursesView(UnregisterCoursesView):
444    """Unregister course list by student
445    """
446    grok.context(ICustomStudentStudyLevel)
447
448    def update(self):
449        if not self.context.__parent__.is_current:
450            emit_lock_message(self)
451            return
452        try:
453            deadline = grok.getSite()['configuration'][
454                str(self.context.level_session)].coursereg_deadline
455        except (TypeError, KeyError):
456            deadline = None
457        # In AAUE fresh students are allowed to "unregister their course"
458        # aside the deadline
459        if deadline and not self.context.student.is_fresh \
460            and deadline < datetime.now(pytz.utc):
461            self.flash(_(
462                "Course registration has ended. "
463                "Unregistration is disabled."), type="warning")
464        elif str(self.context.__parent__.current_level) != self.context.__name__:
465            self.flash(_('This is not your current level.'), type="danger")
466        elif self.context.student.state == REGISTERED:
467            IWorkflowInfo(self.context.student).fireTransition('reset7')
468            message = _('Course list has been unregistered.')
469            self.flash(message)
470        else:
471            self.flash(_('You are in the wrong state.'), type="warning")
472        self.redirect(self.url(self.context))
473        return
474
475class CustomStudyLevelDisplayFormPage(StudyLevelDisplayFormPage):
476    """ Page to display student study levels
477    """
478    grok.context(ICustomStudentStudyLevel)
479    form_fields = grok.AutoFields(ICustomStudentStudyLevel).omit(
480        'total_credits', 'gpa', 'level', 'imported_gpa', 'imported_cgpa')
481    form_fields[
482        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
483
484    @property
485    def translated_values(self):
486        return translated_values(self)
487
488    @property
489    def show_results(self):
490        isStudent = getattr(
491            self.request.principal, 'user_type', None) == 'student'
492        try:
493            show_results = grok.getSite()[
494                'configuration'][str(self.context.level_session)].show_results
495        except KeyError:
496            return False
497        if isStudent and self.context.student.current_mode not in show_results:
498            return False
499        if isStudent and self.context.student.state != RETURNING \
500            and self.context.student.current_level == self.context.level:
501            return False
502        return True
503
504class CustomStudyLevelManageFormPage(StudyLevelManageFormPage):
505    """ Page to edit the student study level data
506    """
507    grok.context(ICustomStudentStudyLevel)
508
509    form_fields = grok.AutoFields(ICustomStudentStudyLevel).omit(
510        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level',
511        'total_credits_s1', 'total_credits_s2')
512
513    form_fields['imported_gpa'].for_display = True
514    form_fields['imported_cgpa'].for_display = True
515
516class CustomStudyLevelEditFormPage(StudyLevelEditFormPage):
517    """ Page to edit the student study level data by students
518    """
519    grok.context(ICustomStudentStudyLevel)
520
521class StudyLevelRepairFormPage(KofaEditFormPage):
522    """ Page to repair the student study level data by students
523    """
524    grok.context(IStudentStudyLevel)
525    grok.name('repair')
526    grok.require('waeup.editStudyLevel')
527    grok.template('studylevelrepairpage')
528    pnav = 4
529    placeholder = _('Enter valid course code')
530
531    def update(self, ADD=None, course=None):
532        try:
533            studylevel_repair_enabled = grok.getSite()['configuration'][
534                str(self.context.level_session)].studylevel_repair_enabled
535        except KeyError:
536            emit_lock_message(self)
537            return
538        if not studylevel_repair_enabled:
539            emit_lock_message(self)
540            return
541        super(StudyLevelRepairFormPage, self).update()
542        if ADD is not None:
543            if not course:
544                self.flash(_('No valid course code entered.'), type="warning")
545                return
546            cat = queryUtility(ICatalog, name='courses_catalog')
547            result = cat.searchResults(code=(course, course))
548            if len(result) != 1:
549                self.flash(_('Course not found.'), type="warning")
550                return
551            course = list(result)[0]
552            addCourseTicket(self, course)
553        return
554
555    @property
556    def label(self):
557        # Here we know that the cookie has been set
558        lang = self.request.cookies.get('kofa.language')
559        level_title = translate(self.context.level_title, 'waeup.kofa',
560            target_language=lang)
561        return _('Repair course list of ${a}',
562            mapping = {'a':level_title})
563
564    @property
565    def translated_values(self):
566        return translated_values(self)
567
568    def _delCourseTicket(self, **data):
569        form = self.request.form
570        if 'val_id' in form:
571            child_id = form['val_id']
572        else:
573            self.flash(_('No ticket selected.'), type="warning")
574            self.redirect(self.url(self.context, '@@edit'))
575            return
576        if not isinstance(child_id, list):
577            child_id = [child_id]
578        deleted = []
579        for id in child_id:
580            # Students are not allowed to remove core tickets
581            if id in self.context and \
582                self.context[id].removable_by_student:
583                del self.context[id]
584                deleted.append(id)
585        if len(deleted):
586            self.flash(_('Successfully removed: ${a}',
587                mapping = {'a':', '.join(deleted)}))
588            self.context.writeLogMessage(
589                self,'removed: %s at %s' %
590                (', '.join(deleted), self.context.level))
591        self.redirect(self.url(self.context, u'@@repair'))
592        return
593
594    @jsaction(_('Remove selected tickets'))
595    def delCourseTicket(self, **data):
596        self._delCourseTicket(**data)
597        return
598
599class CustomExportPDFCourseRegistrationSlip(
600    NigeriaExportPDFCourseRegistrationSlip):
601    """Deliver a PDF slip of the context.
602    """
603    grok.context(ICustomStudentStudyLevel)
604    form_fields = grok.AutoFields(ICustomStudentStudyLevel).omit(
605        'level_session', 'level_verdict',
606        'validated_by', 'validation_date', 'gpa', 'level',
607        'imported_gpa', 'imported_cgpa')
608
609    omit_fields = ('password', 'suspended', 'suspended_comment',
610        'phone', 'adm_code', 'sex', 'email', 'date_of_birth',
611        'department', 'current_mode', 'current_level', 'flash_notice')
612
613    @property
614    def show_results(self):
615        isStudent = getattr(
616            self.request.principal, 'user_type', None) == 'student'
617        try:
618            show_results = grok.getSite()[
619                'configuration'][str(self.context.level_session)].show_results
620        except KeyError:
621            return False
622        if isStudent and self.context.student.current_mode not in show_results:
623            return False
624        if isStudent and self.context.student.state != RETURNING \
625            and self.context.student.current_level == self.context.level:
626            return False
627        return True
628
629    def update(self):
630        if self.context.student.state != REGISTERED \
631            and self.context.student.current_level == self.context.level:
632            self.flash(_('Forbidden'), type="warning")
633            self.redirect(self.url(self.context))
634            return
635
636    @property
637    def label(self):
638        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
639        lang = self.request.cookies.get('kofa.language', portal_language)
640        level_title = translate(self.context.level_title, 'waeup.kofa',
641            target_language=lang)
642        line0 = ''
643        if self.context.student.is_postgrad:
644            line0 = 'SCHOOL OF POSTGRADUATE STUDIES\n'
645        elif self.context.student.current_mode.endswith('_pt'):
646            line0 = 'DIRECTORATE OF PART-TIME DEGREE PROGRAMMES\n'
647        line1 = translate(_('Course Registration Slip'),
648            target_language=portal_language) \
649            + ' %s' % level_title
650        line2 = translate(_('Session'),
651            target_language=portal_language) \
652            + ' %s' % self.context.getSessionString
653        return '%s%s\n%s' % (line0, line1, line2)
654
655    @property
656    def title(self):
657        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
658        return translate(_('Units Registered'), target_language=portal_language)
659
660    def _signatures(self):
661        if self.context.student.current_mode.endswith('_pt') \
662            or self.context.student.current_mode == 'found':
663            return (
664                [('I have selected the course on the advise of my Head of '
665                 'Department. <br>', _('Student\'s Signature'), '<br>')],
666                [('This student has satisfied the department\'s requirements. '
667                 'I recommend to approve the course registration. <br>',
668                 _('Head of Department\'s Signature'), '<br>')],
669                [('' , _('Deputy Registrar\'s Signature'), '<br>')],
670                [('', _('Director\'s Signature'))]
671                )
672        if self.context.student.current_mode in (
673            'de_ft', 'ug_ft', 'dp_ft', 'transfer'):
674            return ([_('Academic Adviser\'s Signature'),
675                _('Faculty Officer\'s Signature'),
676                _('Student\'s Signature')],)
677
678        if self.context.student.current_mode in ('special_pg_ft', 'special_pg_pt'):
679            return (
680                [('I declare that all items of information supplied above are correct:' ,
681                    _('Student\'s Signature'), '<br>')],
682                [('We approved the above registration:',
683                    _('Major Supervisor (Name / Signature)'), '')],
684                [('', _('Co-Supervisor (Name / Signature)'), '')],
685                [('', _('Head of Department'), '<br>')],
686                [('The student has satisfied the conditions for renewal of '
687                  'registration for graduate school programme in this university:',
688                  _('Secretary <br /> (School of Postgraduate Studies)'), '')],
689                [('', _('Dean <br /> (School of Postgraduate Studies)'), '')],
690                )
691        return None
692
693
694    def render(self):
695        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
696        Sem = translate(_('Sem.'), target_language=portal_language)
697        Code = translate(_('Code'), target_language=portal_language)
698        Title = translate(_('Title'), target_language=portal_language)
699        Cred = translate(_('Cred.'), target_language=portal_language)
700        CC = translate(_('Cat.'), target_language=portal_language)
701        if self.show_results:
702            TotalScore = translate(_('Total Score'), target_language=portal_language)
703            #CA = translate(_('CA'), target_language=portal_language)
704            Grade = translate(_('Grade'), target_language=portal_language)
705        Signature = translate(_('Lecturer\'s Signature'), 'waeup.aaue',
706            target_language=portal_language)
707        studentview = StudentBasePDFFormPage(self.context.student,
708            self.request, self.omit_fields)
709        students_utils = getUtility(IStudentsUtils)
710
711        tabledata = []
712        tableheader = []
713        contenttitle = []
714        for i in range(1,7):
715            tabledata.append(sorted(
716                [value for value in self.context.values() if value.semester == i],
717                key=lambda value: str(value.semester) + value.code))
718            if self.show_results:
719                tableheader.append([(Code,'code', 2.0),
720                                   (Title,'title', 7),
721                                   (Cred, 'credits', 1.4),
722                                   (CC, 'course_category', 1.2),
723                                   (TotalScore, 'total_score', 1.4),
724                                   #(CA, 'ca', 1.4),
725                                   (Grade, 'grade', 1.4),
726                                   (Signature, 'dummy', 3),
727                                   ])
728            else:
729                tableheader.append([(Code,'code', 2.0),
730                                   (Title,'title', 7),
731                                   (Cred, 'credits', 1.5),
732                                   (CC, 'course_category', 1.2),
733                                   (Signature, 'dummy', 3),
734                                   ])
735        if len(self.label.split('\n')) == 3:
736            topMargin = 1.9
737        elif len(self.label.split('\n')) == 2:
738            topMargin = 1.7
739        else:
740            topMargin = 1.5
741        return students_utils.renderPDF(
742            self, 'course_registration_slip.pdf',
743            self.context.student, studentview,
744            tableheader=tableheader,
745            tabledata=tabledata,
746            signatures=self._signatures(),
747            topMargin=topMargin,
748            omit_fields=self.omit_fields
749            )
750
751class CustomStudyCourseTranscriptPage(StudyCourseTranscriptPage):
752    """ Page to display the student's transcript.
753    """
754    grok.require('waeup.viewStudent')
755
756class CustomExportPDFTranscriptSlip(ExportPDFTranscriptSlip):
757    """Deliver a PDF slip of the context.
758    """
759#    grok.require('waeup.viewStudent')
760
761    note = _("""
762<br /><br /><br /><br />
763<font size='10'>
764<strong>Note:</strong> This copy is subject to correction for typographical errors and ratification by the departmental board.
765</font>
766""")
767
768    def _sigsInFooter(self):
769        return []
770
771    def _signatures(self):
772        return ([(
773            'O.O OHIKHENA (Manupa)<br />Principal Asst. Registrar<br /> '
774            'Exams, Records and Data Processing Division <br /> For: Registrar')],)
775
776    def render(self):
777        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
778        Term = translate(_('Sem.'), target_language=portal_language)
779        Code = translate(_('Code'), target_language=portal_language)
780        Title = translate(_('Title'), target_language=portal_language)
781        Cred = translate(_('Credits'), target_language=portal_language)
782        Score = translate(_('Score'), target_language=portal_language)
783        Grade = translate(_('Grade'), target_language=portal_language)
784        studentview = StudentBasePDFFormPage(self.context.student,
785            self.request, self.omit_fields)
786        students_utils = getUtility(IStudentsUtils)
787
788        tableheader = [(Code,'code', 2.5),
789                         (Title,'title', 7),
790                         (Term, 'semester', 1.5),
791                         (Cred, 'credits', 1.5),
792                         (Score, 'total_score', 1.5),
793                         (Grade, 'grade', 1.5),
794                         ]
795
796        pdfstream = students_utils.renderPDFTranscript(
797            self, 'transcript.pdf',
798            self.context.student, studentview,
799            omit_fields=self.omit_fields,
800            tableheader=tableheader,
801            signatures=self._signatures(),
802            sigs_in_footer=self._sigsInFooter(),
803            digital_sigs=self._digital_sigs(),
804            save_file=self._save_file(),
805            )
806        if not pdfstream:
807            self.redirect(self.url(self.context.student))
808            return
809        return pdfstream
810
811class CustomExportPDFAdmissionSlip(ExportPDFAdmissionSlip):
812    """Deliver a PDF Admission slip.
813    """
814
815    @property
816    def label(self):
817        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
818        return translate(_('e-Admission Slip \n'),
819            target_language=portal_language) \
820            + ' %s' % self.context.display_fullname
821
822class CustomExportPDFClearanceSlip(NigeriaExportPDFClearanceSlip):
823    """Deliver a PDF slip of the context.
824    """
825
826    @property
827    def label(self):
828        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
829        return translate(_('Verification/Clearance Slip\n'),
830            target_language=portal_language) \
831            + ' %s' % self.context.display_fullname
832
833    @property
834    def form_fields(self):
835        if self.context.is_postgrad:
836            form_fields = grok.AutoFields(
837                ICustomPGStudentClearance).omit('clearance_locked')
838        else:
839            form_fields = grok.AutoFields(
840                ICustomUGStudentClearance).omit('clearance_locked')
841        if not getattr(self.context, 'officer_comment'):
842            form_fields = form_fields.omit('officer_comment')
843        form_fields = form_fields.omit('def_adm')
844        return form_fields
845
846class StudentGetMatricNumberPage(UtilityView, grok.View):
847    """ Construct and set the matriculation number.
848    """
849    grok.context(IStudent)
850    grok.name('get_matric_number')
851    grok.require('waeup.viewStudent')
852
853    def update(self):
854        students_utils = getUtility(IStudentsUtils)
855        msg, mnumber = students_utils.setMatricNumber(self.context)
856        if msg:
857            self.flash(msg, type="danger")
858        else:
859            self.flash(_('Matriculation number %s assigned.' % mnumber))
860            self.context.writeLogMessage(self, '%s assigned' % mnumber)
861        self.redirect(self.url(self.context))
862        return
863
864    def render(self):
865        return
866
867class ExportPDFMatricNumberSlip(UtilityView, grok.View):
868    """Deliver a PDF notification slip.
869    """
870    grok.context(ICustomStudent)
871    grok.name('matric_number_slip.pdf')
872    grok.require('waeup.viewStudent')
873    prefix = 'form'
874
875    form_fields = grok.AutoFields(ICustomStudent).select(
876        'student_id', 'matric_number')
877    omit_fields = ('date_of_birth', 'current_level', 'flash_notice')
878
879    @property
880    def title(self):
881        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
882        return translate(_('Matriculation Number'), 'waeup.kofa',
883            target_language=portal_language)
884
885    @property
886    def label(self):
887        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
888        return translate(_('Matriculation Number Slip\n'),
889            target_language=portal_language) \
890            + ' %s' % self.context.display_fullname
891
892    def render(self):
893        if self.context.state not in (PAID,) or not self.context.is_fresh \
894            or not self.context.matric_number:
895            self.flash('Not allowed.', type="danger")
896            self.redirect(self.url(self.context))
897            return
898        students_utils = getUtility(IStudentsUtils)
899        pre_text = _('Congratulations! Your acceptance fee and school fees ' +
900                     'payments have been received and your matriculation ' +
901                     'number generated with details as follows.')
902        return students_utils.renderPDFAdmissionLetter(self,
903            self.context.student, omit_fields=self.omit_fields,
904            pre_text=pre_text, post_text='')
905
906class ExportPersonalDataSlip(UtilityView, grok.View):
907    """Deliver a PDF notification slip.
908    """
909    grok.context(ICustomStudent)
910    grok.name('personal_data_slip.pdf')
911    grok.require('waeup.viewStudent')
912    prefix = 'form'
913    note = None
914
915    form_fields = grok.AutoFields(ICustomStudentPersonal).omit('personal_updated')
916    omit_fields = ('suspended', 'suspended_comment', 'adm_code',
917                   'certificate', 'flash_notice')
918
919    @property
920    def title(self):
921        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
922        return translate(_('Personal Data'), 'waeup.kofa',
923            target_language=portal_language)
924
925    @property
926    def label(self):
927        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
928        return translate(_('Personal Data Slip\n'),
929            target_language=portal_language) \
930            + ' %s' % self.context.display_fullname
931
932    def render(self):
933        studentview = StudentBasePDFFormPage(self.context.student,
934            self.request, self.omit_fields)
935        students_utils = getUtility(IStudentsUtils)
936        return students_utils.renderPDF(self, 'personal_data_slip.pdf',
937            self.context.student, studentview, note=self.note,
938            omit_fields=self.omit_fields)
939
940class CustomAccommodationManageFormPage(NigeriaAccommodationManageFormPage):
941    """ Page to manage bed tickets.
942    This manage form page is for both students and students officers.
943    """
944    with_hostel_selection = True
945
946class CustomBedTicketAddPage(BedTicketAddPage):
947    with_ac = False
948
949class CustomStudentFilesUploadPage(StudentFilesUploadPage):
950    """ View to upload files by student. Inherit from same class in
951    base package, not from kofacustom.nigeria which
952    requires that no application slip exists.
953    """
954
955class CustomCourseTicketDisplayFormPage(CourseTicketDisplayFormPage):
956    """ Page to display course tickets
957    """
958
959    @property
960    def show_results(self):
961        isStudent = getattr(
962            self.request.principal, 'user_type', None) == 'student'
963        if isStudent:
964            return False
965        return True
966
967    @property
968    def form_fields(self):
969        if self.show_results:
970            return grok.AutoFields(ICustomCourseTicket)
971        else:
972            return grok.AutoFields(ICustomCourseTicket).omit('score').omit('ca')
973
974class CustomCourseTicketManageFormPage(CourseTicketManageFormPage):
975    """ Page to manage course tickets
976    """
977    form_fields = grok.AutoFields(ICustomCourseTicket)
978    form_fields['title'].for_display = True
979    form_fields['fcode'].for_display = True
980    form_fields['dcode'].for_display = True
981    form_fields['semester'].for_display = True
982    form_fields['passmark'].for_display = True
983    form_fields['credits'].for_display = True
984    form_fields['mandatory'].for_display = False
985    form_fields['automatic'].for_display = True
986    form_fields['carry_over'].for_display = True
987    form_fields['ticket_session'].for_display = True
988    form_fields['imported_ts'].for_display = True
989
990class CustomEditScoresPage(EditScoresPage):
991    """Page that filters and lists students.
992    """
993    grok.template('editscorespage')
994
995    def _searchCatalog(self, session):
996        cat = queryUtility(ICatalog, name='coursetickets_catalog')
997        coursetickets = cat.searchResults(
998            session=(session, session),
999            code=(self.context.code, self.context.code)
1000            )
1001        try:
1002            score_editing_enabled = grok.getSite()[
1003                'configuration'][str(session)].score_editing_enabled
1004        except KeyError:
1005            return []
1006        coursetickets_list = [courseticket for courseticket in coursetickets
1007            if courseticket.student.current_mode in score_editing_enabled]
1008        return coursetickets_list
1009
1010    def _extract_uploadfile(self, uploadfile):
1011        """Get a mapping of student-ids to scores.
1012
1013        The mapping is constructed by reading contents from `uploadfile`.
1014
1015        We expect uploadfile to be a regular CSV file with columns
1016        ``student_id``, ``score`` and ``ca`` (other cols are ignored).
1017        """
1018        result = dict()
1019        data = StringIO(uploadfile.read())  # ensure we have something seekable
1020        reader = csv.DictReader(data)
1021        for row in reader:
1022            if not ('student_id' in row and 'score' in row and 'ca' in row):
1023                continue
1024            result[row['student_id']] = (row['score'], row['ca'])
1025        return result
1026
1027    def _update_scores(self, form):
1028        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
1029        error = ''
1030        if 'UPDATE_FILE' in form:
1031            if form['uploadfile']:
1032                try:
1033                    formvals = self._extract_uploadfile(form['uploadfile'])
1034                except:
1035                    self.flash(
1036                        _('Uploaded file contains illegal data. Ignored'),
1037                        type="danger")
1038                    return False
1039            else:
1040                self.flash(
1041                    _('No file provided.'), type="danger")
1042                return False
1043        else:
1044            formvals = dict(zip(form['sids'], zip(form['scores'], form['cas'])))
1045        for ticket in self.editable_tickets:
1046            ticket_error = False
1047            score = ticket.score
1048            ca = ticket.ca
1049            sid = ticket.student.student_id
1050            if formvals[sid][0] == '':
1051                score = None
1052            if formvals[sid][1] == '':
1053                ca = None
1054            try:
1055                if formvals[sid][0]:
1056                    score = int(formvals[sid][0])
1057                if formvals[sid][1]:
1058                    ca = int(formvals[sid][1])
1059            except ValueError:
1060                error += '%s, ' % ticket.student.display_fullname
1061                ticket_error = True
1062            if not ticket_error and ticket.score != score:
1063                try:
1064                    ticket.score = score
1065                except TooBig:
1066                    error += '%s, ' % ticket.student.display_fullname
1067                    ticket_error = True
1068                    pass
1069                ticket.student.__parent__.logger.info(
1070                    '%s - %s %s/%s score updated (%s)' %
1071                    (ob_class, ticket.student.student_id,
1072                     ticket.level, ticket.code, score))
1073            if not ticket_error and ticket.ca != ca:
1074                try:
1075                    ticket.ca = ca
1076                except TooBig:
1077                    error += '%s, ' % ticket.student.display_fullname
1078                    pass
1079                ticket.student.__parent__.logger.info(
1080                    '%s - %s %s/%s ca updated (%s)' %
1081                    (ob_class, ticket.student.student_id,
1082                     ticket.level, ticket.code, ca))
1083        if error:
1084            self.flash(_('Error: Score(s) and CA(s) of %s have not be updated. '
1085              % error.strip(', ')), type="danger")
1086        return True
1087
1088class EditPreviousSessionScoresPage(CustomEditScoresPage):
1089
1090    grok.name('edit_prev_scores')
1091
1092    def update(self,  *args, **kw):
1093        form = self.request.form
1094        self.current_academic_session = grok.getSite()[
1095            'configuration'].current_academic_session
1096        if self.context.__parent__.__parent__.score_editing_disabled:
1097            self.flash(_('Score editing disabled.'), type="warning")
1098            self.redirect(self.url(self.context))
1099            return
1100        if not self.current_academic_session:
1101            self.flash(_('Current academic session not set.'), type="warning")
1102            self.redirect(self.url(self.context))
1103            return
1104        previous_session = self.current_academic_session - 1
1105        self.session_title = academic_sessions_vocab.getTerm(
1106            previous_session).title
1107        self.tickets = self._searchCatalog(previous_session)
1108        if not self.tickets:
1109            self.flash(_('No student found.'), type="warning")
1110            self.redirect(self.url(self.context))
1111            return
1112        self.editable_tickets = [
1113            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
1114        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
1115            return
1116        if not self.editable_tickets:
1117            return
1118        success = self._update_scores(form)
1119        if success:
1120            self.flash(_('You successfully updated course results.'))
1121        return
1122
1123class CustomExportPDFScoresSlip(ExportPDFScoresSlip):
1124    """Deliver a PDF slip of course tickets for a lecturer.
1125    """
1126
1127    note = u'\nUpgraded scores are in asterisks.'
1128
1129    def data(self, session):
1130        #site = grok.getSite()
1131        cat = queryUtility(ICatalog, name='coursetickets_catalog')
1132        coursetickets = cat.searchResults(
1133            session=(session, session),
1134            code=(self.context.code, self.context.code)
1135            )
1136        # Apply filter
1137        try:
1138            score_editing_enabled = grok.getSite()[
1139                'configuration'][str(session)].score_editing_enabled
1140            coursetickets_filtered = [courseticket
1141                for courseticket in coursetickets
1142                if courseticket.student.current_mode in score_editing_enabled
1143                and courseticket.total_score is not None
1144                and courseticket.__parent__.__parent__.is_current]
1145        except KeyError:
1146            coursetickets_filtered = coursetickets
1147        # In AAUE only editable tickets can be printed
1148        editable_tickets = [
1149            ticket for ticket in coursetickets_filtered
1150            if ticket.editable_by_lecturer]
1151        header = [[_(''),
1152                   _('Student Id'),
1153                   _('Matric No.'),
1154                   #_('Reg. No.'),
1155                   #_('Fullname'),
1156                   #_('Status'),
1157                   #_('Course of\nStudies'),
1158                   _('Department'),
1159                   _('Level'),
1160                   _(' CA  '),
1161                   _('Exam\nScore'),
1162                   _('Total '),
1163                   _('Grade'),
1164                   ],]
1165        sorted_tickets = sorted(editable_tickets,
1166            key=lambda ticket: ticket.student.depcode
1167                               + ticket.student.faccode
1168                               + ticket.student.matric_number)
1169        no = 1
1170        tickets = []
1171        passed = 0
1172        failed = 0
1173        with_ca = False
1174        # In AAUE only editable tickets can be printed
1175        for ticket in sorted_tickets:
1176            if ticket.ca > 0:
1177                with_ca = True
1178            total = ticket.total_score
1179            if getattr(ticket, 'imported_ts', None):
1180                total = "**%s**" % ticket.imported_ts
1181            grade = ticket._getGradeWeightFromScore[0]
1182            if grade in ('F', '-'):
1183                failed += 1
1184            else:
1185                passed += 1
1186            fullname = textwrap.fill(ticket.student.display_fullname, 30)
1187            #deptitle = site['faculties'][ticket.student.faccode][
1188            #    ticket.student.depcode].longtitle
1189            row = [str(no),
1190                  ticket.student.student_id,
1191                  ticket.student.matric_number,
1192                  #ticket.student.reg_number,
1193                  #fullname,
1194                  #ticket.student.translated_state,
1195                  #ticket.student.certcode,
1196                  ticket.student.faccode + ' / ' + ticket.student.depcode,
1197                  ticket.level,
1198                  ticket.ca,
1199                  ticket.score,
1200                  total,
1201                  grade,
1202                  ]
1203            tickets.append(row)
1204            no += 1
1205        total = passed + failed
1206        passed_perc = 0
1207        failed_perc = 0
1208        if total:
1209            passed_perc = 100 * passed / total
1210            failed_perc = 100 * failed / total
1211        dep = self.context.__parent__.__parent__.longtitle
1212        fac = self.context.__parent__.__parent__.__parent__.longtitle
1213        # remove CA column if not necessary
1214        if not with_ca:
1215            header = [[_(''),
1216                       _('Student Id'),
1217                       _('Matric No.'),
1218                       #_('Reg. No.'),
1219                       #_('Fullname'),
1220                       #_('Status'),
1221                       #_('Course of\nStudies'),
1222                       _('Department'),
1223                       _('Level'),
1224                       #_(' CA  '),
1225                       _('Exam\nScore'),
1226                       _('Total '),
1227                       _('Grade'),
1228                       ],]
1229            for ticket in tickets:
1230                del(ticket[5])
1231        return header + tickets, [
1232            dep, fac, total, passed, passed_perc, failed, failed_perc]
1233
1234    def render(self):
1235        session = grok.getSite()['configuration'].current_academic_session
1236        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
1237                     if i['local_role'] == 'waeup.local.Lecturer']
1238        lecturers =  ', '.join(lecturers)
1239        students_utils = getUtility(IStudentsUtils)
1240        # only orientation is different
1241        return students_utils.renderPDFCourseticketsOverview(
1242            self, session, self.data(session), lecturers, '', 45, self.note)
1243
1244class DownloadPreviousSessionScoresView(DownloadScoresView):
1245    """View that exports scores.
1246    """
1247    grok.name('download_prev_scores')
1248
1249    def update(self):
1250        self.current_academic_session = grok.getSite()[
1251            'configuration'].current_academic_session
1252        if self.context.__parent__.__parent__.score_editing_disabled:
1253            self.flash(_('Score editing disabled.'), type="warning")
1254            self.redirect(self.url(self.context))
1255            return
1256        if not self.current_academic_session:
1257            self.flash(_('Current academic session not set.'), type="warning")
1258            self.redirect(self.url(self.context))
1259            return
1260        site = grok.getSite()
1261        exporter = getUtility(ICSVExporter, name='lecturer')
1262        self.csv = exporter.export_filtered(site, filepath=None,
1263                                 catalog='coursetickets',
1264                                 session=self.current_academic_session-1,
1265                                 level=None,
1266                                 code=self.context.code)
1267        return
1268
1269class AlumniRequestPasswordPage(StudentRequestPasswordPage):
1270    """Captcha'd request password page for students.
1271    """
1272    grok.name('alumni_requestpw')
1273    grok.require('waeup.Anonymous')
1274    grok.template('alumni_requestpw')
1275    form_fields = grok.AutoFields(IStudentRequestPW).select(
1276        'lastname','number','email')
1277    label = _('Search student record and send password for first-time login')
1278
1279    def _redirect_no_student(self):
1280        self.flash(_('No student record found.'), type="warning")
1281        self.redirect(self.application_url() + '/applicants/trans2017/register')
1282        return
Note: See TracBrowser for help on using the repository browser.