source: main/waeup.kofa/branches/henrik-transcript-workflow/src/waeup/kofa/students/browser.py @ 15153

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

Print electronic signatures on pdf files.

Remove final transcript file when resetting the transcript process.

  • Property svn:keywords set to Id
File size: 139.7 KB
Line 
1## $Id: browser.py 15150 2018-09-19 11:11:38Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for students and related components.
19"""
20import csv
21import grok
22import pytz
23import sys
24from cStringIO import StringIO
25from datetime import datetime
26from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
27from urllib import urlencode
28from zope.catalog.interfaces import ICatalog
29from zope.component import queryUtility, getUtility, createObject
30from zope.event import notify
31from zope.formlib.textwidgets import BytesDisplayWidget
32from zope.i18n import translate
33from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
34from zope.security import checkPermission
35from waeup.kofa.accesscodes import invalidate_accesscode, get_access_code
36from waeup.kofa.accesscodes.workflow import USED
37from waeup.kofa.browser.breadcrumbs import Breadcrumb
38from waeup.kofa.browser.interfaces import ICaptchaManager
39from waeup.kofa.browser.layout import (
40    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
41    NullValidator, jsaction, action, UtilityView)
42from waeup.kofa.browser.pages import (
43    ContactAdminFormPage, ExportCSVView, doll_up, exports_not_allowed,
44    LocalRoleAssignmentUtilityView)
45from waeup.kofa.hostels.hostel import NOT_OCCUPIED
46from waeup.kofa.interfaces import (
47    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
48    IKofaUtils, IObjectHistory, academic_sessions, ICSVExporter,
49    academic_sessions_vocab, IDataCenter, DOCLINK)
50from waeup.kofa.interfaces import MessageFactory as _
51from waeup.kofa.mandates.mandate import PasswordMandate
52from waeup.kofa.university.interfaces import (
53    IDepartment, ICertificate, ICourse)
54from waeup.kofa.university.certificate import (
55    VirtualCertificateExportJobContainer)
56from waeup.kofa.university.department import (
57    VirtualDepartmentExportJobContainer)
58from waeup.kofa.university.faculty import VirtualFacultyExportJobContainer
59from waeup.kofa.university.facultiescontainer import (
60    VirtualFacultiesExportJobContainer)
61from waeup.kofa.university.course import (
62    VirtualCourseExportJobContainer,)
63from waeup.kofa.university.vocabularies import course_levels
64from waeup.kofa.utils.batching import VirtualExportJobContainer
65from waeup.kofa.utils.helpers import get_current_principal, now
66from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
67from waeup.kofa.students.interfaces import (
68    IStudentsContainer, IStudent, IUGStudentClearance, IPGStudentClearance,
69    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
70    IStudentStudyCourseTransfer, IStudentStudyCourseTranscript,
71    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, ICourseTicketAdd,
72    IStudentPaymentsContainer, IStudentOnlinePayment, IStudentPreviousPayment,
73    IStudentBalancePayment, IBedTicket, IStudentsUtils, IStudentRequestPW,
74    IStudentTranscript
75    )
76from waeup.kofa.students.catalog import search, StudentQueryResultItem
77from waeup.kofa.students.vocabularies import StudyLevelSource
78from waeup.kofa.students.workflow import (
79    ADMITTED, PAID, CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED,
80    VALIDATED, GRADUATED, TRANSREQ, TRANSVAL, TRANSREL, FORBIDDEN_POSTGRAD_TRANS
81    )
82
83
84grok.context(IKofaObject)  # Make IKofaObject the default context
85
86
87class TicketError(Exception):
88    """A course ticket could not be added
89    """
90    pass
91
92# Save function used for save methods in pages
93def msave(view, **data):
94    changed_fields = view.applyData(view.context, **data)
95    # Turn list of lists into single list
96    if changed_fields:
97        changed_fields = reduce(lambda x, y: x+y, changed_fields.values())
98    # Inform catalog if certificate has changed
99    # (applyData does this only for the context)
100    if 'certificate' in changed_fields:
101        notify(grok.ObjectModifiedEvent(view.context.student))
102    fields_string = ' + '.join(changed_fields)
103    view.flash(_('Form has been saved.'))
104    if fields_string:
105        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
106    return
107
108def emit_lock_message(view):
109    """Flash a lock message.
110    """
111    view.flash(_('The requested form is locked (read-only).'), type="warning")
112    view.redirect(view.url(view.context))
113    return
114
115def translated_values(view):
116    """Translate course ticket attribute values to be displayed on
117    studylevel pages.
118    """
119    lang = view.request.cookies.get('kofa.language')
120    for value in view.context.values():
121        # We have to unghostify (according to Tres Seaver) the __dict__
122        # by activating the object, otherwise value_dict will be empty
123        # when calling the first time.
124        value._p_activate()
125        value_dict = dict([i for i in value.__dict__.items()])
126        value_dict['url'] = view.url(value)
127        value_dict['removable_by_student'] = value.removable_by_student
128        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
129            target_language=lang)
130        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
131            target_language=lang)
132        value_dict['outstanding'] = translate(str(value.outstanding), 'zope',
133            target_language=lang)
134        value_dict['automatic'] = translate(str(value.automatic), 'zope',
135            target_language=lang)
136        value_dict['grade'] = value.grade
137        value_dict['weight'] = value.weight
138        value_dict['course_category'] = value.course_category
139        value_dict['total_score'] = value.total_score
140        semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
141        value_dict['semester'] = semester_dict[
142            value.semester].replace('mester', 'm.')
143        yield value_dict
144
145def addCourseTicket(view, course=None):
146    students_utils = getUtility(IStudentsUtils)
147    ticket = createObject(u'waeup.CourseTicket')
148    ticket.automatic = False
149    ticket.carry_over = False
150    warning = students_utils.warnCreditsOOR(view.context, course)
151    if warning:
152        view.flash(warning, type="warning")
153        return False
154    try:
155        view.context.addCourseTicket(ticket, course)
156    except KeyError:
157        view.flash(_('The ticket exists.'), type="warning")
158        return False
159    except TicketError, error:
160        # Ticket errors are not being raised in the base package.
161        view.flash(error, type="warning")
162        return False
163    view.flash(_('Successfully added ${a}.',
164        mapping = {'a':ticket.code}))
165    view.context.writeLogMessage(
166        view,'added: %s|%s|%s' % (
167        ticket.code, ticket.level, ticket.level_session))
168    return True
169
170def level_dict(studycourse):
171    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
172    level_dict = {}
173    studylevelsource = StudyLevelSource().factory
174    for code in studylevelsource.getValues(studycourse):
175        title = translate(studylevelsource.getTitle(studycourse, code),
176            'waeup.kofa', target_language=portal_language)
177        level_dict[code] = title
178    return level_dict
179
180class StudentsBreadcrumb(Breadcrumb):
181    """A breadcrumb for the students container.
182    """
183    grok.context(IStudentsContainer)
184    title = _('Students')
185
186    @property
187    def target(self):
188        user = get_current_principal()
189        if getattr(user, 'user_type', None) == 'student':
190            return None
191        return self.viewname
192
193class StudentBreadcrumb(Breadcrumb):
194    """A breadcrumb for the student container.
195    """
196    grok.context(IStudent)
197
198    def title(self):
199        return self.context.display_fullname
200
201class SudyCourseBreadcrumb(Breadcrumb):
202    """A breadcrumb for the student study course.
203    """
204    grok.context(IStudentStudyCourse)
205
206    def title(self):
207        if self.context.is_current:
208            return _('Study Course')
209        else:
210            return _('Previous Study Course')
211
212class PaymentsBreadcrumb(Breadcrumb):
213    """A breadcrumb for the student payments folder.
214    """
215    grok.context(IStudentPaymentsContainer)
216    title = _('Payments')
217
218class OnlinePaymentBreadcrumb(Breadcrumb):
219    """A breadcrumb for payments.
220    """
221    grok.context(IStudentOnlinePayment)
222
223    @property
224    def title(self):
225        return self.context.p_id
226
227class AccommodationBreadcrumb(Breadcrumb):
228    """A breadcrumb for the student accommodation folder.
229    """
230    grok.context(IStudentAccommodation)
231    title = _('Accommodation')
232
233class BedTicketBreadcrumb(Breadcrumb):
234    """A breadcrumb for bed tickets.
235    """
236    grok.context(IBedTicket)
237
238    @property
239    def title(self):
240        return _('Bed Ticket ${a}',
241            mapping = {'a':self.context.getSessionString()})
242
243class StudyLevelBreadcrumb(Breadcrumb):
244    """A breadcrumb for course lists.
245    """
246    grok.context(IStudentStudyLevel)
247
248    @property
249    def title(self):
250        return self.context.level_title
251
252class StudentsContainerPage(KofaPage):
253    """The standard view for student containers.
254    """
255    grok.context(IStudentsContainer)
256    grok.name('index')
257    grok.require('waeup.viewStudentsContainer')
258    grok.template('containerpage')
259    label = _('Find students')
260    search_button = _('Find student(s)')
261    pnav = 4
262
263    def update(self, *args, **kw):
264        form = self.request.form
265        self.hitlist = []
266        if form.get('searchtype', None) in ('suspended', TRANSREQ, TRANSVAL):
267            self.searchtype = form['searchtype']
268            self.searchterm = None
269        elif 'searchterm' in form and form['searchterm']:
270            self.searchterm = form['searchterm']
271            self.searchtype = form['searchtype']
272        elif 'old_searchterm' in form:
273            self.searchterm = form['old_searchterm']
274            self.searchtype = form['old_searchtype']
275        else:
276            if 'search' in form:
277                self.flash(_('Empty search string'), type="warning")
278            return
279        if self.searchtype == 'current_session':
280            try:
281                self.searchterm = int(self.searchterm)
282            except ValueError:
283                self.flash(_('Only year dates allowed (e.g. 2011).'),
284                           type="danger")
285                return
286        self.hitlist = search(query=self.searchterm,
287            searchtype=self.searchtype, view=self)
288        if not self.hitlist:
289            self.flash(_('No student found.'), type="warning")
290        return
291
292class StudentsContainerManagePage(KofaPage):
293    """The manage page for student containers.
294    """
295    grok.context(IStudentsContainer)
296    grok.name('manage')
297    grok.require('waeup.manageStudent')
298    grok.template('containermanagepage')
299    pnav = 4
300    label = _('Manage students section')
301    search_button = _('Find student(s)')
302    remove_button = _('Remove selected')
303    doclink = DOCLINK + '/students.html'
304
305    def update(self, *args, **kw):
306        form = self.request.form
307        self.hitlist = []
308        if form.get('searchtype', None) in ('suspended', TRANSREQ, TRANSVAL):
309            self.searchtype = form['searchtype']
310            self.searchterm = None
311        elif 'searchterm' in form and form['searchterm']:
312            self.searchterm = form['searchterm']
313            self.searchtype = form['searchtype']
314        elif 'old_searchterm' in form:
315            self.searchterm = form['old_searchterm']
316            self.searchtype = form['old_searchtype']
317        else:
318            if 'search' in form:
319                self.flash(_('Empty search string'), type="warning")
320            return
321        if self.searchtype == 'current_session':
322            try:
323                self.searchterm = int(self.searchterm)
324            except ValueError:
325                self.flash(_('Only year dates allowed (e.g. 2011).'),
326                           type="danger")
327                return
328        if not 'entries' in form:
329            self.hitlist = search(query=self.searchterm,
330                searchtype=self.searchtype, view=self)
331            if not self.hitlist:
332                self.flash(_('No student found.'), type="warning")
333            if 'remove' in form:
334                self.flash(_('No item selected.'), type="warning")
335            return
336        entries = form['entries']
337        if isinstance(entries, basestring):
338            entries = [entries]
339        deleted = []
340        for entry in entries:
341            if 'remove' in form:
342                del self.context[entry]
343                deleted.append(entry)
344        self.hitlist = search(query=self.searchterm,
345            searchtype=self.searchtype, view=self)
346        if len(deleted):
347            self.flash(_('Successfully removed: ${a}',
348                mapping = {'a':', '.join(deleted)}))
349        return
350
351class StudentAddFormPage(KofaAddFormPage):
352    """Add-form to add a student.
353    """
354    grok.context(IStudentsContainer)
355    grok.require('waeup.manageStudent')
356    grok.name('addstudent')
357    form_fields = grok.AutoFields(IStudent).select(
358        'firstname', 'middlename', 'lastname', 'reg_number')
359    label = _('Add student')
360    pnav = 4
361
362    @action(_('Create student'), style='primary')
363    def addStudent(self, **data):
364        student = createObject(u'waeup.Student')
365        self.applyData(student, **data)
366        self.context.addStudent(student)
367        self.flash(_('Student record created.'))
368        self.redirect(self.url(self.context[student.student_id], 'index'))
369        return
370
371    @action(_('Create graduated student'), style='primary')
372    def addGraduatedStudent(self, **data):
373        student = createObject(u'waeup.Student')
374        self.applyData(student, **data)
375        self.context.addStudent(student)
376        IWorkflowState(student).setState(GRADUATED)
377        self.flash(_('Student record created.'))
378        self.redirect(self.url(self.context[student.student_id], 'index'))
379        return
380
381class LoginAsStudentStep1(KofaEditFormPage):
382    """ View to temporarily set a student password.
383    """
384    grok.context(IStudent)
385    grok.name('loginasstep1')
386    grok.require('waeup.loginAsStudent')
387    grok.template('loginasstep1')
388    pnav = 4
389
390    def label(self):
391        return _(u'Set temporary password for ${a}',
392            mapping = {'a':self.context.display_fullname})
393
394    @action('Set password now', style='primary')
395    def setPassword(self, *args, **data):
396        kofa_utils = getUtility(IKofaUtils)
397        password = kofa_utils.genPassword()
398        self.context.setTempPassword(self.request.principal.id, password)
399        self.context.writeLogMessage(
400            self, 'temp_password generated: %s' % password)
401        args = {'password':password}
402        self.redirect(self.url(self.context) +
403            '/loginasstep2?%s' % urlencode(args))
404        return
405
406class LoginAsStudentStep2(KofaPage):
407    """ View to temporarily login as student with a temporary password.
408    """
409    grok.context(IStudent)
410    grok.name('loginasstep2')
411    grok.require('waeup.Public')
412    grok.template('loginasstep2')
413    login_button = _('Login now')
414    pnav = 4
415
416    def label(self):
417        return _(u'Login as ${a}',
418            mapping = {'a':self.context.student_id})
419
420    def update(self, SUBMIT=None, password=None):
421        self.password = password
422        if SUBMIT is not None:
423            self.flash(_('You successfully logged in as student.'))
424            self.redirect(self.url(self.context))
425        return
426
427class StudentBaseDisplayFormPage(KofaDisplayFormPage):
428    """ Page to display student base data
429    """
430    grok.context(IStudent)
431    grok.name('index')
432    grok.require('waeup.viewStudent')
433    grok.template('basepage')
434    form_fields = grok.AutoFields(IStudentBase).omit(
435        'password', 'suspended', 'suspended_comment', 'flash_notice')
436    pnav = 4
437
438    @property
439    def label(self):
440        if self.context.suspended:
441            return _('${a}: Base Data (account deactivated)',
442                mapping = {'a':self.context.display_fullname})
443        return  _('${a}: Base Data',
444            mapping = {'a':self.context.display_fullname})
445
446    @property
447    def hasPassword(self):
448        if self.context.password:
449            return _('set')
450        return _('unset')
451
452    def update(self):
453        if self.context.flash_notice:
454            self.flash(self.context.flash_notice, type="warning")
455        super(StudentBaseDisplayFormPage, self).update()
456        return
457
458class StudentBasePDFFormPage(KofaDisplayFormPage):
459    """ Page to display student base data in pdf files.
460    """
461
462    def __init__(self, context, request, omit_fields=()):
463        self.omit_fields = omit_fields
464        super(StudentBasePDFFormPage, self).__init__(context, request)
465
466    @property
467    def form_fields(self):
468        form_fields = grok.AutoFields(IStudentBase)
469        for field in self.omit_fields:
470            form_fields = form_fields.omit(field)
471        return form_fields
472
473class ContactStudentFormPage(ContactAdminFormPage):
474    grok.context(IStudent)
475    grok.name('contactstudent')
476    grok.require('waeup.viewStudent')
477    pnav = 4
478    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
479
480    def update(self, subject=u'', body=u''):
481        super(ContactStudentFormPage, self).update()
482        self.form_fields.get('subject').field.default = subject
483        self.form_fields.get('body').field.default = body
484        return
485
486    def label(self):
487        return _(u'Send message to ${a}',
488            mapping = {'a':self.context.display_fullname})
489
490    @action('Send message now', style='primary')
491    def send(self, *args, **data):
492        try:
493            email = self.request.principal.email
494        except AttributeError:
495            email = self.config.email_admin
496        usertype = getattr(self.request.principal,
497                           'user_type', 'system').title()
498        kofa_utils = getUtility(IKofaUtils)
499        success = kofa_utils.sendContactForm(
500                self.request.principal.title,email,
501                self.context.display_fullname,self.context.email,
502                self.request.principal.id,usertype,
503                self.config.name,
504                data['body'],data['subject'])
505        if success:
506            self.flash(_('Your message has been sent.'))
507        else:
508            self.flash(_('An smtp server error occurred.'), type="danger")
509        return
510
511class ExportPDFAdmissionSlip(UtilityView, grok.View):
512    """Deliver a PDF Admission slip.
513    """
514    grok.context(IStudent)
515    grok.name('admission_slip.pdf')
516    grok.require('waeup.viewStudent')
517    prefix = 'form'
518
519    omit_fields = ('date_of_birth', 'current_level', 'flash_notice')
520
521    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
522
523    @property
524    def label(self):
525        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
526        return translate(_('Admission Letter of'),
527            'waeup.kofa', target_language=portal_language) \
528            + ' %s' % self.context.display_fullname
529
530    def render(self):
531        students_utils = getUtility(IStudentsUtils)
532        return students_utils.renderPDFAdmissionLetter(self,
533            self.context.student, omit_fields=self.omit_fields)
534
535class StudentBaseManageFormPage(KofaEditFormPage):
536    """ View to manage student base data
537    """
538    grok.context(IStudent)
539    grok.name('manage_base')
540    grok.require('waeup.manageStudent')
541    form_fields = grok.AutoFields(IStudentBase).omit(
542        'student_id', 'adm_code', 'suspended')
543    grok.template('basemanagepage')
544    label = _('Manage base data')
545    pnav = 4
546
547    def update(self):
548        super(StudentBaseManageFormPage, self).update()
549        self.wf_info = IWorkflowInfo(self.context)
550        return
551
552    @action(_('Save'), style='primary')
553    def save(self, **data):
554        form = self.request.form
555        password = form.get('password', None)
556        password_ctl = form.get('control_password', None)
557        if password:
558            validator = getUtility(IPasswordValidator)
559            errors = validator.validate_password(password, password_ctl)
560            if errors:
561                self.flash( ' '.join(errors), type="danger")
562                return
563        changed_fields = self.applyData(self.context, **data)
564        # Turn list of lists into single list
565        if changed_fields:
566            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
567        else:
568            changed_fields = []
569        if password:
570            # Now we know that the form has no errors and can set password
571            IUserAccount(self.context).setPassword(password)
572            changed_fields.append('password')
573        fields_string = ' + '.join(changed_fields)
574        self.flash(_('Form has been saved.'))
575        if fields_string:
576            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
577        return
578
579class StudentTriggerTransitionFormPage(KofaEditFormPage):
580    """ View to trigger student workflow transitions
581    """
582    grok.context(IStudent)
583    grok.name('trigtrans')
584    grok.require('waeup.triggerTransition')
585    grok.template('trigtrans')
586    label = _('Trigger registration transition')
587    pnav = 4
588
589    def getTransitions(self):
590        """Return a list of dicts of allowed transition ids and titles.
591
592        Each list entry provides keys ``name`` and ``title`` for
593        internal name and (human readable) title of a single
594        transition.
595        """
596        wf_info = IWorkflowInfo(self.context)
597        allowed_transitions = [t for t in wf_info.getManualTransitions()
598            if not t[0].startswith('pay')]
599        if self.context.is_postgrad and not self.context.is_special_postgrad:
600            allowed_transitions = [t for t in allowed_transitions
601                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
602        return [dict(name='', title=_('No transition'))] +[
603            dict(name=x, title=y) for x, y in allowed_transitions]
604
605    @action(_('Save'), style='primary')
606    def save(self, **data):
607        form = self.request.form
608        if 'transition' in form and form['transition']:
609            transition_id = form['transition']
610            wf_info = IWorkflowInfo(self.context)
611            wf_info.fireTransition(transition_id)
612        return
613
614class StudentActivateView(UtilityView, grok.View):
615    """ Activate student account
616    """
617    grok.context(IStudent)
618    grok.name('activate')
619    grok.require('waeup.manageStudent')
620
621    def update(self):
622        self.context.suspended = False
623        self.context.writeLogMessage(self, 'account activated')
624        history = IObjectHistory(self.context)
625        history.addMessage('Student account activated')
626        self.flash(_('Student account has been activated.'))
627        self.redirect(self.url(self.context))
628        return
629
630    def render(self):
631        return
632
633class StudentDeactivateView(UtilityView, grok.View):
634    """ Deactivate student account
635    """
636    grok.context(IStudent)
637    grok.name('deactivate')
638    grok.require('waeup.manageStudent')
639
640    def update(self):
641        self.context.suspended = True
642        self.context.writeLogMessage(self, 'account deactivated')
643        history = IObjectHistory(self.context)
644        history.addMessage('Student account deactivated')
645        self.flash(_('Student account has been deactivated.'))
646        self.redirect(self.url(self.context))
647        return
648
649    def render(self):
650        return
651
652class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
653    """ Page to display student clearance data
654    """
655    grok.context(IStudent)
656    grok.name('view_clearance')
657    grok.require('waeup.viewStudent')
658    pnav = 4
659
660    @property
661    def separators(self):
662        return getUtility(IStudentsUtils).SEPARATORS_DICT
663
664    @property
665    def form_fields(self):
666        if self.context.is_postgrad:
667            form_fields = grok.AutoFields(IPGStudentClearance)
668        else:
669            form_fields = grok.AutoFields(IUGStudentClearance)
670        if not getattr(self.context, 'officer_comment'):
671            form_fields = form_fields.omit('officer_comment')
672        else:
673            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
674        return form_fields
675
676    @property
677    def label(self):
678        return _('${a}: Clearance Data',
679            mapping = {'a':self.context.display_fullname})
680
681class ExportPDFClearanceSlip(grok.View):
682    """Deliver a PDF slip of the context.
683    """
684    grok.context(IStudent)
685    grok.name('clearance_slip.pdf')
686    grok.require('waeup.viewStudent')
687    prefix = 'form'
688    omit_fields = (
689        'suspended', 'phone',
690        'adm_code', 'suspended_comment',
691        'date_of_birth', 'current_level',
692        'flash_notice')
693
694    @property
695    def form_fields(self):
696        if self.context.is_postgrad:
697            form_fields = grok.AutoFields(IPGStudentClearance)
698        else:
699            form_fields = grok.AutoFields(IUGStudentClearance)
700        if not getattr(self.context, 'officer_comment'):
701            form_fields = form_fields.omit('officer_comment')
702        return form_fields
703
704    @property
705    def title(self):
706        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
707        return translate(_('Clearance Data'), 'waeup.kofa',
708            target_language=portal_language)
709
710    @property
711    def label(self):
712        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
713        return translate(_('Clearance Slip of'),
714            'waeup.kofa', target_language=portal_language) \
715            + ' %s' % self.context.display_fullname
716
717    # XXX: not used in waeup.kofa and thus not tested
718    def _signatures(self):
719        isStudent = getattr(
720            self.request.principal, 'user_type', None) == 'student'
721        if not isStudent and self.context.state in (CLEARED, ):
722            return ([_('Student Signature')],
723                    [_('Clearance Officer Signature')])
724        return
725
726    def _sigsInFooter(self):
727        isStudent = getattr(
728            self.request.principal, 'user_type', None) == 'student'
729        if not isStudent and self.context.state in (CLEARED, ):
730            return (_('Date, Student Signature'),
731                    _('Date, Clearance Officer Signature'),
732                    )
733        return ()
734
735    def render(self):
736        studentview = StudentBasePDFFormPage(self.context.student,
737            self.request, self.omit_fields)
738        students_utils = getUtility(IStudentsUtils)
739        return students_utils.renderPDF(
740            self, 'clearance_slip.pdf',
741            self.context.student, studentview, signatures=self._signatures(),
742            sigs_in_footer=self._sigsInFooter(),
743            omit_fields=self.omit_fields)
744
745class StudentClearanceManageFormPage(KofaEditFormPage):
746    """ Page to manage student clearance data
747    """
748    grok.context(IStudent)
749    grok.name('manage_clearance')
750    grok.require('waeup.manageStudent')
751    grok.template('clearanceeditpage')
752    label = _('Manage clearance data')
753    deletion_warning = _('Are you sure?')
754    pnav = 4
755
756    @property
757    def separators(self):
758        return getUtility(IStudentsUtils).SEPARATORS_DICT
759
760    @property
761    def form_fields(self):
762        if self.context.is_postgrad:
763            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
764        else:
765            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
766        return form_fields
767
768    @action(_('Save'), style='primary')
769    def save(self, **data):
770        msave(self, **data)
771        return
772
773class StudentClearView(UtilityView, grok.View):
774    """ Clear student by clearance officer
775    """
776    grok.context(IStudent)
777    grok.name('clear')
778    grok.require('waeup.clearStudent')
779
780    def update(self):
781        cdm = getUtility(IStudentsUtils).clearance_disabled_message(
782            self.context)
783        if cdm:
784            self.flash(cdm)
785            self.redirect(self.url(self.context,'view_clearance'))
786            return
787        if self.context.state == REQUESTED:
788            IWorkflowInfo(self.context).fireTransition('clear')
789            self.flash(_('Student has been cleared.'))
790        else:
791            self.flash(_('Student is in wrong state.'), type="warning")
792        self.redirect(self.url(self.context,'view_clearance'))
793        return
794
795    def render(self):
796        return
797
798class StudentRejectClearancePage(KofaEditFormPage):
799    """ Reject clearance by clearance officers.
800    """
801    grok.context(IStudent)
802    grok.name('reject_clearance')
803    label = _('Reject clearance')
804    grok.require('waeup.clearStudent')
805    form_fields = grok.AutoFields(
806        IUGStudentClearance).select('officer_comment')
807
808    def update(self):
809        cdm = getUtility(IStudentsUtils).clearance_disabled_message(
810            self.context)
811        if cdm:
812            self.flash(cdm, type="warning")
813            self.redirect(self.url(self.context,'view_clearance'))
814            return
815        return super(StudentRejectClearancePage, self).update()
816
817    @action(_('Save comment and reject clearance now'), style='primary')
818    def reject(self, **data):
819        if self.context.state == CLEARED:
820            IWorkflowInfo(self.context).fireTransition('reset4')
821            message = _('Clearance has been annulled.')
822            self.flash(message, type="warning")
823        elif self.context.state == REQUESTED:
824            IWorkflowInfo(self.context).fireTransition('reset3')
825            message = _('Clearance request has been rejected.')
826            self.flash(message, type="warning")
827        else:
828            self.flash(_('Student is in wrong state.'), type="warning")
829            self.redirect(self.url(self.context,'view_clearance'))
830            return
831        self.applyData(self.context, **data)
832        comment = data['officer_comment']
833        if comment:
834            self.context.writeLogMessage(
835                self, 'comment: %s' % comment.replace('\n', '<br>'))
836            args = {'subject':message, 'body':comment}
837        else:
838            args = {'subject':message,}
839        self.redirect(self.url(self.context) +
840            '/contactstudent?%s' % urlencode(args))
841        return
842
843
844class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
845    """ Page to display student personal data
846    """
847    grok.context(IStudent)
848    grok.name('view_personal')
849    grok.require('waeup.viewStudent')
850    form_fields = grok.AutoFields(IStudentPersonal)
851    form_fields['perm_address'].custom_widget = BytesDisplayWidget
852    form_fields[
853        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
854    pnav = 4
855
856    @property
857    def label(self):
858        return _('${a}: Personal Data',
859            mapping = {'a':self.context.display_fullname})
860
861class StudentPersonalManageFormPage(KofaEditFormPage):
862    """ Page to manage personal data
863    """
864    grok.context(IStudent)
865    grok.name('manage_personal')
866    grok.require('waeup.manageStudent')
867    form_fields = grok.AutoFields(IStudentPersonal)
868    form_fields['personal_updated'].for_display = True
869    form_fields[
870        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
871    label = _('Manage personal data')
872    pnav = 4
873
874    @action(_('Save'), style='primary')
875    def save(self, **data):
876        msave(self, **data)
877        return
878
879class StudentPersonalEditFormPage(KofaEditFormPage):
880    """ Page to edit personal data
881    """
882    grok.context(IStudent)
883    grok.name('edit_personal')
884    grok.require('waeup.handleStudent')
885    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
886    label = _('Edit personal data')
887    pnav = 4
888
889    @action(_('Save/Confirm'), style='primary')
890    def save(self, **data):
891        msave(self, **data)
892        self.context.personal_updated = datetime.utcnow()
893        return
894
895class StudyCourseDisplayFormPage(KofaDisplayFormPage):
896    """ Page to display the student study course data
897    """
898    grok.context(IStudentStudyCourse)
899    grok.name('index')
900    grok.require('waeup.viewStudent')
901    grok.template('studycoursepage')
902    pnav = 4
903
904    @property
905    def form_fields(self):
906        if self.context.is_postgrad:
907            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
908                'previous_verdict')
909        else:
910            form_fields = grok.AutoFields(IStudentStudyCourse)
911        return form_fields
912
913    @property
914    def label(self):
915        if self.context.is_current:
916            return _('${a}: Study Course',
917                mapping = {'a':self.context.__parent__.display_fullname})
918        else:
919            return _('${a}: Previous Study Course',
920                mapping = {'a':self.context.__parent__.display_fullname})
921
922    @property
923    def current_mode(self):
924        if self.context.certificate is not None:
925            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
926            return studymodes_dict[self.context.certificate.study_mode]
927        return
928
929    @property
930    def department(self):
931        try:
932            if self.context.certificate is not None:
933                return self.context.certificate.__parent__.__parent__
934        except AttributeError:
935            # handle_certificate_removed does only clear
936            # studycourses with certificate code 'studycourse' but not
937            # 'studycourse_1' or 'studycourse_2'. These certificates do
938            # still exist but have no parents.
939            pass
940        return
941
942    @property
943    def faculty(self):
944        try:
945            if self.context.certificate is not None:
946                return self.context.certificate.__parent__.__parent__.__parent__
947        except AttributeError:
948            # handle_certificate_removed does only clear
949            # studycourses with certificate code 'studycourse' but not
950            # 'studycourse_1' or 'studycourse_2'. These certificates do
951            # still exist but have no parents.
952            pass
953        return
954
955    @property
956    def prev_studycourses(self):
957        if self.context.is_current:
958            if self.context.__parent__.get('studycourse_2', None) is not None:
959                return (
960                        {'href':self.url(self.context.student) + '/studycourse_1',
961                        'title':_('First Study Course, ')},
962                        {'href':self.url(self.context.student) + '/studycourse_2',
963                        'title':_('Second Study Course')}
964                        )
965            if self.context.__parent__.get('studycourse_1', None) is not None:
966                return (
967                        {'href':self.url(self.context.student) + '/studycourse_1',
968                        'title':_('First Study Course')},
969                        )
970        return
971
972class StudyCourseManageFormPage(KofaEditFormPage):
973    """ Page to edit the student study course data
974    """
975    grok.context(IStudentStudyCourse)
976    grok.name('manage')
977    grok.require('waeup.manageStudent')
978    grok.template('studycoursemanagepage')
979    label = _('Manage study course')
980    pnav = 4
981    taboneactions = [_('Save'),_('Cancel')]
982    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
983    tabthreeactions = [_('Add study level')]
984
985    @property
986    def form_fields(self):
987        if self.context.is_postgrad:
988            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
989                'previous_verdict')
990        else:
991            form_fields = grok.AutoFields(IStudentStudyCourse)
992        return form_fields
993
994    def update(self):
995        if not self.context.is_current \
996            or self.context.student.studycourse_locked:
997            emit_lock_message(self)
998            return
999        super(StudyCourseManageFormPage, self).update()
1000        return
1001
1002    @action(_('Save'), style='primary')
1003    def save(self, **data):
1004        try:
1005            msave(self, **data)
1006        except ConstraintNotSatisfied:
1007            # The selected level might not exist in certificate
1008            self.flash(_('Current level not available for certificate.'),
1009                       type="warning")
1010            return
1011        notify(grok.ObjectModifiedEvent(self.context.__parent__))
1012        return
1013
1014    @property
1015    def level_dicts(self):
1016        studylevelsource = StudyLevelSource().factory
1017        for code in studylevelsource.getValues(self.context):
1018            title = studylevelsource.getTitle(self.context, code)
1019            yield(dict(code=code, title=title))
1020
1021    @property
1022    def session_dicts(self):
1023        yield(dict(code='', title='--'))
1024        for item in academic_sessions():
1025            code = item[1]
1026            title = item[0]
1027            yield(dict(code=code, title=title))
1028
1029    @action(_('Add study level'), style='primary')
1030    def addStudyLevel(self, **data):
1031        level_code = self.request.form.get('addlevel', None)
1032        level_session = self.request.form.get('level_session', None)
1033        if not level_session:
1034            self.flash(_('You must select a session for the level.'),
1035                       type="warning")
1036            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1037            return
1038        studylevel = createObject(u'waeup.StudentStudyLevel')
1039        studylevel.level = int(level_code)
1040        studylevel.level_session = int(level_session)
1041        try:
1042            self.context.addStudentStudyLevel(
1043                self.context.certificate,studylevel)
1044            self.flash(_('Study level has been added.'))
1045        except KeyError:
1046            self.flash(_('This level exists.'), type="warning")
1047        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1048        return
1049
1050    @jsaction(_('Remove selected levels'))
1051    def delStudyLevels(self, **data):
1052        form = self.request.form
1053        if 'val_id' in form:
1054            child_id = form['val_id']
1055        else:
1056            self.flash(_('No study level selected.'), type="warning")
1057            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1058            return
1059        if not isinstance(child_id, list):
1060            child_id = [child_id]
1061        deleted = []
1062        for id in child_id:
1063            del self.context[id]
1064            deleted.append(id)
1065        if len(deleted):
1066            self.flash(_('Successfully removed: ${a}',
1067                mapping = {'a':', '.join(deleted)}))
1068            self.context.writeLogMessage(
1069                self,'removed: %s' % ', '.join(deleted))
1070        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1071        return
1072
1073class StudentTranscriptRequestPage(KofaPage):
1074    """ Page to request transcript by student
1075    """
1076    grok.context(IStudentTranscript)
1077    grok.name('request_transcript')
1078    grok.require('waeup.handleStudent')
1079    grok.template('transcriptrequest')
1080    label = _('Request transcript')
1081    ac_prefix = 'TSC'
1082    notice = ''
1083    pnav = 4
1084    buttonname = _('Submit')
1085    with_ac = True
1086
1087    def update(self, SUBMIT=None):
1088        super(StudentTranscriptRequestPage, self).update()
1089        if not self.context.state == GRADUATED:
1090            self.flash(_("Wrong state"), type="danger")
1091            self.redirect(self.url(self.context))
1092            return
1093        if self.with_ac:
1094            self.ac_series = self.request.form.get('ac_series', None)
1095            self.ac_number = self.request.form.get('ac_number', None)
1096        if self.context.transcript_comment is not None:
1097            self.correspondence = self.context.transcript_comment.replace(
1098                '\n', '<br>')
1099        else:
1100            self.correspondence = ''
1101        if SUBMIT is None:
1102            return
1103        if self.with_ac:
1104            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1105            code = get_access_code(pin)
1106            if not code:
1107                self.flash(_('Activation code is invalid.'), type="warning")
1108                return
1109            if code.state == USED:
1110                self.flash(_('Activation code has already been used.'),
1111                           type="warning")
1112                return
1113            # Mark pin as used (this also fires a pin related transition)
1114            # and fire transition request_transcript
1115            comment = _(u"invalidated")
1116            # Here we know that the ac is in state initialized so we do not
1117            # expect an exception, but the owner might be different
1118            if not invalidate_accesscode(pin, comment, self.context.student_id):
1119                self.flash(_('You are not the owner of this access code.'),
1120                           type="warning")
1121                return
1122            self.context.clr_code = pin
1123        IWorkflowInfo(self.context).fireTransition('request_transcript')
1124        comment = self.request.form.get('comment', '').replace('\r', '')
1125        address = self.request.form.get('address', '').replace('\r', '')
1126        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1127        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1128        old_transcript_comment = self.context.transcript_comment
1129        if old_transcript_comment == None:
1130            old_transcript_comment = ''
1131        self.context.transcript_comment = '''On %s %s wrote:
1132
1133%s
1134
1135Dispatch Address:
1136%s
1137
1138%s''' % (today, self.request.principal.id, comment, address,
1139         old_transcript_comment)
1140        self.context.writeLogMessage(
1141            self, 'comment: %s' % comment.replace('\n', '<br>'))
1142        self.flash(_('Transcript processing has been started.'))
1143        self.redirect(self.url(self.context))
1144        return
1145
1146class StudentTranscriptValidateView(UtilityView, grok.View):
1147    """ View to validate transcript
1148    """
1149    grok.context(IStudentTranscript)
1150    grok.name('validate_transcript')
1151    grok.require('waeup.processTranscript')
1152
1153    def update(self, SUBMIT=None):
1154        if self.context.student.state != TRANSREQ:
1155            self.flash(_('Student is in wrong state.'), type="warning")
1156            self.redirect(self.url(self.context))
1157            return
1158        # Fire transition
1159        IWorkflowInfo(self.context).fireTransition('validate_transcript')
1160        self.flash(_('Transcript validated.'))
1161        self.redirect(self.url(self.context))
1162        return
1163
1164    def render(self):
1165        return
1166
1167class StudentTranscriptSignView(UtilityView, grok.View):
1168    """ View to sign transcript
1169    """
1170    grok.context(IStudentStudyCourse)
1171    grok.name('sign_transcript')
1172    grok.require('waeup.signTranscript')
1173
1174    def update(self, SUBMIT=None):
1175        if self.context.student.state != TRANSVAL:
1176            self.flash(_('Student is in wrong state.'), type="warning")
1177            self.redirect(self.url(self.context))
1178            return
1179        self.flash(_('Transcript signed.'))
1180        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1181        self.context.student.__parent__.logger.info(
1182            '%s - %s - Transcript signed'
1183            % (ob_class, self.context.student.student_id))
1184        self.context.student.history.addMessage('Transcript signed')
1185        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1186        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1187        prev_transcript_signees = self.context.student.transcript_signees
1188        if prev_transcript_signees == None:
1189            prev_transcript_signees = ''
1190        self.context.student.transcript_signees = (
1191            u"Electronically signed by %s (%s) on %s\n%s"
1192            % (self.request.principal.title, self.request.principal.id, today,
1193            prev_transcript_signees))
1194        self.redirect(self.url(self.context) + '/transcript')
1195        return
1196
1197    def render(self):
1198        return
1199
1200class StudentTranscriptReleaseFormPage(KofaEditFormPage):
1201    """ Page to release transcript
1202    """
1203    grok.context(IStudentTranscript)
1204    grok.name('release_transcript')
1205    grok.require('waeup.processTranscript')
1206    grok.template('transcriptrelease')
1207    form_fields = grok.AutoFields(IStudentTranscript)
1208    label = _('Release transcript')
1209    buttonname = _('Save comment and release transcript')
1210    pnav = 4
1211
1212    def update(self, SUBMIT=None):
1213        super(StudentTranscriptReleaseFormPage, self).update()
1214        if self.context.state != TRANSVAL:
1215            self.flash(_('Student is in wrong state.'), type="warning")
1216            self.redirect(self.url(self.context))
1217            return
1218        if self.context.transcript_comment is not None:
1219            self.correspondence = self.context.transcript_comment.replace(
1220                '\n', '<br>')
1221        else:
1222            self.correspondence = ''
1223        if self.context.transcript_signees is not None:
1224            self.signees = self.context.transcript_signees.replace(
1225                '\n', '<br><br>')
1226        else:
1227            self.signees = ''
1228        if SUBMIT is None:
1229            return
1230        # Fire transition
1231        IWorkflowInfo(self.context).fireTransition('release_transcript')
1232        self.flash(_('Transcript released and final transcript file saved.'))
1233        comment = self.request.form.get('comment', '').replace('\r', '')
1234        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1235        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1236        old_transcript_comment = self.context.transcript_comment
1237        if old_transcript_comment == None:
1238            old_transcript_comment = ''
1239        self.context.transcript_comment = '''On %s %s wrote:
1240
1241%s
1242
1243%s''' % (today, self.request.principal.id, comment,
1244         old_transcript_comment)
1245        self.context.writeLogMessage(
1246            self, 'comment: %s' % comment.replace('\n', '<br>'))
1247        # Produce transcript file
1248        self.redirect(self.url(self.context) + '/studycourse/transcript.pdf')
1249        return
1250
1251class StudentTranscriptRequestManageFormPage(KofaEditFormPage):
1252    """ Page to manage transcript data. There is no link button
1253    for this page. It's an emergency page.
1254    """
1255    grok.context(IStudentTranscript)
1256    grok.name('manage_transcript_request')
1257    grok.require('waeup.processTranscript')
1258    form_fields = grok.AutoFields(IStudentTranscript)
1259    label = _('Manage transcript request')
1260    pnav = 4
1261
1262    @action(_('Save'), style='primary')
1263    def save(self, **data):
1264        msave(self, **data)
1265        return
1266
1267class StudyCourseTranscriptPage(KofaDisplayFormPage):
1268    """ Page to display the student's transcript.
1269    """
1270    grok.context(IStudentStudyCourse)
1271    grok.name('transcript')
1272    grok.require('waeup.viewTranscript')
1273    grok.template('transcript')
1274    pnav = 4
1275
1276    def update(self):
1277        final_slip = getUtility(IExtFileStore).getFileByContext(
1278            self.context.student, attr='final_transcript')
1279        if not self.context.student.transcript_enabled or final_slip:
1280            self.flash(_('Forbidden!'), type="warning")
1281            self.redirect(self.url(self.context))
1282            return
1283        super(StudyCourseTranscriptPage, self).update()
1284        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1285        self.level_dict = level_dict(self.context)
1286        self.session_dict = dict(
1287            [(item[1], item[0]) for item in academic_sessions()])
1288        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1289        return
1290
1291    @property
1292    def label(self):
1293        # Here we know that the cookie has been set
1294        return _('${a}: Transcript Data', mapping = {
1295            'a':self.context.student.display_fullname})
1296
1297class ExportPDFTranscriptSlip(UtilityView, grok.View):
1298    """Deliver a PDF slip of the context.
1299    """
1300    grok.context(IStudentStudyCourse)
1301    grok.name('transcript.pdf')
1302    grok.require('waeup.viewTranscript')
1303    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
1304    prefix = 'form'
1305    omit_fields = (
1306        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
1307        'password', 'suspended', 'phone', 'email',
1308        'adm_code', 'suspended_comment', 'current_level', 'flash_notice')
1309
1310    def update(self):
1311        final_slip = getUtility(IExtFileStore).getFileByContext(
1312            self.context.student, attr='final_transcript')
1313        if not self.context.student.transcript_enabled \
1314            or final_slip:
1315            self.flash(_('Forbidden!'), type="warning")
1316            self.redirect(self.url(self.context))
1317            return
1318        super(ExportPDFTranscriptSlip, self).update()
1319        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1320        self.level_dict = level_dict(self.context)
1321        self.session_dict = dict(
1322            [(item[1], item[0]) for item in academic_sessions()])
1323        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1324        return
1325
1326    @property
1327    def label(self):
1328        # Here we know that the cookie has been set
1329        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1330        return translate(_('Academic Transcript'),
1331            'waeup.kofa', target_language=portal_language)
1332
1333    def _sigsInFooter(self):
1334        if self.context.student.transcript_signees:
1335            return ()
1336        return (_('CERTIFIED TRUE COPY'),)
1337
1338    def _signatures(self):
1339        return ()
1340
1341    def _digital_sigs(self):
1342        if self.context.student.transcript_signees:
1343            return self.context.student.transcript_signees #.split('\n')
1344        return ()
1345
1346    def _save_file(self):
1347        if self.context.student.state == TRANSREL:
1348            return True
1349        return False
1350
1351    def render(self):
1352        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1353        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1354        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1355        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1356        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1357        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1358        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1359        studentview = StudentBasePDFFormPage(self.context.student,
1360            self.request, self.omit_fields)
1361        students_utils = getUtility(IStudentsUtils)
1362
1363        tableheader = [(Code,'code', 2.5),
1364                         (Title,'title', 7),
1365                         (Term, 'semester', 1.5),
1366                         (Cred, 'credits', 1.5),
1367                         (Score, 'total_score', 1.5),
1368                         (Grade, 'grade', 1.5),
1369                         ]
1370
1371        pdfstream = students_utils.renderPDFTranscript(
1372            self, 'transcript.pdf',
1373            self.context.student, studentview,
1374            omit_fields=self.omit_fields,
1375            tableheader=tableheader,
1376            signatures=self._signatures(),
1377            sigs_in_footer=self._sigsInFooter(),
1378            digital_sigs=self._digital_sigs(),
1379            save_file=self._save_file(),
1380            )
1381        if not pdfstream:
1382            self.redirect(self.url(self.context.student))
1383            return
1384        return pdfstream
1385
1386class StudentTransferFormPage(KofaAddFormPage):
1387    """Page to transfer the student.
1388    """
1389    grok.context(IStudent)
1390    grok.name('transfer')
1391    grok.require('waeup.manageStudent')
1392    label = _('Transfer student')
1393    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1394        'entry_mode', 'entry_session')
1395    pnav = 4
1396
1397    @jsaction(_('Transfer'))
1398    def transferStudent(self, **data):
1399        error = self.context.transfer(**data)
1400        if error == -1:
1401            self.flash(_('Current level does not match certificate levels.'),
1402                       type="warning")
1403        elif error == -2:
1404            self.flash(_('Former study course record incomplete.'),
1405                       type="warning")
1406        elif error == -3:
1407            self.flash(_('Maximum number of transfers exceeded.'),
1408                       type="warning")
1409        else:
1410            self.flash(_('Successfully transferred.'))
1411        return
1412
1413class RevertTransferFormPage(KofaEditFormPage):
1414    """View that reverts the previous transfer.
1415    """
1416    grok.context(IStudent)
1417    grok.name('revert_transfer')
1418    grok.require('waeup.manageStudent')
1419    grok.template('reverttransfer')
1420    label = _('Revert previous transfer')
1421
1422    def update(self):
1423        if not self.context.has_key('studycourse_1'):
1424            self.flash(_('No previous transfer.'), type="warning")
1425            self.redirect(self.url(self.context))
1426            return
1427        return
1428
1429    @jsaction(_('Revert now'))
1430    def transferStudent(self, **data):
1431        self.context.revert_transfer()
1432        self.flash(_('Previous transfer reverted.'))
1433        self.redirect(self.url(self.context, 'studycourse'))
1434        return
1435
1436class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1437    """ Page to display student study levels
1438    """
1439    grok.context(IStudentStudyLevel)
1440    grok.name('index')
1441    grok.require('waeup.viewStudent')
1442    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1443    form_fields[
1444        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1445    grok.template('studylevelpage')
1446    pnav = 4
1447
1448    def update(self):
1449        super(StudyLevelDisplayFormPage, self).update()
1450        return
1451
1452    @property
1453    def translated_values(self):
1454        return translated_values(self)
1455
1456    @property
1457    def label(self):
1458        # Here we know that the cookie has been set
1459        lang = self.request.cookies.get('kofa.language')
1460        level_title = translate(self.context.level_title, 'waeup.kofa',
1461            target_language=lang)
1462        return _('${a}: Study Level ${b}', mapping = {
1463            'a':self.context.student.display_fullname,
1464            'b':level_title})
1465
1466class ExportPDFCourseRegistrationSlip(UtilityView, grok.View):
1467    """Deliver a PDF slip of the context.
1468    """
1469    grok.context(IStudentStudyLevel)
1470    grok.name('course_registration_slip.pdf')
1471    grok.require('waeup.viewStudent')
1472    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1473    form_fields[
1474        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1475    prefix = 'form'
1476    omit_fields = (
1477        'password', 'suspended', 'phone', 'date_of_birth',
1478        'adm_code', 'sex', 'suspended_comment', 'current_level',
1479        'flash_notice')
1480
1481    @property
1482    def title(self):
1483        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1484        return translate(_('Level Data'), 'waeup.kofa',
1485            target_language=portal_language)
1486
1487    @property
1488    def label(self):
1489        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1490        lang = self.request.cookies.get('kofa.language', portal_language)
1491        level_title = translate(self.context.level_title, 'waeup.kofa',
1492            target_language=lang)
1493        return translate(_('Course Registration Slip'),
1494            'waeup.kofa', target_language=portal_language) \
1495            + ' %s' % level_title
1496
1497    @property
1498    def tabletitle(self):
1499        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1500        tabletitle = []
1501        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1502            target_language=portal_language))
1503        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1504            target_language=portal_language))
1505        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1506            target_language=portal_language))
1507        return tabletitle
1508
1509    def render(self):
1510        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1511        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1512        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1513        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1514        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1515        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1516        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1517        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1518        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1519        studentview = StudentBasePDFFormPage(self.context.student,
1520            self.request, self.omit_fields)
1521        students_utils = getUtility(IStudentsUtils)
1522
1523        tabledata = []
1524        tableheader = []
1525        for i in range(1,7):
1526            tabledata.append(sorted(
1527                [value for value in self.context.values() if value.semester == i],
1528                key=lambda value: str(value.semester) + value.code))
1529            tableheader.append([(Code,'code', 2.5),
1530                             (Title,'title', 5),
1531                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1532                             (Cred, 'credits', 1.5),
1533                             #(Mand, 'mandatory', 1.5),
1534                             (Score, 'score', 1.5),
1535                             (Grade, 'grade', 1.5),
1536                             #('Auto', 'automatic', 1.5)
1537                             ])
1538        return students_utils.renderPDF(
1539            self, 'course_registration_slip.pdf',
1540            self.context.student, studentview,
1541            tableheader=tableheader,
1542            tabledata=tabledata,
1543            omit_fields=self.omit_fields
1544            )
1545
1546class StudyLevelManageFormPage(KofaEditFormPage):
1547    """ Page to edit the student study level data
1548    """
1549    grok.context(IStudentStudyLevel)
1550    grok.name('manage')
1551    grok.require('waeup.manageStudent')
1552    grok.template('studylevelmanagepage')
1553    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1554        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
1555    pnav = 4
1556    taboneactions = [_('Save'),_('Cancel')]
1557    tabtwoactions = [_('Add course ticket'),
1558        _('Remove selected tickets'),_('Cancel')]
1559    placeholder = _('Enter valid course code')
1560
1561    def update(self, ADD=None, course=None):
1562        if not self.context.__parent__.is_current \
1563            or self.context.student.studycourse_locked:
1564            emit_lock_message(self)
1565            return
1566        super(StudyLevelManageFormPage, self).update()
1567        if ADD is not None:
1568            if not course:
1569                self.flash(_('No valid course code entered.'), type="warning")
1570                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1571                return
1572            cat = queryUtility(ICatalog, name='courses_catalog')
1573            result = cat.searchResults(code=(course, course))
1574            if len(result) != 1:
1575                self.flash(_('Course not found.'), type="warning")
1576            else:
1577                course = list(result)[0]
1578                addCourseTicket(self, course)
1579            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1580        return
1581
1582    @property
1583    def translated_values(self):
1584        return translated_values(self)
1585
1586    @property
1587    def label(self):
1588        # Here we know that the cookie has been set
1589        lang = self.request.cookies.get('kofa.language')
1590        level_title = translate(self.context.level_title, 'waeup.kofa',
1591            target_language=lang)
1592        return _('Manage study level ${a}',
1593            mapping = {'a':level_title})
1594
1595    @action(_('Save'), style='primary')
1596    def save(self, **data):
1597        msave(self, **data)
1598        return
1599
1600    @jsaction(_('Remove selected tickets'))
1601    def delCourseTicket(self, **data):
1602        form = self.request.form
1603        if 'val_id' in form:
1604            child_id = form['val_id']
1605        else:
1606            self.flash(_('No ticket selected.'), type="warning")
1607            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1608            return
1609        if not isinstance(child_id, list):
1610            child_id = [child_id]
1611        deleted = []
1612        for id in child_id:
1613            del self.context[id]
1614            deleted.append(id)
1615        if len(deleted):
1616            self.flash(_('Successfully removed: ${a}',
1617                mapping = {'a':', '.join(deleted)}))
1618            self.context.writeLogMessage(
1619                self,'removed: %s at %s' %
1620                (', '.join(deleted), self.context.level))
1621        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1622        return
1623
1624class ValidateCoursesView(UtilityView, grok.View):
1625    """ Validate course list by course adviser
1626    """
1627    grok.context(IStudentStudyLevel)
1628    grok.name('validate_courses')
1629    grok.require('waeup.validateStudent')
1630
1631    def update(self):
1632        if not self.context.__parent__.is_current:
1633            emit_lock_message(self)
1634            return
1635        if str(self.context.student.current_level) != self.context.__name__:
1636            self.flash(_('This is not the student\'s current level.'),
1637                       type="danger")
1638        elif self.context.student.state == REGISTERED:
1639            IWorkflowInfo(self.context.student).fireTransition(
1640                'validate_courses')
1641            self.flash(_('Course list has been validated.'))
1642        else:
1643            self.flash(_('Student is in the wrong state.'), type="warning")
1644        self.redirect(self.url(self.context))
1645        return
1646
1647    def render(self):
1648        return
1649
1650class RejectCoursesView(UtilityView, grok.View):
1651    """ Reject course list by course adviser
1652    """
1653    grok.context(IStudentStudyLevel)
1654    grok.name('reject_courses')
1655    grok.require('waeup.validateStudent')
1656
1657    def update(self):
1658        if not self.context.__parent__.is_current:
1659            emit_lock_message(self)
1660            return
1661        if str(self.context.__parent__.current_level) != self.context.__name__:
1662            self.flash(_('This is not the student\'s current level.'),
1663                       type="danger")
1664            self.redirect(self.url(self.context))
1665            return
1666        elif self.context.student.state == VALIDATED:
1667            IWorkflowInfo(self.context.student).fireTransition('reset8')
1668            message = _('Course list request has been annulled.')
1669            self.flash(message)
1670        elif self.context.student.state == REGISTERED:
1671            IWorkflowInfo(self.context.student).fireTransition('reset7')
1672            message = _('Course list has been unregistered.')
1673            self.flash(message)
1674        else:
1675            self.flash(_('Student is in the wrong state.'), type="warning")
1676            self.redirect(self.url(self.context))
1677            return
1678        args = {'subject':message}
1679        self.redirect(self.url(self.context.student) +
1680            '/contactstudent?%s' % urlencode(args))
1681        return
1682
1683    def render(self):
1684        return
1685
1686class UnregisterCoursesView(UtilityView, grok.View):
1687    """Unregister course list by student
1688    """
1689    grok.context(IStudentStudyLevel)
1690    grok.name('unregister_courses')
1691    grok.require('waeup.handleStudent')
1692
1693    def update(self):
1694        if not self.context.__parent__.is_current:
1695            emit_lock_message(self)
1696            return
1697        try:
1698            deadline = grok.getSite()['configuration'][
1699                str(self.context.level_session)].coursereg_deadline
1700        except (TypeError, KeyError):
1701            deadline = None
1702        if deadline and deadline < datetime.now(pytz.utc):
1703            self.flash(_(
1704                "Course registration has ended. "
1705                "Unregistration is disabled."), type="warning")
1706        elif str(self.context.__parent__.current_level) != self.context.__name__:
1707            self.flash(_('This is not your current level.'), type="danger")
1708        elif self.context.student.state == REGISTERED:
1709            IWorkflowInfo(self.context.student).fireTransition('reset7')
1710            message = _('Course list has been unregistered.')
1711            self.flash(message)
1712        else:
1713            self.flash(_('You are in the wrong state.'), type="warning")
1714        self.redirect(self.url(self.context))
1715        return
1716
1717    def render(self):
1718        return
1719
1720class CourseTicketAddFormPage(KofaAddFormPage):
1721    """Add a course ticket.
1722    """
1723    grok.context(IStudentStudyLevel)
1724    grok.name('add')
1725    grok.require('waeup.manageStudent')
1726    label = _('Add course ticket')
1727    form_fields = grok.AutoFields(ICourseTicketAdd)
1728    pnav = 4
1729
1730    def update(self):
1731        if not self.context.__parent__.is_current \
1732            or self.context.student.studycourse_locked:
1733            emit_lock_message(self)
1734            return
1735        super(CourseTicketAddFormPage, self).update()
1736        return
1737
1738    @action(_('Add course ticket'), style='primary')
1739    def addCourseTicket(self, **data):
1740        course = data['course']
1741        success = addCourseTicket(self, course)
1742        if success:
1743            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1744        return
1745
1746    @action(_('Cancel'), validator=NullValidator)
1747    def cancel(self, **data):
1748        self.redirect(self.url(self.context))
1749
1750class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1751    """ Page to display course tickets
1752    """
1753    grok.context(ICourseTicket)
1754    grok.name('index')
1755    grok.require('waeup.viewStudent')
1756    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1757    grok.template('courseticketpage')
1758    pnav = 4
1759
1760    @property
1761    def label(self):
1762        return _('${a}: Course Ticket ${b}', mapping = {
1763            'a':self.context.student.display_fullname,
1764            'b':self.context.code})
1765
1766class CourseTicketManageFormPage(KofaEditFormPage):
1767    """ Page to manage course tickets
1768    """
1769    grok.context(ICourseTicket)
1770    grok.name('manage')
1771    grok.require('waeup.manageStudent')
1772    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1773    form_fields['title'].for_display = True
1774    form_fields['fcode'].for_display = True
1775    form_fields['dcode'].for_display = True
1776    form_fields['semester'].for_display = True
1777    form_fields['passmark'].for_display = True
1778    form_fields['credits'].for_display = True
1779    form_fields['mandatory'].for_display = False
1780    form_fields['automatic'].for_display = True
1781    form_fields['carry_over'].for_display = True
1782    pnav = 4
1783    grok.template('courseticketmanagepage')
1784
1785    def update(self):
1786        if not self.context.__parent__.__parent__.is_current \
1787            or self.context.student.studycourse_locked:
1788            emit_lock_message(self)
1789            return
1790        super(CourseTicketManageFormPage, self).update()
1791        return
1792
1793    @property
1794    def label(self):
1795        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1796
1797    @action('Save', style='primary')
1798    def save(self, **data):
1799        msave(self, **data)
1800        return
1801
1802class PaymentsManageFormPage(KofaEditFormPage):
1803    """ Page to manage the student payments
1804
1805    This manage form page is for both students and students officers.
1806    """
1807    grok.context(IStudentPaymentsContainer)
1808    grok.name('index')
1809    grok.require('waeup.viewStudent')
1810    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1811    grok.template('paymentsmanagepage')
1812    pnav = 4
1813
1814    @property
1815    def manage_payments_allowed(self):
1816        return checkPermission('waeup.payStudent', self.context)
1817
1818    def unremovable(self, ticket):
1819        usertype = getattr(self.request.principal, 'user_type', None)
1820        if not usertype:
1821            return False
1822        if not self.manage_payments_allowed:
1823            return True
1824        return (self.request.principal.user_type == 'student' and ticket.r_code)
1825
1826    @property
1827    def label(self):
1828        return _('${a}: Payments',
1829            mapping = {'a':self.context.__parent__.display_fullname})
1830
1831    @jsaction(_('Remove selected tickets'))
1832    def delPaymentTicket(self, **data):
1833        form = self.request.form
1834        if 'val_id' in form:
1835            child_id = form['val_id']
1836        else:
1837            self.flash(_('No payment selected.'), type="warning")
1838            self.redirect(self.url(self.context))
1839            return
1840        if not isinstance(child_id, list):
1841            child_id = [child_id]
1842        deleted = []
1843        for id in child_id:
1844            # Students are not allowed to remove used payment tickets
1845            ticket = self.context.get(id, None)
1846            if ticket is not None and not self.unremovable(ticket):
1847                del self.context[id]
1848                deleted.append(id)
1849        if len(deleted):
1850            self.flash(_('Successfully removed: ${a}',
1851                mapping = {'a': ', '.join(deleted)}))
1852            self.context.writeLogMessage(
1853                self,'removed: %s' % ', '.join(deleted))
1854        self.redirect(self.url(self.context))
1855        return
1856
1857    #@action(_('Add online payment ticket'))
1858    #def addPaymentTicket(self, **data):
1859    #    self.redirect(self.url(self.context, '@@addop'))
1860
1861class OnlinePaymentAddFormPage(KofaAddFormPage):
1862    """ Page to add an online payment ticket
1863    """
1864    grok.context(IStudentPaymentsContainer)
1865    grok.name('addop')
1866    grok.template('onlinepaymentaddform')
1867    grok.require('waeup.payStudent')
1868    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1869        'p_category')
1870    label = _('Add online payment')
1871    pnav = 4
1872
1873    @property
1874    def selectable_categories(self):
1875        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1876        return sorted(categories.items(), key=lambda value: value[1])
1877
1878    @action(_('Create ticket'), style='primary')
1879    def createTicket(self, **data):
1880        p_category = data['p_category']
1881        previous_session = data.get('p_session', None)
1882        previous_level = data.get('p_level', None)
1883        student = self.context.__parent__
1884        # The hostel_application payment category is temporarily used
1885        # by Uniben.
1886        if p_category in ('bed_allocation', 'hostel_application') and student[
1887            'studycourse'].current_session != grok.getSite()[
1888            'hostels'].accommodation_session:
1889                self.flash(
1890                    _('Your current session does not match ' + \
1891                    'accommodation session.'), type="danger")
1892                return
1893        if 'maintenance' in p_category:
1894            current_session = str(student['studycourse'].current_session)
1895            if not current_session in student['accommodation']:
1896                self.flash(_('You have not yet booked accommodation.'),
1897                           type="warning")
1898                return
1899        students_utils = getUtility(IStudentsUtils)
1900        error, payment = students_utils.setPaymentDetails(
1901            p_category, student, previous_session, previous_level)
1902        if error is not None:
1903            self.flash(error, type="danger")
1904            return
1905        if p_category == 'transfer':
1906            payment.p_item = self.request.form['new_programme']
1907        self.context[payment.p_id] = payment
1908        self.flash(_('Payment ticket created.'))
1909        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1910        self.redirect(self.url(self.context))
1911        return
1912
1913    @action(_('Cancel'), validator=NullValidator)
1914    def cancel(self, **data):
1915        self.redirect(self.url(self.context))
1916
1917class PreviousPaymentAddFormPage(KofaAddFormPage):
1918    """ Page to add an online payment ticket for previous sessions.
1919    """
1920    grok.context(IStudentPaymentsContainer)
1921    grok.name('addpp')
1922    grok.require('waeup.payStudent')
1923    form_fields = grok.AutoFields(IStudentPreviousPayment)
1924    label = _('Add previous session online payment')
1925    pnav = 4
1926
1927    def update(self):
1928        if self.context.student.before_payment:
1929            self.flash(_("No previous payment to be made."), type="warning")
1930            self.redirect(self.url(self.context))
1931        super(PreviousPaymentAddFormPage, self).update()
1932        return
1933
1934    @action(_('Create ticket'), style='primary')
1935    def createTicket(self, **data):
1936        p_category = data['p_category']
1937        previous_session = data.get('p_session', None)
1938        previous_level = data.get('p_level', None)
1939        student = self.context.__parent__
1940        students_utils = getUtility(IStudentsUtils)
1941        error, payment = students_utils.setPaymentDetails(
1942            p_category, student, previous_session, previous_level)
1943        if error is not None:
1944            self.flash(error, type="danger")
1945            return
1946        self.context[payment.p_id] = payment
1947        self.flash(_('Payment ticket created.'))
1948        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1949        self.redirect(self.url(self.context))
1950        return
1951
1952    @action(_('Cancel'), validator=NullValidator)
1953    def cancel(self, **data):
1954        self.redirect(self.url(self.context))
1955
1956class BalancePaymentAddFormPage(KofaAddFormPage):
1957    """ Page to add an online payment which can balance s previous session
1958    payment.
1959    """
1960    grok.context(IStudentPaymentsContainer)
1961    grok.name('addbp')
1962    grok.require('waeup.manageStudent')
1963    form_fields = grok.AutoFields(IStudentBalancePayment)
1964    label = _('Add balance')
1965    pnav = 4
1966
1967    @action(_('Create ticket'), style='primary')
1968    def createTicket(self, **data):
1969        p_category = data['p_category']
1970        balance_session = data.get('balance_session', None)
1971        balance_level = data.get('balance_level', None)
1972        balance_amount = data.get('balance_amount', None)
1973        student = self.context.__parent__
1974        students_utils = getUtility(IStudentsUtils)
1975        error, payment = students_utils.setBalanceDetails(
1976            p_category, student, balance_session,
1977            balance_level, balance_amount)
1978        if error is not None:
1979            self.flash(error, type="danger")
1980            return
1981        self.context[payment.p_id] = payment
1982        self.flash(_('Payment ticket created.'))
1983        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1984        self.redirect(self.url(self.context))
1985        return
1986
1987    @action(_('Cancel'), validator=NullValidator)
1988    def cancel(self, **data):
1989        self.redirect(self.url(self.context))
1990
1991class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1992    """ Page to view an online payment ticket
1993    """
1994    grok.context(IStudentOnlinePayment)
1995    grok.name('index')
1996    grok.require('waeup.viewStudent')
1997    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1998    form_fields[
1999        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2000    form_fields[
2001        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2002    pnav = 4
2003
2004    @property
2005    def label(self):
2006        return _('${a}: Online Payment Ticket ${b}', mapping = {
2007            'a':self.context.student.display_fullname,
2008            'b':self.context.p_id})
2009
2010class OnlinePaymentApproveView(UtilityView, grok.View):
2011    """ Callback view
2012    """
2013    grok.context(IStudentOnlinePayment)
2014    grok.name('approve')
2015    grok.require('waeup.managePortal')
2016
2017    def update(self):
2018        flashtype, msg, log = self.context.approveStudentPayment()
2019        if log is not None:
2020            # Add log message to students.log
2021            self.context.writeLogMessage(self,log)
2022            # Add log message to payments.log
2023            self.context.logger.info(
2024                '%s,%s,%s,%s,%s,,,,,,' % (
2025                self.context.student.student_id,
2026                self.context.p_id, self.context.p_category,
2027                self.context.amount_auth, self.context.r_code))
2028        self.flash(msg, type=flashtype)
2029        return
2030
2031    def render(self):
2032        self.redirect(self.url(self.context, '@@index'))
2033        return
2034
2035class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
2036    """ Approval view for students.
2037
2038    This view is used for browser tests only and
2039    must be neutralized in custom pages!
2040    """
2041    grok.name('fake_approve')
2042    grok.require('waeup.payStudent')
2043
2044class ExportPDFPaymentSlip(UtilityView, grok.View):
2045    """Deliver a PDF slip of the context.
2046    """
2047    grok.context(IStudentOnlinePayment)
2048    grok.name('payment_slip.pdf')
2049    grok.require('waeup.viewStudent')
2050    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
2051    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2052    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2053    prefix = 'form'
2054    note = None
2055    omit_fields = (
2056        'password', 'suspended', 'phone', 'date_of_birth',
2057        'adm_code', 'sex', 'suspended_comment', 'current_level',
2058        'flash_notice')
2059
2060    @property
2061    def title(self):
2062        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2063        return translate(_('Payment Data'), 'waeup.kofa',
2064            target_language=portal_language)
2065
2066    @property
2067    def label(self):
2068        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2069        return translate(_('Online Payment Slip'),
2070            'waeup.kofa', target_language=portal_language) \
2071            + ' %s' % self.context.p_id
2072
2073    def render(self):
2074        #if self.context.p_state != 'paid':
2075        #    self.flash('Ticket not yet paid.')
2076        #    self.redirect(self.url(self.context))
2077        #    return
2078        studentview = StudentBasePDFFormPage(self.context.student,
2079            self.request, self.omit_fields)
2080        students_utils = getUtility(IStudentsUtils)
2081        return students_utils.renderPDF(self, 'payment_slip.pdf',
2082            self.context.student, studentview, note=self.note,
2083            omit_fields=self.omit_fields)
2084
2085
2086class AccommodationManageFormPage(KofaEditFormPage):
2087    """ Page to manage bed tickets.
2088
2089    This manage form page is for both students and students officers.
2090    """
2091    grok.context(IStudentAccommodation)
2092    grok.name('index')
2093    grok.require('waeup.handleAccommodation')
2094    form_fields = grok.AutoFields(IStudentAccommodation)
2095    grok.template('accommodationmanagepage')
2096    pnav = 4
2097    with_hostel_selection = True
2098
2099    @property
2100    def actionsgroup1(self):
2101        if not self.with_hostel_selection:
2102            return []
2103        students_utils = getUtility(IStudentsUtils)
2104        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2105        error_message = students_utils.checkAccommodationRequirements(
2106            self.context.student, acc_details)
2107        if error_message:
2108            return []
2109        return [_('Save')]
2110
2111    @property
2112    def actionsgroup2(self):
2113        if getattr(self.request.principal, 'user_type', None) == 'student':
2114            return [_('Book accommodation')]
2115        return [_('Book accommodation'), _('Remove selected')]
2116
2117    @property
2118    def label(self):
2119        return _('${a}: Accommodation',
2120            mapping = {'a':self.context.__parent__.display_fullname})
2121
2122    @property
2123    def desired_hostel(self):
2124        if self.context.desired_hostel:
2125            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2126            if hostel is not None:
2127                return hostel.hostel_name
2128        return
2129
2130    def getHostels(self):
2131        """Get a list of all stored hostels.
2132        """
2133        yield(dict(name=None, title='--', selected=''))
2134        for val in grok.getSite()['hostels'].values():
2135            selected = ''
2136            if val.hostel_id == self.context.desired_hostel:
2137                selected = 'selected'
2138            yield(dict(name=val.hostel_id, title=val.hostel_name,
2139                       selected=selected))
2140
2141    @action(_('Save'), style='primary')
2142    def save(self):
2143        hostel = self.request.form.get('hostel', None)
2144        self.context.desired_hostel = hostel
2145        self.flash(_('Your selection has been saved.'))
2146        return
2147
2148    @action(_('Book accommodation'), style='primary')
2149    def bookAccommodation(self, **data):
2150        self.redirect(self.url(self.context, 'add'))
2151        return
2152
2153    @jsaction(_('Remove selected'))
2154    def delBedTickets(self, **data):
2155        if getattr(self.request.principal, 'user_type', None) == 'student':
2156            self.flash(_('You are not allowed to remove bed tickets.'),
2157                       type="warning")
2158            self.redirect(self.url(self.context))
2159            return
2160        form = self.request.form
2161        if 'val_id' in form:
2162            child_id = form['val_id']
2163        else:
2164            self.flash(_('No bed ticket selected.'), type="warning")
2165            self.redirect(self.url(self.context))
2166            return
2167        if not isinstance(child_id, list):
2168            child_id = [child_id]
2169        deleted = []
2170        for id in child_id:
2171            del self.context[id]
2172            deleted.append(id)
2173        if len(deleted):
2174            self.flash(_('Successfully removed: ${a}',
2175                mapping = {'a':', '.join(deleted)}))
2176            self.context.writeLogMessage(
2177                self,'removed: % s' % ', '.join(deleted))
2178        self.redirect(self.url(self.context))
2179        return
2180
2181class BedTicketAddPage(KofaPage):
2182    """ Page to add a bed ticket
2183    """
2184    grok.context(IStudentAccommodation)
2185    grok.name('add')
2186    grok.require('waeup.handleAccommodation')
2187    grok.template('enterpin')
2188    ac_prefix = 'HOS'
2189    label = _('Add bed ticket')
2190    pnav = 4
2191    buttonname = _('Create bed ticket')
2192    notice = ''
2193    with_ac = True
2194
2195    def update(self, SUBMIT=None):
2196        student = self.context.student
2197        students_utils = getUtility(IStudentsUtils)
2198        acc_details  = students_utils.getAccommodationDetails(student)
2199        error_message = students_utils.checkAccommodationRequirements(
2200            student, acc_details)
2201        if error_message:
2202            self.flash(error_message, type="warning")
2203            self.redirect(self.url(self.context))
2204            return
2205        if self.with_ac:
2206            self.ac_series = self.request.form.get('ac_series', None)
2207            self.ac_number = self.request.form.get('ac_number', None)
2208        if SUBMIT is None:
2209            return
2210        if self.with_ac:
2211            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2212            code = get_access_code(pin)
2213            if not code:
2214                self.flash(_('Activation code is invalid.'), type="warning")
2215                return
2216        # Search and book bed
2217        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2218        entries = cat.searchResults(
2219            owner=(student.student_id,student.student_id))
2220        if len(entries):
2221            # If bed space has been manually allocated use this bed
2222            manual = True
2223            bed = [entry for entry in entries][0]
2224            # Safety belt for paranoids: Does this bed really exist on portal?
2225            # XXX: Can be remove if nobody complains.
2226            if bed.__parent__.__parent__ is None:
2227                self.flash(_('System error: Please contact the adminsitrator.'),
2228                           type="danger")
2229                self.context.writeLogMessage(
2230                    self, 'fatal error: %s' % bed.bed_id)
2231                return
2232        else:
2233            # else search for other available beds
2234            manual = False
2235            entries = cat.searchResults(
2236                bed_type=(acc_details['bt'],acc_details['bt']))
2237            available_beds = [
2238                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2239            if available_beds:
2240                students_utils = getUtility(IStudentsUtils)
2241                bed = students_utils.selectBed(
2242                    available_beds, self.context.desired_hostel)
2243                if bed is None:
2244                    self.flash(_(
2245                        'There is no free bed in your desired hostel. '
2246                        'Please try another hostel.'),
2247                        type="warning")
2248                    self.redirect(self.url(self.context))
2249                    return
2250                # Safety belt for paranoids: Does this bed really exist
2251                # in portal?
2252                # XXX: Can be remove if nobody complains.
2253                if bed.__parent__.__parent__ is None:
2254                    self.flash(_(
2255                        'System error: Please contact the administrator.'),
2256                        type="warning")
2257                    self.context.writeLogMessage(
2258                        self, 'fatal error: %s' % bed.bed_id)
2259                    return
2260                bed.bookBed(student.student_id)
2261            else:
2262                self.flash(_('There is no free bed in your category ${a}.',
2263                    mapping = {'a':acc_details['bt']}), type="warning")
2264                self.redirect(self.url(self.context))
2265                return
2266        if self.with_ac:
2267            # Mark pin as used (this also fires a pin related transition)
2268            if code.state == USED:
2269                self.flash(_('Activation code has already been used.'),
2270                           type="warning")
2271                if not manual:
2272                    # Release the previously booked bed
2273                    bed.owner = NOT_OCCUPIED
2274                    # Catalog must be informed
2275                    notify(grok.ObjectModifiedEvent(bed))
2276                return
2277            else:
2278                comment = _(u'invalidated')
2279                # Here we know that the ac is in state initialized so we do not
2280                # expect an exception, but the owner might be different
2281                success = invalidate_accesscode(
2282                    pin, comment, self.context.student.student_id)
2283                if not success:
2284                    self.flash(_('You are not the owner of this access code.'),
2285                               type="warning")
2286                    if not manual:
2287                        # Release the previously booked bed
2288                        bed.owner = NOT_OCCUPIED
2289                        # Catalog must be informed
2290                        notify(grok.ObjectModifiedEvent(bed))
2291                    return
2292        # Create bed ticket
2293        bedticket = createObject(u'waeup.BedTicket')
2294        if self.with_ac:
2295            bedticket.booking_code = pin
2296        bedticket.booking_session = acc_details['booking_session']
2297        bedticket.bed_type = acc_details['bt']
2298        bedticket.bed = bed
2299        hall_title = bed.__parent__.hostel_name
2300        coordinates = bed.coordinates[1:]
2301        block, room_nr, bed_nr = coordinates
2302        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2303            'a':hall_title, 'b':block,
2304            'c':room_nr, 'd':bed_nr,
2305            'e':bed.bed_type})
2306        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2307        bedticket.bed_coordinates = translate(
2308            bc, 'waeup.kofa',target_language=portal_language)
2309        self.context.addBedTicket(bedticket)
2310        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2311        self.flash(_('Bed ticket created and bed booked: ${a}',
2312            mapping = {'a':bedticket.display_coordinates}))
2313        self.redirect(self.url(self.context))
2314        return
2315
2316class BedTicketDisplayFormPage(KofaDisplayFormPage):
2317    """ Page to display bed tickets
2318    """
2319    grok.context(IBedTicket)
2320    grok.name('index')
2321    grok.require('waeup.handleAccommodation')
2322    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2323    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2324    pnav = 4
2325
2326    @property
2327    def label(self):
2328        return _('Bed Ticket for Session ${a}',
2329            mapping = {'a':self.context.getSessionString()})
2330
2331class ExportPDFBedTicketSlip(UtilityView, grok.View):
2332    """Deliver a PDF slip of the context.
2333    """
2334    grok.context(IBedTicket)
2335    grok.name('bed_allocation_slip.pdf')
2336    grok.require('waeup.handleAccommodation')
2337    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2338    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2339    prefix = 'form'
2340    omit_fields = (
2341        'password', 'suspended', 'phone', 'adm_code',
2342        'suspended_comment', 'date_of_birth', 'current_level',
2343        'flash_notice')
2344
2345    @property
2346    def title(self):
2347        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2348        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2349            target_language=portal_language)
2350
2351    @property
2352    def label(self):
2353        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2354        #return translate(_('Bed Allocation: '),
2355        #    'waeup.kofa', target_language=portal_language) \
2356        #    + ' %s' % self.context.bed_coordinates
2357        return translate(_('Bed Allocation Slip'),
2358            'waeup.kofa', target_language=portal_language) \
2359            + ' %s' % self.context.getSessionString()
2360
2361    def render(self):
2362        studentview = StudentBasePDFFormPage(self.context.student,
2363            self.request, self.omit_fields)
2364        students_utils = getUtility(IStudentsUtils)
2365        return students_utils.renderPDF(
2366            self, 'bed_allocation_slip.pdf',
2367            self.context.student, studentview,
2368            omit_fields=self.omit_fields)
2369
2370class BedTicketRelocationView(UtilityView, grok.View):
2371    """ Callback view
2372    """
2373    grok.context(IBedTicket)
2374    grok.name('relocate')
2375    grok.require('waeup.manageHostels')
2376
2377    # Relocate student if student parameters have changed or the bed_type
2378    # of the bed has changed
2379    def update(self):
2380        success, msg = self.context.relocateStudent()
2381        if not success:
2382            self.flash(msg, type="warning")
2383        else:
2384            self.flash(msg)
2385        self.redirect(self.url(self.context))
2386        return
2387
2388    def render(self):
2389        return
2390
2391class StudentHistoryPage(KofaPage):
2392    """ Page to display student history
2393    """
2394    grok.context(IStudent)
2395    grok.name('history')
2396    grok.require('waeup.viewStudent')
2397    grok.template('studenthistory')
2398    pnav = 4
2399
2400    @property
2401    def label(self):
2402        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2403
2404# Pages for students only
2405
2406class StudentBaseEditFormPage(KofaEditFormPage):
2407    """ View to edit student base data
2408    """
2409    grok.context(IStudent)
2410    grok.name('edit_base')
2411    grok.require('waeup.handleStudent')
2412    form_fields = grok.AutoFields(IStudentBase).select(
2413        'email', 'phone')
2414    label = _('Edit base data')
2415    pnav = 4
2416
2417    @action(_('Save'), style='primary')
2418    def save(self, **data):
2419        msave(self, **data)
2420        return
2421
2422class StudentChangePasswordPage(KofaEditFormPage):
2423    """ View to edit student passwords
2424    """
2425    grok.context(IStudent)
2426    grok.name('change_password')
2427    grok.require('waeup.handleStudent')
2428    grok.template('change_password')
2429    label = _('Change password')
2430    pnav = 4
2431
2432    @action(_('Save'), style='primary')
2433    def save(self, **data):
2434        form = self.request.form
2435        password = form.get('change_password', None)
2436        password_ctl = form.get('change_password_repeat', None)
2437        if password:
2438            validator = getUtility(IPasswordValidator)
2439            errors = validator.validate_password(password, password_ctl)
2440            if not errors:
2441                IUserAccount(self.context).setPassword(password)
2442                # Unset temporary password
2443                self.context.temp_password = None
2444                self.context.writeLogMessage(self, 'saved: password')
2445                self.flash(_('Password changed.'))
2446            else:
2447                self.flash( ' '.join(errors), type="warning")
2448        return
2449
2450class StudentFilesUploadPage(KofaPage):
2451    """ View to upload files by student
2452    """
2453    grok.context(IStudent)
2454    grok.name('change_portrait')
2455    grok.require('waeup.uploadStudentFile')
2456    grok.template('filesuploadpage')
2457    label = _('Upload portrait')
2458    pnav = 4
2459
2460    def update(self):
2461        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2462        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2463            emit_lock_message(self)
2464            return
2465        super(StudentFilesUploadPage, self).update()
2466        return
2467
2468class StartClearancePage(KofaPage):
2469    grok.context(IStudent)
2470    grok.name('start_clearance')
2471    grok.require('waeup.handleStudent')
2472    grok.template('enterpin')
2473    label = _('Start clearance')
2474    ac_prefix = 'CLR'
2475    notice = ''
2476    pnav = 4
2477    buttonname = _('Start clearance now')
2478    with_ac = True
2479
2480    @property
2481    def all_required_fields_filled(self):
2482        if not self.context.email:
2483            return _("Email address is missing."), 'edit_base'
2484        if not self.context.phone:
2485            return _("Phone number is missing."), 'edit_base'
2486        return
2487
2488    @property
2489    def portrait_uploaded(self):
2490        store = getUtility(IExtFileStore)
2491        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2492            return True
2493        return False
2494
2495    def update(self, SUBMIT=None):
2496        if not self.context.state == ADMITTED:
2497            self.flash(_("Wrong state"), type="warning")
2498            self.redirect(self.url(self.context))
2499            return
2500        if not self.portrait_uploaded:
2501            self.flash(_("No portrait uploaded."), type="warning")
2502            self.redirect(self.url(self.context, 'change_portrait'))
2503            return
2504        if self.all_required_fields_filled:
2505            arf_warning = self.all_required_fields_filled[0]
2506            arf_redirect = self.all_required_fields_filled[1]
2507            self.flash(arf_warning, type="warning")
2508            self.redirect(self.url(self.context, arf_redirect))
2509            return
2510        if self.with_ac:
2511            self.ac_series = self.request.form.get('ac_series', None)
2512            self.ac_number = self.request.form.get('ac_number', None)
2513        if SUBMIT is None:
2514            return
2515        if self.with_ac:
2516            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2517            code = get_access_code(pin)
2518            if not code:
2519                self.flash(_('Activation code is invalid.'), type="warning")
2520                return
2521            if code.state == USED:
2522                self.flash(_('Activation code has already been used.'),
2523                           type="warning")
2524                return
2525            # Mark pin as used (this also fires a pin related transition)
2526            # and fire transition start_clearance
2527            comment = _(u"invalidated")
2528            # Here we know that the ac is in state initialized so we do not
2529            # expect an exception, but the owner might be different
2530            if not invalidate_accesscode(pin, comment, self.context.student_id):
2531                self.flash(_('You are not the owner of this access code.'),
2532                           type="warning")
2533                return
2534            self.context.clr_code = pin
2535        IWorkflowInfo(self.context).fireTransition('start_clearance')
2536        self.flash(_('Clearance process has been started.'))
2537        self.redirect(self.url(self.context,'cedit'))
2538        return
2539
2540class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2541    """ View to edit student clearance data by student
2542    """
2543    grok.context(IStudent)
2544    grok.name('cedit')
2545    grok.require('waeup.handleStudent')
2546    label = _('Edit clearance data')
2547
2548    @property
2549    def form_fields(self):
2550        if self.context.is_postgrad:
2551            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2552                'clr_code', 'officer_comment')
2553        else:
2554            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2555                'clr_code', 'officer_comment')
2556        return form_fields
2557
2558    def update(self):
2559        if self.context.clearance_locked:
2560            emit_lock_message(self)
2561            return
2562        return super(StudentClearanceEditFormPage, self).update()
2563
2564    @action(_('Save'), style='primary')
2565    def save(self, **data):
2566        self.applyData(self.context, **data)
2567        self.flash(_('Clearance form has been saved.'))
2568        return
2569
2570    def dataNotComplete(self):
2571        """To be implemented in the customization package.
2572        """
2573        return False
2574
2575    @action(_('Save and request clearance'), style='primary',
2576            warning=_('You can not edit your data after '
2577            'requesting clearance. You really want to request clearance now?'))
2578    def requestClearance(self, **data):
2579        self.applyData(self.context, **data)
2580        if self.dataNotComplete():
2581            self.flash(self.dataNotComplete(), type="warning")
2582            return
2583        self.flash(_('Clearance form has been saved.'))
2584        if self.context.clr_code:
2585            self.redirect(self.url(self.context, 'request_clearance'))
2586        else:
2587            # We bypass the request_clearance page if student
2588            # has been imported in state 'clearance started' and
2589            # no clr_code was entered before.
2590            state = IWorkflowState(self.context).getState()
2591            if state != CLEARANCE:
2592                # This shouldn't happen, but the application officer
2593                # might have forgotten to lock the form after changing the state
2594                self.flash(_('This form cannot be submitted. Wrong state!'),
2595                           type="danger")
2596                return
2597            IWorkflowInfo(self.context).fireTransition('request_clearance')
2598            self.flash(_('Clearance has been requested.'))
2599            self.redirect(self.url(self.context))
2600        return
2601
2602class RequestClearancePage(KofaPage):
2603    grok.context(IStudent)
2604    grok.name('request_clearance')
2605    grok.require('waeup.handleStudent')
2606    grok.template('enterpin')
2607    label = _('Request clearance')
2608    notice = _('Enter the CLR access code used for starting clearance.')
2609    ac_prefix = 'CLR'
2610    pnav = 4
2611    buttonname = _('Request clearance now')
2612    with_ac = True
2613
2614    def update(self, SUBMIT=None):
2615        if self.with_ac:
2616            self.ac_series = self.request.form.get('ac_series', None)
2617            self.ac_number = self.request.form.get('ac_number', None)
2618        if SUBMIT is None:
2619            return
2620        if self.with_ac:
2621            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2622            if self.context.clr_code and self.context.clr_code != pin:
2623                self.flash(_("This isn't your CLR access code."), type="danger")
2624                return
2625        state = IWorkflowState(self.context).getState()
2626        if state != CLEARANCE:
2627            # This shouldn't happen, but the application officer
2628            # might have forgotten to lock the form after changing the state
2629            self.flash(_('This form cannot be submitted. Wrong state!'),
2630                       type="danger")
2631            return
2632        IWorkflowInfo(self.context).fireTransition('request_clearance')
2633        self.flash(_('Clearance has been requested.'))
2634        self.redirect(self.url(self.context))
2635        return
2636
2637class StartSessionPage(KofaPage):
2638    grok.context(IStudentStudyCourse)
2639    grok.name('start_session')
2640    grok.require('waeup.handleStudent')
2641    grok.template('enterpin')
2642    label = _('Start session')
2643    ac_prefix = 'SFE'
2644    notice = ''
2645    pnav = 4
2646    buttonname = _('Start now')
2647    with_ac = True
2648
2649    def update(self, SUBMIT=None):
2650        if not self.context.is_current:
2651            emit_lock_message(self)
2652            return
2653        super(StartSessionPage, self).update()
2654        if not self.context.next_session_allowed:
2655            self.flash(_("You are not entitled to start session."),
2656                       type="warning")
2657            self.redirect(self.url(self.context))
2658            return
2659        if self.with_ac:
2660            self.ac_series = self.request.form.get('ac_series', None)
2661            self.ac_number = self.request.form.get('ac_number', None)
2662        if SUBMIT is None:
2663            return
2664        if self.with_ac:
2665            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2666            code = get_access_code(pin)
2667            if not code:
2668                self.flash(_('Activation code is invalid.'), type="warning")
2669                return
2670            # Mark pin as used (this also fires a pin related transition)
2671            if code.state == USED:
2672                self.flash(_('Activation code has already been used.'),
2673                           type="warning")
2674                return
2675            else:
2676                comment = _(u"invalidated")
2677                # Here we know that the ac is in state initialized so we do not
2678                # expect an error, but the owner might be different
2679                if not invalidate_accesscode(
2680                    pin,comment,self.context.student.student_id):
2681                    self.flash(_('You are not the owner of this access code.'),
2682                               type="warning")
2683                    return
2684        try:
2685            if self.context.student.state == CLEARED:
2686                IWorkflowInfo(self.context.student).fireTransition(
2687                    'pay_first_school_fee')
2688            elif self.context.student.state == RETURNING:
2689                IWorkflowInfo(self.context.student).fireTransition(
2690                    'pay_school_fee')
2691            elif self.context.student.state == PAID:
2692                IWorkflowInfo(self.context.student).fireTransition(
2693                    'pay_pg_fee')
2694        except ConstraintNotSatisfied:
2695            self.flash(_('An error occurred, please contact the system administrator.'),
2696                       type="danger")
2697            return
2698        self.flash(_('Session started.'))
2699        self.redirect(self.url(self.context))
2700        return
2701
2702class AddStudyLevelFormPage(KofaEditFormPage):
2703    """ Page for students to add current study levels
2704    """
2705    grok.context(IStudentStudyCourse)
2706    grok.name('add')
2707    grok.require('waeup.handleStudent')
2708    grok.template('studyleveladdpage')
2709    form_fields = grok.AutoFields(IStudentStudyCourse)
2710    pnav = 4
2711
2712    @property
2713    def label(self):
2714        studylevelsource = StudyLevelSource().factory
2715        code = self.context.current_level
2716        title = studylevelsource.getTitle(self.context, code)
2717        return _('Add current level ${a}', mapping = {'a':title})
2718
2719    def update(self):
2720        if not self.context.is_current \
2721            or self.context.student.studycourse_locked:
2722            emit_lock_message(self)
2723            return
2724        if self.context.student.state != PAID:
2725            emit_lock_message(self)
2726            return
2727        code = self.context.current_level
2728        if code is None:
2729            self.flash(_('Your data are incomplete'), type="danger")
2730            self.redirect(self.url(self.context))
2731            return
2732        super(AddStudyLevelFormPage, self).update()
2733        return
2734
2735    @action(_('Create course list now'), style='primary')
2736    def addStudyLevel(self, **data):
2737        studylevel = createObject(u'waeup.StudentStudyLevel')
2738        studylevel.level = self.context.current_level
2739        studylevel.level_session = self.context.current_session
2740        try:
2741            self.context.addStudentStudyLevel(
2742                self.context.certificate,studylevel)
2743        except KeyError:
2744            self.flash(_('This level exists.'), type="warning")
2745            self.redirect(self.url(self.context))
2746            return
2747        except RequiredMissing:
2748            self.flash(_('Your data are incomplete.'), type="danger")
2749            self.redirect(self.url(self.context))
2750            return
2751        self.flash(_('You successfully created a new course list.'))
2752        self.redirect(self.url(self.context, str(studylevel.level)))
2753        return
2754
2755class StudyLevelEditFormPage(KofaEditFormPage):
2756    """ Page to edit the student study level data by students
2757    """
2758    grok.context(IStudentStudyLevel)
2759    grok.name('edit')
2760    grok.require('waeup.editStudyLevel')
2761    grok.template('studyleveleditpage')
2762    pnav = 4
2763    placeholder = _('Enter valid course code')
2764
2765    def update(self, ADD=None, course=None):
2766        if not self.context.__parent__.is_current:
2767            emit_lock_message(self)
2768            return
2769        if self.context.student.state != PAID or \
2770            not self.context.is_current_level:
2771            emit_lock_message(self)
2772            return
2773        super(StudyLevelEditFormPage, self).update()
2774        if ADD is not None:
2775            if not course:
2776                self.flash(_('No valid course code entered.'), type="warning")
2777                return
2778            cat = queryUtility(ICatalog, name='courses_catalog')
2779            result = cat.searchResults(code=(course, course))
2780            if len(result) != 1:
2781                self.flash(_('Course not found.'), type="warning")
2782                return
2783            course = list(result)[0]
2784            addCourseTicket(self, course)
2785        return
2786
2787    @property
2788    def label(self):
2789        # Here we know that the cookie has been set
2790        lang = self.request.cookies.get('kofa.language')
2791        level_title = translate(self.context.level_title, 'waeup.kofa',
2792            target_language=lang)
2793        return _('Edit course list of ${a}',
2794            mapping = {'a':level_title})
2795
2796    @property
2797    def translated_values(self):
2798        return translated_values(self)
2799
2800    def _delCourseTicket(self, **data):
2801        form = self.request.form
2802        if 'val_id' in form:
2803            child_id = form['val_id']
2804        else:
2805            self.flash(_('No ticket selected.'), type="warning")
2806            self.redirect(self.url(self.context, '@@edit'))
2807            return
2808        if not isinstance(child_id, list):
2809            child_id = [child_id]
2810        deleted = []
2811        for id in child_id:
2812            # Students are not allowed to remove core tickets
2813            if id in self.context and \
2814                self.context[id].removable_by_student:
2815                del self.context[id]
2816                deleted.append(id)
2817        if len(deleted):
2818            self.flash(_('Successfully removed: ${a}',
2819                mapping = {'a':', '.join(deleted)}))
2820            self.context.writeLogMessage(
2821                self,'removed: %s at %s' %
2822                (', '.join(deleted), self.context.level))
2823        self.redirect(self.url(self.context, u'@@edit'))
2824        return
2825
2826    @jsaction(_('Remove selected tickets'))
2827    def delCourseTicket(self, **data):
2828        self._delCourseTicket(**data)
2829        return
2830
2831    def _updateTickets(self, **data):
2832        cat = queryUtility(ICatalog, name='courses_catalog')
2833        invalidated = list()
2834        for value in self.context.values():
2835            result = cat.searchResults(code=(value.code, value.code))
2836            if len(result) != 1:
2837                course = None
2838            else:
2839                course = list(result)[0]
2840            invalid = self.context.updateCourseTicket(value, course)
2841            if invalid:
2842                invalidated.append(invalid)
2843        if invalidated:
2844            invalidated_string = ', '.join(invalidated)
2845            self.context.writeLogMessage(
2846                self, 'course tickets invalidated: %s' % invalidated_string)
2847        self.flash(_('All course tickets updated.'))
2848        return
2849
2850    @action(_('Update all tickets'),
2851        tooltip=_('Update all course parameters including course titles.'))
2852    def updateTickets(self, **data):
2853        self._updateTickets(**data)
2854        return
2855
2856    def _registerCourses(self, **data):
2857        if self.context.student.is_postgrad and \
2858            not self.context.student.is_special_postgrad:
2859            self.flash(_(
2860                "You are a postgraduate student, "
2861                "your course list can't bee registered."), type="warning")
2862            self.redirect(self.url(self.context))
2863            return
2864        students_utils = getUtility(IStudentsUtils)
2865        warning = students_utils.warnCreditsOOR(self.context)
2866        if warning:
2867            self.flash(warning, type="warning")
2868            return
2869        msg = self.context.course_registration_forbidden
2870        if msg:
2871            self.flash(msg, type="warning")
2872            return
2873        IWorkflowInfo(self.context.student).fireTransition(
2874            'register_courses')
2875        self.flash(_('Course list has been registered.'))
2876        self.redirect(self.url(self.context))
2877        return
2878
2879    @action(_('Register course list'), style='primary',
2880        warning=_('You can not edit your course list after registration.'
2881            ' You really want to register?'))
2882    def registerCourses(self, **data):
2883        self._registerCourses(**data)
2884        return
2885
2886class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2887    """Add a course ticket by student.
2888    """
2889    grok.name('ctadd')
2890    grok.require('waeup.handleStudent')
2891    form_fields = grok.AutoFields(ICourseTicketAdd)
2892
2893    def update(self):
2894        if self.context.student.state != PAID or \
2895            not self.context.is_current_level:
2896            emit_lock_message(self)
2897            return
2898        super(CourseTicketAddFormPage2, self).update()
2899        return
2900
2901    @action(_('Add course ticket'))
2902    def addCourseTicket(self, **data):
2903        # Safety belt
2904        if self.context.student.state != PAID:
2905            return
2906        course = data['course']
2907        success = addCourseTicket(self, course)
2908        if success:
2909            self.redirect(self.url(self.context, u'@@edit'))
2910        return
2911
2912class SetPasswordPage(KofaPage):
2913    grok.context(IKofaObject)
2914    grok.name('setpassword')
2915    grok.require('waeup.Anonymous')
2916    grok.template('setpassword')
2917    label = _('Set password for first-time login')
2918    ac_prefix = 'PWD'
2919    pnav = 0
2920    set_button = _('Set')
2921
2922    def update(self, SUBMIT=None):
2923        self.reg_number = self.request.form.get('reg_number', None)
2924        self.ac_series = self.request.form.get('ac_series', None)
2925        self.ac_number = self.request.form.get('ac_number', None)
2926
2927        if SUBMIT is None:
2928            return
2929        hitlist = search(query=self.reg_number,
2930            searchtype='reg_number', view=self)
2931        if not hitlist:
2932            self.flash(_('No student found.'), type="warning")
2933            return
2934        if len(hitlist) != 1:   # Cannot happen but anyway
2935            self.flash(_('More than one student found.'), type="warning")
2936            return
2937        student = hitlist[0].context
2938        self.student_id = student.student_id
2939        student_pw = student.password
2940        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2941        code = get_access_code(pin)
2942        if not code:
2943            self.flash(_('Access code is invalid.'), type="warning")
2944            return
2945        if student_pw and pin == student.adm_code:
2946            self.flash(_(
2947                'Password has already been set. Your Student Id is ${a}',
2948                mapping = {'a':self.student_id}))
2949            return
2950        elif student_pw:
2951            self.flash(
2952                _('Password has already been set. You are using the ' +
2953                'wrong Access Code.'), type="warning")
2954            return
2955        # Mark pin as used (this also fires a pin related transition)
2956        # and set student password
2957        if code.state == USED:
2958            self.flash(_('Access code has already been used.'), type="warning")
2959            return
2960        else:
2961            comment = _(u"invalidated")
2962            # Here we know that the ac is in state initialized so we do not
2963            # expect an exception
2964            invalidate_accesscode(pin,comment)
2965            IUserAccount(student).setPassword(self.ac_number)
2966            student.adm_code = pin
2967        self.flash(_('Password has been set. Your Student Id is ${a}',
2968            mapping = {'a':self.student_id}))
2969        return
2970
2971class StudentRequestPasswordPage(KofaAddFormPage):
2972    """Captcha'd request password page for students.
2973    """
2974    grok.name('requestpw')
2975    grok.require('waeup.Anonymous')
2976    grok.template('requestpw')
2977    form_fields = grok.AutoFields(IStudentRequestPW).select(
2978        'lastname','number','email')
2979    label = _('Request password for first-time login')
2980
2981    def update(self):
2982        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2983        if blocker:
2984            self.flash(_('The portal is in maintenance mode. '
2985                        'Password request forms are temporarily disabled.'),
2986                       type='warning')
2987            self.redirect(self.url(self.context))
2988            return
2989        # Handle captcha
2990        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2991        self.captcha_result = self.captcha.verify(self.request)
2992        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2993        return
2994
2995    def _redirect(self, email, password, student_id):
2996        # Forward only email to landing page in base package.
2997        self.redirect(self.url(self.context, 'requestpw_complete',
2998            data = dict(email=email)))
2999        return
3000
3001    def _redirect_no_student(self):
3002        # No record found, this is the truth. We do not redirect here.
3003        # We are using this method in custom packages
3004        # for redirecting alumni to the application section.
3005        self.flash(_('No student record found.'), type="warning")
3006        return
3007
3008    def _pw_used(self):
3009        # XXX: False if password has not been used. We need an extra
3010        #      attribute which remembers if student logged in.
3011        return True
3012
3013    @action(_('Send login credentials to email address'), style='primary')
3014    def get_credentials(self, **data):
3015        if not self.captcha_result.is_valid:
3016            # Captcha will display error messages automatically.
3017            # No need to flash something.
3018            return
3019        number = data.get('number','')
3020        lastname = data.get('lastname','')
3021        cat = getUtility(ICatalog, name='students_catalog')
3022        results = list(
3023            cat.searchResults(reg_number=(number, number)))
3024        if not results:
3025            results = list(
3026                cat.searchResults(matric_number=(number, number)))
3027        if results:
3028            student = results[0]
3029            if getattr(student,'lastname',None) is None:
3030                self.flash(_('An error occurred.'), type="danger")
3031                return
3032            elif student.lastname.lower() != lastname.lower():
3033                # Don't tell the truth here. Anonymous must not
3034                # know that a record was found and only the lastname
3035                # verification failed.
3036                self.flash(_('No student record found.'), type="warning")
3037                return
3038            elif student.password is not None and self._pw_used:
3039                self.flash(_('Your password has already been set and used. '
3040                             'Please proceed to the login page.'),
3041                           type="warning")
3042                return
3043            # Store email address but nothing else.
3044            student.email = data['email']
3045            notify(grok.ObjectModifiedEvent(student))
3046        else:
3047            self._redirect_no_student()
3048            return
3049
3050        kofa_utils = getUtility(IKofaUtils)
3051        password = kofa_utils.genPassword()
3052        mandate = PasswordMandate()
3053        mandate.params['password'] = password
3054        mandate.params['user'] = student
3055        site = grok.getSite()
3056        site['mandates'].addMandate(mandate)
3057        # Send email with credentials
3058        args = {'mandate_id':mandate.mandate_id}
3059        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3060        url_info = u'Confirmation link: %s' % mandate_url
3061        msg = _('You have successfully requested a password for the')
3062        if kofa_utils.sendCredentials(IUserAccount(student),
3063            password, url_info, msg):
3064            email_sent = student.email
3065        else:
3066            email_sent = None
3067        self._redirect(email=email_sent, password=password,
3068            student_id=student.student_id)
3069        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3070        self.context.logger.info(
3071            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3072        return
3073
3074class StudentRequestPasswordEmailSent(KofaPage):
3075    """Landing page after successful password request.
3076
3077    """
3078    grok.name('requestpw_complete')
3079    grok.require('waeup.Public')
3080    grok.template('requestpwmailsent')
3081    label = _('Your password request was successful.')
3082
3083    def update(self, email=None, student_id=None, password=None):
3084        self.email = email
3085        self.password = password
3086        self.student_id = student_id
3087        return
3088
3089class FilterStudentsInDepartmentPage(KofaPage):
3090    """Page that filters and lists students.
3091    """
3092    grok.context(IDepartment)
3093    grok.require('waeup.showStudents')
3094    grok.name('students')
3095    grok.template('filterstudentspage')
3096    pnav = 1
3097    session_label = _('Current Session')
3098    level_label = _('Current Level')
3099
3100    def label(self):
3101        return 'Students in %s' % self.context.longtitle
3102
3103    def _set_session_values(self):
3104        vocab_terms = academic_sessions_vocab.by_value.values()
3105        self.sessions = sorted(
3106            [(x.title, x.token) for x in vocab_terms], reverse=True)
3107        self.sessions += [('All Sessions', 'all')]
3108        return
3109
3110    def _set_level_values(self):
3111        vocab_terms = course_levels.by_value.values()
3112        self.levels = sorted(
3113            [(x.title, x.token) for x in vocab_terms])
3114        self.levels += [('All Levels', 'all')]
3115        return
3116
3117    def _searchCatalog(self, session, level):
3118        if level not in (10, 999, None):
3119            start_level = 100 * (level // 100)
3120            end_level = start_level + 90
3121        else:
3122            start_level = end_level = level
3123        cat = queryUtility(ICatalog, name='students_catalog')
3124        students = cat.searchResults(
3125            current_session=(session, session),
3126            current_level=(start_level, end_level),
3127            depcode=(self.context.code, self.context.code)
3128            )
3129        hitlist = []
3130        for student in students:
3131            hitlist.append(StudentQueryResultItem(student, view=self))
3132        return hitlist
3133
3134    def update(self, SHOW=None, session=None, level=None):
3135        self.parent_url = self.url(self.context.__parent__)
3136        self._set_session_values()
3137        self._set_level_values()
3138        self.hitlist = []
3139        self.session_default = session
3140        self.level_default = level
3141        if SHOW is not None:
3142            if session != 'all':
3143                self.session = int(session)
3144                self.session_string = '%s %s/%s' % (
3145                    self.session_label, self.session, self.session+1)
3146            else:
3147                self.session = None
3148                self.session_string = _('in any session')
3149            if level != 'all':
3150                self.level = int(level)
3151                self.level_string = '%s %s' % (self.level_label, self.level)
3152            else:
3153                self.level = None
3154                self.level_string = _('at any level')
3155            self.hitlist = self._searchCatalog(self.session, self.level)
3156            if not self.hitlist:
3157                self.flash(_('No student found.'), type="warning")
3158        return
3159
3160class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3161    """Page that filters and lists students.
3162    """
3163    grok.context(ICertificate)
3164
3165    def label(self):
3166        return 'Students studying %s' % self.context.longtitle
3167
3168    def _searchCatalog(self, session, level):
3169        if level not in (10, 999, None):
3170            start_level = 100 * (level // 100)
3171            end_level = start_level + 90
3172        else:
3173            start_level = end_level = level
3174        cat = queryUtility(ICatalog, name='students_catalog')
3175        students = cat.searchResults(
3176            current_session=(session, session),
3177            current_level=(start_level, end_level),
3178            certcode=(self.context.code, self.context.code)
3179            )
3180        hitlist = []
3181        for student in students:
3182            hitlist.append(StudentQueryResultItem(student, view=self))
3183        return hitlist
3184
3185class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3186    """Page that filters and lists students.
3187    """
3188    grok.context(ICourse)
3189    grok.require('waeup.viewStudent')
3190
3191    session_label = _('Session')
3192    level_label = _('Level')
3193
3194    def label(self):
3195        return 'Students registered for %s' % self.context.longtitle
3196
3197    def _searchCatalog(self, session, level):
3198        if level not in (10, 999, None):
3199            start_level = 100 * (level // 100)
3200            end_level = start_level + 90
3201        else:
3202            start_level = end_level = level
3203        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3204        coursetickets = cat.searchResults(
3205            session=(session, session),
3206            level=(start_level, end_level),
3207            code=(self.context.code, self.context.code)
3208            )
3209        hitlist = []
3210        for ticket in coursetickets:
3211            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3212        return list(set(hitlist))
3213
3214class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3215    """ Clear all students of a department in state 'clearance requested'.
3216    """
3217    grok.context(IDepartment)
3218    grok.name('clearallstudents')
3219    grok.require('waeup.clearAllStudents')
3220
3221    def update(self):
3222        cat = queryUtility(ICatalog, name='students_catalog')
3223        students = cat.searchResults(
3224            depcode=(self.context.code, self.context.code),
3225            state=(REQUESTED, REQUESTED)
3226            )
3227        num = 0
3228        for student in students:
3229            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3230                continue
3231            IWorkflowInfo(student).fireTransition('clear')
3232            num += 1
3233        self.flash(_('%d students have been cleared.' % num))
3234        self.redirect(self.url(self.context))
3235        return
3236
3237    def render(self):
3238        return
3239
3240
3241class EditScoresPage(KofaPage):
3242    """Page that allows to edit batches of scores.
3243    """
3244    grok.context(ICourse)
3245    grok.require('waeup.editScores')
3246    grok.name('edit_scores')
3247    grok.template('editscorespage')
3248    pnav = 1
3249    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3250
3251    def label(self):
3252        return '%s tickets in academic session %s' % (
3253            self.context.code, self.session_title)
3254
3255    def _searchCatalog(self, session):
3256        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3257        coursetickets = cat.searchResults(
3258            session=(session, session),
3259            code=(self.context.code, self.context.code)
3260            )
3261        return list(coursetickets)
3262
3263    def _extract_uploadfile(self, uploadfile):
3264        """Get a mapping of student-ids to scores.
3265
3266        The mapping is constructed by reading contents from `uploadfile`.
3267
3268        We expect uploadfile to be a regular CSV file with columns
3269        ``student_id`` and ``score`` (other cols are ignored).
3270        """
3271        result = dict()
3272        data = StringIO(uploadfile.read())  # ensure we have something seekable
3273        reader = csv.DictReader(data)
3274        for row in reader:
3275            if not 'student_id' in row or not 'score' in row:
3276                continue
3277            result[row['student_id']] = row['score']
3278        return result
3279
3280    def _update_scores(self, form):
3281        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3282        error = ''
3283        if 'UPDATE_FILE' in form:
3284            if form['uploadfile']:
3285                try:
3286                    formvals = self._extract_uploadfile(form['uploadfile'])
3287                except:
3288                    self.flash(
3289                        _('Uploaded file contains illegal data. Ignored'),
3290                        type="danger")
3291                    return False
3292            else:
3293                self.flash(
3294                    _('No file provided.'), type="danger")
3295                return False
3296        else:
3297            formvals = dict(zip(form['sids'], form['scores']))
3298        for ticket in self.editable_tickets:
3299            score = ticket.score
3300            sid = ticket.student.student_id
3301            if sid not in formvals:
3302                continue
3303            if formvals[sid] == '':
3304                score = None
3305            else:
3306                try:
3307                    score = int(formvals[sid])
3308                except ValueError:
3309                    error += '%s, ' % ticket.student.display_fullname
3310            if ticket.score != score:
3311                ticket.score = score
3312                ticket.student.__parent__.logger.info(
3313                    '%s - %s %s/%s score updated (%s)' % (
3314                        ob_class, ticket.student.student_id,
3315                        ticket.level, ticket.code, score)
3316                    )
3317        if error:
3318            self.flash(
3319                _('Error: Score(s) of following students have not been '
3320                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3321                type="danger")
3322        return True
3323
3324    def update(self,  *args, **kw):
3325        form = self.request.form
3326        self.current_academic_session = grok.getSite()[
3327            'configuration'].current_academic_session
3328        if self.context.__parent__.__parent__.score_editing_disabled:
3329            self.flash(_('Score editing disabled.'), type="warning")
3330            self.redirect(self.url(self.context))
3331            return
3332        if not self.current_academic_session:
3333            self.flash(_('Current academic session not set.'), type="warning")
3334            self.redirect(self.url(self.context))
3335            return
3336        self.session_title = academic_sessions_vocab.getTerm(
3337            self.current_academic_session).title
3338        self.tickets = self._searchCatalog(self.current_academic_session)
3339        if not self.tickets:
3340            self.flash(_('No student found.'), type="warning")
3341            self.redirect(self.url(self.context))
3342            return
3343        self.editable_tickets = [
3344            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3345        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3346            return
3347        if not self.editable_tickets:
3348            return
3349        success = self._update_scores(form)
3350        if success:
3351            self.flash(_('You successfully updated course results.'))
3352        return
3353
3354
3355class DownloadScoresView(UtilityView, grok.View):
3356    """View that exports scores.
3357    """
3358    grok.context(ICourse)
3359    grok.require('waeup.editScores')
3360    grok.name('download_scores')
3361
3362    def update(self):
3363        self.current_academic_session = grok.getSite()[
3364            'configuration'].current_academic_session
3365        if self.context.__parent__.__parent__.score_editing_disabled:
3366            self.flash(_('Score editing disabled.'), type="warning")
3367            self.redirect(self.url(self.context))
3368            return
3369        if not self.current_academic_session:
3370            self.flash(_('Current academic session not set.'), type="warning")
3371            self.redirect(self.url(self.context))
3372            return
3373        site = grok.getSite()
3374        exporter = getUtility(ICSVExporter, name='lecturer')
3375        self.csv = exporter.export_filtered(site, filepath=None,
3376                                 catalog='coursetickets',
3377                                 session=self.current_academic_session,
3378                                 level=None,
3379                                 code=self.context.code)
3380        return
3381
3382    def render(self):
3383        filename = 'results_%s_%s.csv' % (
3384            self.context.code, self.current_academic_session)
3385        self.response.setHeader(
3386            'Content-Type', 'text/csv; charset=UTF-8')
3387        self.response.setHeader(
3388            'Content-Disposition:', 'attachment; filename="%s' % filename)
3389        return self.csv
3390
3391class ExportPDFScoresSlip(UtilityView, grok.View,
3392    LocalRoleAssignmentUtilityView):
3393    """Deliver a PDF slip of course tickets for a lecturer.
3394    """
3395    grok.context(ICourse)
3396    grok.name('coursetickets.pdf')
3397    grok.require('waeup.editScores')
3398
3399    def data(self, session):
3400        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3401        coursetickets = cat.searchResults(
3402            session=(session, session),
3403            code=(self.context.code, self.context.code)
3404            )
3405        header = [[_('Matric No.'),
3406                   _('Reg. No.'),
3407                   _('Fullname'),
3408                   _('Status'),
3409                   _('Course of Studies'),
3410                   _('Level'),
3411                   _('Score') ],]
3412        tickets = []
3413        for ticket in list(coursetickets):
3414            row = [ticket.student.matric_number,
3415                  ticket.student.reg_number,
3416                  ticket.student.display_fullname,
3417                  ticket.student.translated_state,
3418                  ticket.student.certcode,
3419                  ticket.level,
3420                  ticket.score]
3421            tickets.append(row)
3422        return header + sorted(tickets, key=lambda value: value[0]), None
3423
3424    def render(self):
3425        session = grok.getSite()['configuration'].current_academic_session
3426        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3427                     if i['local_role'] == 'waeup.local.Lecturer']
3428        lecturers =  ', '.join(lecturers)
3429        students_utils = getUtility(IStudentsUtils)
3430        return students_utils.renderPDFCourseticketsOverview(
3431            self, session, self.data(session), lecturers, 'landscape')
3432
3433class ExportJobContainerOverview(KofaPage):
3434    """Page that lists active student data export jobs and provides links
3435    to discard or download CSV files.
3436
3437    """
3438    grok.context(VirtualExportJobContainer)
3439    grok.require('waeup.showStudents')
3440    grok.name('index.html')
3441    grok.template('exportjobsindex')
3442    label = _('Student Data Exports')
3443    pnav = 1
3444    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3445
3446    def update(self, CREATE=None, DISCARD=None, job_id=None):
3447        if CREATE:
3448            self.redirect(self.url('@@exportconfig'))
3449            return
3450        if DISCARD and job_id:
3451            entry = self.context.entry_from_job_id(job_id)
3452            self.context.delete_export_entry(entry)
3453            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3454            self.context.logger.info(
3455                '%s - discarded: job_id=%s' % (ob_class, job_id))
3456            self.flash(_('Discarded export') + ' %s' % job_id)
3457        self.entries = doll_up(self, user=self.request.principal.id)
3458        return
3459
3460class ExportJobContainerJobConfig(KofaPage):
3461    """Page that configures a students export job.
3462
3463    This is a baseclass.
3464    """
3465    grok.baseclass()
3466    grok.name('exportconfig')
3467    grok.require('waeup.showStudents')
3468    grok.template('exportconfig')
3469    label = _('Configure student data export')
3470    pnav = 1
3471    redirect_target = ''
3472    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3473
3474    def _set_session_values(self):
3475        vocab_terms = academic_sessions_vocab.by_value.values()
3476        self.sessions = [(_('All Sessions'), 'all')]
3477        self.sessions += sorted(
3478            [(x.title, x.token) for x in vocab_terms], reverse=True)
3479        return
3480
3481    def _set_level_values(self):
3482        vocab_terms = course_levels.by_value.values()
3483        self.levels = [(_('All Levels'), 'all')]
3484        self.levels += sorted(
3485            [(x.title, x.token) for x in vocab_terms])
3486        return
3487
3488    def _set_mode_values(self):
3489        utils = getUtility(IKofaUtils)
3490        self.modes =[(_('All Modes'), 'all')]
3491        self.modes += sorted([(value, key) for key, value in
3492                      utils.STUDY_MODES_DICT.items()])
3493        return
3494
3495    def _set_paycat_values(self):
3496        utils = getUtility(IKofaUtils)
3497        self.paycats =[(_('All Payment Categories'), 'all')]
3498        self.paycats += sorted([(value, key) for key, value in
3499                      utils.PAYMENT_CATEGORIES.items()])
3500        return
3501
3502    def _set_exporter_values(self):
3503        # We provide all student exporters, nothing else, yet.
3504        # Bursary or Department Officers don't have the general exportData
3505        # permission and are only allowed to export bursary or payments
3506        # overview data respectively. This is the only place where
3507        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3508        # are used.
3509        exporters = []
3510        if not checkPermission('waeup.exportData', self.context):
3511            if checkPermission('waeup.exportBursaryData', self.context):
3512                exporters += [('Bursary Data', 'bursary')]
3513            if checkPermission('waeup.exportPaymentsOverview', self.context):
3514                exporters += [('School Fee Payments Overview',
3515                               'sfpaymentsoverview'),
3516                              ('Session Payments Overview',
3517                               'sessionpaymentsoverview')]
3518            self.exporters = exporters
3519            return
3520        STUDENT_EXPORTER_NAMES = getUtility(
3521            IStudentsUtils).STUDENT_EXPORTER_NAMES
3522        for name in STUDENT_EXPORTER_NAMES:
3523            util = getUtility(ICSVExporter, name=name)
3524            exporters.append((util.title, name),)
3525        self.exporters = exporters
3526        return
3527
3528    @property
3529    def faccode(self):
3530        return None
3531
3532    @property
3533    def depcode(self):
3534        return None
3535
3536    @property
3537    def certcode(self):
3538        return None
3539
3540    def update(self, START=None, session=None, level=None, mode=None,
3541               payments_start=None, payments_end=None, ct_level=None,
3542               ct_session=None, paycat=None, paysession=None, exporter=None):
3543        self._set_session_values()
3544        self._set_level_values()
3545        self._set_mode_values()
3546        self._set_paycat_values()
3547        self._set_exporter_values()
3548        if START is None:
3549            return
3550        ena = exports_not_allowed(self)
3551        if ena:
3552            self.flash(ena, type='danger')
3553            return
3554        if payments_start or payments_end:
3555            date_format = '%d/%m/%Y'
3556            try:
3557                datetime.strptime(payments_start, date_format)
3558                datetime.strptime(payments_end, date_format)
3559            except ValueError:
3560                self.flash(_('Payment dates do not match format d/m/Y.'),
3561                           type="danger")
3562                return
3563        if session == 'all':
3564            session=None
3565        if level == 'all':
3566            level = None
3567        if mode == 'all':
3568            mode = None
3569        if (mode,
3570            level,
3571            session,
3572            self.faccode,
3573            self.depcode,
3574            self.certcode) == (None, None, None, None, None, None):
3575            # Export all students including those without certificate
3576            job_id = self.context.start_export_job(exporter,
3577                                          self.request.principal.id,
3578                                          payments_start = payments_start,
3579                                          payments_end = payments_end,
3580                                          paycat=paycat,
3581                                          paysession=paysession,
3582                                          ct_level = ct_level,
3583                                          ct_session = ct_session,
3584                                          )
3585        else:
3586            job_id = self.context.start_export_job(exporter,
3587                                          self.request.principal.id,
3588                                          current_session=session,
3589                                          current_level=level,
3590                                          current_mode=mode,
3591                                          faccode=self.faccode,
3592                                          depcode=self.depcode,
3593                                          certcode=self.certcode,
3594                                          payments_start = payments_start,
3595                                          payments_end = payments_end,
3596                                          paycat=paycat,
3597                                          paysession=paysession,
3598                                          ct_level = ct_level,
3599                                          ct_session = ct_session,)
3600        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3601        self.context.logger.info(
3602            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3603            % (ob_class, exporter, session, level, mode, self.faccode,
3604            self.depcode, self.certcode, payments_start, payments_end,
3605            ct_level, ct_session, paycat, paysession, job_id))
3606        self.flash(_('Export started for students with') +
3607                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3608                   session, level, mode))
3609        self.redirect(self.url(self.redirect_target))
3610        return
3611
3612class ExportJobContainerDownload(ExportCSVView):
3613    """Page that downloads a students export csv file.
3614
3615    """
3616    grok.context(VirtualExportJobContainer)
3617    grok.require('waeup.showStudents')
3618
3619class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3620    """Page that configures a students export job in datacenter.
3621
3622    """
3623    grok.context(IDataCenter)
3624    redirect_target = '@@export'
3625
3626class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3627    """Page that configures a students export job in datacenter.
3628
3629    """
3630    grok.name('exportselected')
3631    grok.context(IDataCenter)
3632    redirect_target = '@@export'
3633    grok.template('exportselected')
3634    label = _('Configure student data export')
3635
3636    def update(self, START=None, students=None, exporter=None):
3637        self._set_exporter_values()
3638        if START is None:
3639            return
3640        ena = exports_not_allowed(self)
3641        if ena:
3642            self.flash(ena, type='danger')
3643            return
3644        try:
3645            ids = students.replace(',', ' ').split()
3646        except:
3647            self.flash(sys.exc_info()[1])
3648            self.redirect(self.url(self.redirect_target))
3649            return
3650        job_id = self.context.start_export_job(
3651            exporter, self.request.principal.id, selected=ids)
3652        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3653        self.context.logger.info(
3654            '%s - selected students exported: %s, job_id=%s' %
3655            (ob_class, exporter, job_id))
3656        self.flash(_('Export of selected students started.'))
3657        self.redirect(self.url(self.redirect_target))
3658        return
3659
3660class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3661    """Page that configures a students export job in facultiescontainer.
3662
3663    """
3664    grok.context(VirtualFacultiesExportJobContainer)
3665
3666
3667class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3668    """Page that configures a students export job in faculties.
3669
3670    """
3671    grok.context(VirtualFacultyExportJobContainer)
3672
3673    @property
3674    def faccode(self):
3675        return self.context.__parent__.code
3676
3677class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3678    """Page that configures a students export job in departments.
3679
3680    """
3681    grok.context(VirtualDepartmentExportJobContainer)
3682
3683    @property
3684    def depcode(self):
3685        return self.context.__parent__.code
3686
3687class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3688    """Page that configures a students export job for certificates.
3689
3690    """
3691    grok.context(VirtualCertificateExportJobContainer)
3692    grok.template('exportconfig_certificate')
3693
3694    @property
3695    def certcode(self):
3696        return self.context.__parent__.code
3697
3698class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3699    """Page that configures a students export job for courses.
3700
3701    In contrast to department or certificate student data exports the
3702    coursetickets_catalog is searched here. Therefore the update
3703    method from the base class is customized.
3704    """
3705    grok.context(VirtualCourseExportJobContainer)
3706    grok.template('exportconfig_course')
3707
3708    def _set_exporter_values(self):
3709        # We provide only the 'coursetickets' and 'lecturer' exporter
3710        # but can add more.
3711        exporters = []
3712        for name in ('coursetickets', 'lecturer'):
3713            util = getUtility(ICSVExporter, name=name)
3714            exporters.append((util.title, name),)
3715        self.exporters = exporters
3716
3717    def _set_session_values(self):
3718        # We allow only current academic session
3719        academic_session = grok.getSite()['configuration'].current_academic_session
3720        if not academic_session:
3721            self.sessions = []
3722            return
3723        x = academic_sessions_vocab.getTerm(academic_session)
3724        self.sessions = [(x.title, x.token)]
3725        return
3726
3727    def update(self, START=None, session=None, level=None, mode=None,
3728               exporter=None):
3729        self._set_session_values()
3730        self._set_level_values()
3731        self._set_mode_values()
3732        self._set_exporter_values()
3733        if not self.sessions:
3734            self.flash(
3735                _('Academic session not set. '
3736                  'Please contact the administrator.'),
3737                type='danger')
3738            self.redirect(self.url(self.context))
3739            return
3740        if START is None:
3741            return
3742        ena = exports_not_allowed(self)
3743        if ena:
3744            self.flash(ena, type='danger')
3745            return
3746        if session == 'all':
3747            session = None
3748        if level == 'all':
3749            level = None
3750        job_id = self.context.start_export_job(exporter,
3751                                      self.request.principal.id,
3752                                      # Use a different catalog and
3753                                      # pass different keywords than
3754                                      # for the (default) students_catalog
3755                                      catalog='coursetickets',
3756                                      session=session,
3757                                      level=level,
3758                                      code=self.context.__parent__.code)
3759        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3760        self.context.logger.info(
3761            '%s - exported: %s (%s, %s, %s), job_id=%s'
3762            % (ob_class, exporter, session, level,
3763            self.context.__parent__.code, job_id))
3764        self.flash(_('Export started for course tickets with') +
3765                   ' level_session=%s, level=%s' % (
3766                   session, level))
3767        self.redirect(self.url(self.redirect_target))
3768        return
Note: See TracBrowser for help on using the repository browser.