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

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

Extend tests.

  • Property svn:keywords set to Id
File size: 140.2 KB
Line 
1## $Id: browser.py 15159 2018-09-20 20:40:45Z 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,
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(IStudent)
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 getattr(
1097            self.context['studycourse'], 'transcript_comment', None) is not None:
1098            self.correspondence = self.context[
1099                'studycourse'].transcript_comment.replace(
1100                    '\n', '<br>')
1101        else:
1102            self.correspondence = ''
1103        if SUBMIT is None:
1104            return
1105        if self.with_ac:
1106            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1107            code = get_access_code(pin)
1108            if not code:
1109                self.flash(_('Activation code is invalid.'), type="warning")
1110                return
1111            if code.state == USED:
1112                self.flash(_('Activation code has already been used.'),
1113                           type="warning")
1114                return
1115            # Mark pin as used (this also fires a pin related transition)
1116            # and fire transition request_transcript
1117            comment = _(u"invalidated")
1118            # Here we know that the ac is in state initialized so we do not
1119            # expect an exception, but the owner might be different
1120            if not invalidate_accesscode(pin, comment, self.context.student_id):
1121                self.flash(_('You are not the owner of this access code.'),
1122                           type="warning")
1123                return
1124            self.context.clr_code = pin
1125        IWorkflowInfo(self.context).fireTransition('request_transcript')
1126        comment = self.request.form.get('comment', '').replace('\r', '')
1127        address = self.request.form.get('address', '').replace('\r', '')
1128        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1129        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1130        old_transcript_comment = getattr(
1131            self.context['studycourse'], 'transcript_comment', None)
1132        if old_transcript_comment == None:
1133            old_transcript_comment = ''
1134        self.context['studycourse'].transcript_comment = '''On %s %s wrote:
1135
1136%s
1137
1138Dispatch Address:
1139%s
1140
1141%s''' % (today, self.request.principal.id, comment, address,
1142         old_transcript_comment)
1143        self.context.writeLogMessage(
1144            self, 'comment: %s' % comment.replace('\n', '<br>'))
1145        self.flash(_('Transcript processing has been started.'))
1146        self.redirect(self.url(self.context))
1147        return
1148
1149class StudentTranscriptValidateView(UtilityView, grok.View):
1150    """ View to validate transcript
1151    """
1152    grok.context(IStudentStudyCourse)
1153    grok.name('validate_transcript')
1154    grok.require('waeup.processTranscript')
1155
1156    def update(self, SUBMIT=None):
1157        if self.context.student.state != TRANSREQ:
1158            self.flash(_('Student is in wrong state.'), type="warning")
1159            self.redirect(self.url(self.context))
1160            return
1161        # Fire transition
1162        IWorkflowInfo(self.context.student).fireTransition(
1163            'validate_transcript')
1164        self.flash(_('Transcript validated.'))
1165        self.redirect(self.url(self.context) + '/transcript')
1166        return
1167
1168    def render(self):
1169        return
1170
1171class StudentTranscriptSignView(UtilityView, grok.View):
1172    """ View to sign transcript
1173    """
1174    grok.context(IStudentStudyCourse)
1175    grok.name('sign_transcript')
1176    grok.require('waeup.signTranscript')
1177
1178    def update(self, SUBMIT=None):
1179        if self.context.student.state != TRANSVAL:
1180            self.flash(_('Student is in wrong state.'), type="warning")
1181            self.redirect(self.url(self.context))
1182            return
1183        prev_transcript_signees = getattr(
1184            self.context, 'transcript_signees', None)
1185        if prev_transcript_signees \
1186            and '(%s)' % self.request.principal.id in prev_transcript_signees:
1187            self.flash(_('You have already signed this transcript.'),
1188                type="warning")
1189            self.redirect(self.url(self.context) + '/transcript')
1190            return
1191        self.flash(_('Transcript signed.'))
1192        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1193        self.context.student.__parent__.logger.info(
1194            '%s - %s - Transcript signed'
1195            % (ob_class, self.context.student.student_id))
1196        self.context.student.history.addMessage('Transcript signed')
1197        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1198        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1199        if prev_transcript_signees == None:
1200            prev_transcript_signees = ''
1201        self.context.transcript_signees = (
1202            u"Electronically signed by %s (%s) on %s\n%s"
1203            % (self.request.principal.title, self.request.principal.id, today,
1204            prev_transcript_signees))
1205        self.redirect(self.url(self.context) + '/transcript')
1206        return
1207
1208    def render(self):
1209        return
1210
1211class StudentTranscriptReleaseFormPage(KofaEditFormPage):
1212    """ Page to release transcript
1213    """
1214    grok.context(IStudentStudyCourse)
1215    grok.name('release_transcript')
1216    grok.require('waeup.processTranscript')
1217    grok.template('transcriptrelease')
1218    label = _('Release transcript')
1219    buttonname = _('Save comment and release transcript')
1220    pnav = 4
1221
1222    def update(self, SUBMIT=None):
1223        super(StudentTranscriptReleaseFormPage, self).update()
1224        if self.context.student.state != TRANSVAL:
1225            self.flash(_('Student is in wrong state.'), type="warning")
1226            self.redirect(self.url(self.context))
1227            return
1228        if getattr(self.context, 'transcript_comment', None) is not None:
1229            self.correspondence = self.context.transcript_comment.replace(
1230                '\n', '<br>')
1231        else:
1232            self.correspondence = ''
1233        if getattr(self.context, 'transcript_signees', None) is not None:
1234            self.signees = self.context.transcript_signees.replace(
1235                '\n', '<br><br>')
1236        else:
1237            self.signees = ''
1238        if SUBMIT is None:
1239            return
1240        # Fire transition
1241        IWorkflowInfo(self.context.student).fireTransition('release_transcript')
1242        self.flash(_('Transcript released and final transcript file saved.'))
1243        comment = self.request.form.get('comment', '').replace('\r', '')
1244        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1245        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1246        old_transcript_comment = getattr(
1247            self.context, 'transcript_comment', None)
1248        if old_transcript_comment == None:
1249            old_transcript_comment = ''
1250        self.context.transcript_comment = '''On %s %s wrote:
1251
1252%s
1253
1254%s''' % (today, self.request.principal.id, comment,
1255         old_transcript_comment)
1256        self.context.writeLogMessage(
1257            self, 'comment: %s' % comment.replace('\n', '<br>'))
1258        # Produce transcript file
1259        self.redirect(self.url(self.context) + '/transcript.pdf')
1260        return
1261
1262class StudentTranscriptRequestManageFormPage(KofaEditFormPage):
1263    """ Page to manage transcript data. There is no link button
1264    for this page. It's an emergency page.
1265    """
1266    grok.context(IStudentStudyCourse)
1267    grok.name('manage_transcript_request')
1268    grok.require('waeup.processTranscript')
1269    form_fields = grok.AutoFields(IStudentTranscript)
1270    label = _('Manage transcript request')
1271    pnav = 4
1272
1273    @action(_('Save'), style='primary')
1274    def save(self, **data):
1275        msave(self, **data)
1276        return
1277
1278class StudyCourseTranscriptPage(KofaDisplayFormPage):
1279    """ Page to display the student's transcript.
1280    """
1281    grok.context(IStudentStudyCourse)
1282    grok.name('transcript')
1283    grok.require('waeup.viewTranscript')
1284    grok.template('transcript')
1285    pnav = 4
1286
1287    def update(self):
1288        final_slip = getUtility(IExtFileStore).getFileByContext(
1289            self.context.student, attr='final_transcript')
1290        if not self.context.student.transcript_enabled or final_slip:
1291            self.flash(_('Forbidden!'), type="warning")
1292            self.redirect(self.url(self.context))
1293            return
1294        super(StudyCourseTranscriptPage, self).update()
1295        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1296        self.level_dict = level_dict(self.context)
1297        self.session_dict = dict(
1298            [(item[1], item[0]) for item in academic_sessions()])
1299        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1300        return
1301
1302    @property
1303    def label(self):
1304        # Here we know that the cookie has been set
1305        return _('${a}: Transcript Data', mapping = {
1306            'a':self.context.student.display_fullname})
1307
1308class ExportPDFTranscriptSlip(UtilityView, grok.View):
1309    """Deliver a PDF slip of the context.
1310    """
1311    grok.context(IStudentStudyCourse)
1312    grok.name('transcript.pdf')
1313    grok.require('waeup.viewTranscript')
1314    prefix = 'form'
1315    omit_fields = (
1316        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
1317        'password', 'suspended', 'phone', 'email',
1318        'adm_code', 'suspended_comment', 'current_level', 'flash_notice')
1319
1320    def update(self):
1321        final_slip = getUtility(IExtFileStore).getFileByContext(
1322            self.context.student, attr='final_transcript')
1323        if not self.context.student.transcript_enabled \
1324            or final_slip:
1325            self.flash(_('Forbidden!'), type="warning")
1326            self.redirect(self.url(self.context))
1327            return
1328        super(ExportPDFTranscriptSlip, self).update()
1329        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1330        self.level_dict = level_dict(self.context)
1331        self.session_dict = dict(
1332            [(item[1], item[0]) for item in academic_sessions()])
1333        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1334        return
1335
1336    @property
1337    def label(self):
1338        # Here we know that the cookie has been set
1339        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1340        return translate(_('Academic Transcript'),
1341            'waeup.kofa', target_language=portal_language)
1342
1343    def _sigsInFooter(self):
1344        if getattr(
1345            self.context.student['studycourse'], 'transcript_signees', None):
1346            return ()
1347        return (_('CERTIFIED TRUE COPY'),)
1348
1349    def _signatures(self):
1350        return ()
1351
1352    def _digital_sigs(self):
1353        if getattr(
1354            self.context.student['studycourse'], 'transcript_signees', None):
1355            return self.context.student['studycourse'].transcript_signees
1356        return ()
1357
1358    def _save_file(self):
1359        if self.context.student.state == TRANSREL:
1360            return True
1361        return False
1362
1363    def render(self):
1364        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1365        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1366        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1367        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1368        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1369        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1370        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1371        studentview = StudentBasePDFFormPage(self.context.student,
1372            self.request, self.omit_fields)
1373        students_utils = getUtility(IStudentsUtils)
1374
1375        tableheader = [(Code,'code', 2.5),
1376                         (Title,'title', 7),
1377                         (Term, 'semester', 1.5),
1378                         (Cred, 'credits', 1.5),
1379                         (Score, 'total_score', 1.5),
1380                         (Grade, 'grade', 1.5),
1381                         ]
1382
1383        pdfstream = students_utils.renderPDFTranscript(
1384            self, 'transcript.pdf',
1385            self.context.student, studentview,
1386            omit_fields=self.omit_fields,
1387            tableheader=tableheader,
1388            signatures=self._signatures(),
1389            sigs_in_footer=self._sigsInFooter(),
1390            digital_sigs=self._digital_sigs(),
1391            save_file=self._save_file(),
1392            )
1393        if not pdfstream:
1394            self.redirect(self.url(self.context.student))
1395            return
1396        return pdfstream
1397
1398class StudentTransferFormPage(KofaAddFormPage):
1399    """Page to transfer the student.
1400    """
1401    grok.context(IStudent)
1402    grok.name('transfer')
1403    grok.require('waeup.manageStudent')
1404    label = _('Transfer student')
1405    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1406        'entry_mode', 'entry_session')
1407    pnav = 4
1408
1409    @jsaction(_('Transfer'))
1410    def transferStudent(self, **data):
1411        error = self.context.transfer(**data)
1412        if error == -1:
1413            self.flash(_('Current level does not match certificate levels.'),
1414                       type="warning")
1415        elif error == -2:
1416            self.flash(_('Former study course record incomplete.'),
1417                       type="warning")
1418        elif error == -3:
1419            self.flash(_('Maximum number of transfers exceeded.'),
1420                       type="warning")
1421        else:
1422            self.flash(_('Successfully transferred.'))
1423        return
1424
1425class RevertTransferFormPage(KofaEditFormPage):
1426    """View that reverts the previous transfer.
1427    """
1428    grok.context(IStudent)
1429    grok.name('revert_transfer')
1430    grok.require('waeup.manageStudent')
1431    grok.template('reverttransfer')
1432    label = _('Revert previous transfer')
1433
1434    def update(self):
1435        if not self.context.has_key('studycourse_1'):
1436            self.flash(_('No previous transfer.'), type="warning")
1437            self.redirect(self.url(self.context))
1438            return
1439        return
1440
1441    @jsaction(_('Revert now'))
1442    def transferStudent(self, **data):
1443        self.context.revert_transfer()
1444        self.flash(_('Previous transfer reverted.'))
1445        self.redirect(self.url(self.context, 'studycourse'))
1446        return
1447
1448class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1449    """ Page to display student study levels
1450    """
1451    grok.context(IStudentStudyLevel)
1452    grok.name('index')
1453    grok.require('waeup.viewStudent')
1454    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1455    form_fields[
1456        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1457    grok.template('studylevelpage')
1458    pnav = 4
1459
1460    def update(self):
1461        super(StudyLevelDisplayFormPage, self).update()
1462        return
1463
1464    @property
1465    def translated_values(self):
1466        return translated_values(self)
1467
1468    @property
1469    def label(self):
1470        # Here we know that the cookie has been set
1471        lang = self.request.cookies.get('kofa.language')
1472        level_title = translate(self.context.level_title, 'waeup.kofa',
1473            target_language=lang)
1474        return _('${a}: Study Level ${b}', mapping = {
1475            'a':self.context.student.display_fullname,
1476            'b':level_title})
1477
1478class ExportPDFCourseRegistrationSlip(UtilityView, grok.View):
1479    """Deliver a PDF slip of the context.
1480    """
1481    grok.context(IStudentStudyLevel)
1482    grok.name('course_registration_slip.pdf')
1483    grok.require('waeup.viewStudent')
1484    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1485    form_fields[
1486        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1487    prefix = 'form'
1488    omit_fields = (
1489        'password', 'suspended', 'phone', 'date_of_birth',
1490        'adm_code', 'sex', 'suspended_comment', 'current_level',
1491        'flash_notice')
1492
1493    @property
1494    def title(self):
1495        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1496        return translate(_('Level Data'), 'waeup.kofa',
1497            target_language=portal_language)
1498
1499    @property
1500    def label(self):
1501        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1502        lang = self.request.cookies.get('kofa.language', portal_language)
1503        level_title = translate(self.context.level_title, 'waeup.kofa',
1504            target_language=lang)
1505        return translate(_('Course Registration Slip'),
1506            'waeup.kofa', target_language=portal_language) \
1507            + ' %s' % level_title
1508
1509    @property
1510    def tabletitle(self):
1511        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1512        tabletitle = []
1513        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1514            target_language=portal_language))
1515        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1516            target_language=portal_language))
1517        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1518            target_language=portal_language))
1519        return tabletitle
1520
1521    def render(self):
1522        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1523        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1524        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1525        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1526        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1527        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1528        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1529        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1530        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1531        studentview = StudentBasePDFFormPage(self.context.student,
1532            self.request, self.omit_fields)
1533        students_utils = getUtility(IStudentsUtils)
1534
1535        tabledata = []
1536        tableheader = []
1537        for i in range(1,7):
1538            tabledata.append(sorted(
1539                [value for value in self.context.values() if value.semester == i],
1540                key=lambda value: str(value.semester) + value.code))
1541            tableheader.append([(Code,'code', 2.5),
1542                             (Title,'title', 5),
1543                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1544                             (Cred, 'credits', 1.5),
1545                             #(Mand, 'mandatory', 1.5),
1546                             (Score, 'score', 1.5),
1547                             (Grade, 'grade', 1.5),
1548                             #('Auto', 'automatic', 1.5)
1549                             ])
1550        return students_utils.renderPDF(
1551            self, 'course_registration_slip.pdf',
1552            self.context.student, studentview,
1553            tableheader=tableheader,
1554            tabledata=tabledata,
1555            omit_fields=self.omit_fields
1556            )
1557
1558class StudyLevelManageFormPage(KofaEditFormPage):
1559    """ Page to edit the student study level data
1560    """
1561    grok.context(IStudentStudyLevel)
1562    grok.name('manage')
1563    grok.require('waeup.manageStudent')
1564    grok.template('studylevelmanagepage')
1565    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1566        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
1567    pnav = 4
1568    taboneactions = [_('Save'),_('Cancel')]
1569    tabtwoactions = [_('Add course ticket'),
1570        _('Remove selected tickets'),_('Cancel')]
1571    placeholder = _('Enter valid course code')
1572
1573    def update(self, ADD=None, course=None):
1574        if not self.context.__parent__.is_current \
1575            or self.context.student.studycourse_locked:
1576            emit_lock_message(self)
1577            return
1578        super(StudyLevelManageFormPage, self).update()
1579        if ADD is not None:
1580            if not course:
1581                self.flash(_('No valid course code entered.'), type="warning")
1582                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1583                return
1584            cat = queryUtility(ICatalog, name='courses_catalog')
1585            result = cat.searchResults(code=(course, course))
1586            if len(result) != 1:
1587                self.flash(_('Course not found.'), type="warning")
1588            else:
1589                course = list(result)[0]
1590                addCourseTicket(self, course)
1591            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1592        return
1593
1594    @property
1595    def translated_values(self):
1596        return translated_values(self)
1597
1598    @property
1599    def label(self):
1600        # Here we know that the cookie has been set
1601        lang = self.request.cookies.get('kofa.language')
1602        level_title = translate(self.context.level_title, 'waeup.kofa',
1603            target_language=lang)
1604        return _('Manage study level ${a}',
1605            mapping = {'a':level_title})
1606
1607    @action(_('Save'), style='primary')
1608    def save(self, **data):
1609        msave(self, **data)
1610        return
1611
1612    @jsaction(_('Remove selected tickets'))
1613    def delCourseTicket(self, **data):
1614        form = self.request.form
1615        if 'val_id' in form:
1616            child_id = form['val_id']
1617        else:
1618            self.flash(_('No ticket selected.'), type="warning")
1619            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1620            return
1621        if not isinstance(child_id, list):
1622            child_id = [child_id]
1623        deleted = []
1624        for id in child_id:
1625            del self.context[id]
1626            deleted.append(id)
1627        if len(deleted):
1628            self.flash(_('Successfully removed: ${a}',
1629                mapping = {'a':', '.join(deleted)}))
1630            self.context.writeLogMessage(
1631                self,'removed: %s at %s' %
1632                (', '.join(deleted), self.context.level))
1633        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1634        return
1635
1636class ValidateCoursesView(UtilityView, grok.View):
1637    """ Validate course list by course adviser
1638    """
1639    grok.context(IStudentStudyLevel)
1640    grok.name('validate_courses')
1641    grok.require('waeup.validateStudent')
1642
1643    def update(self):
1644        if not self.context.__parent__.is_current:
1645            emit_lock_message(self)
1646            return
1647        if str(self.context.student.current_level) != self.context.__name__:
1648            self.flash(_('This is not the student\'s current level.'),
1649                       type="danger")
1650        elif self.context.student.state == REGISTERED:
1651            IWorkflowInfo(self.context.student).fireTransition(
1652                'validate_courses')
1653            self.flash(_('Course list has been validated.'))
1654        else:
1655            self.flash(_('Student is in the wrong state.'), type="warning")
1656        self.redirect(self.url(self.context))
1657        return
1658
1659    def render(self):
1660        return
1661
1662class RejectCoursesView(UtilityView, grok.View):
1663    """ Reject course list by course adviser
1664    """
1665    grok.context(IStudentStudyLevel)
1666    grok.name('reject_courses')
1667    grok.require('waeup.validateStudent')
1668
1669    def update(self):
1670        if not self.context.__parent__.is_current:
1671            emit_lock_message(self)
1672            return
1673        if str(self.context.__parent__.current_level) != self.context.__name__:
1674            self.flash(_('This is not the student\'s current level.'),
1675                       type="danger")
1676            self.redirect(self.url(self.context))
1677            return
1678        elif self.context.student.state == VALIDATED:
1679            IWorkflowInfo(self.context.student).fireTransition('reset8')
1680            message = _('Course list request has been annulled.')
1681            self.flash(message)
1682        elif self.context.student.state == REGISTERED:
1683            IWorkflowInfo(self.context.student).fireTransition('reset7')
1684            message = _('Course list has been unregistered.')
1685            self.flash(message)
1686        else:
1687            self.flash(_('Student is in the wrong state.'), type="warning")
1688            self.redirect(self.url(self.context))
1689            return
1690        args = {'subject':message}
1691        self.redirect(self.url(self.context.student) +
1692            '/contactstudent?%s' % urlencode(args))
1693        return
1694
1695    def render(self):
1696        return
1697
1698class UnregisterCoursesView(UtilityView, grok.View):
1699    """Unregister course list by student
1700    """
1701    grok.context(IStudentStudyLevel)
1702    grok.name('unregister_courses')
1703    grok.require('waeup.handleStudent')
1704
1705    def update(self):
1706        if not self.context.__parent__.is_current:
1707            emit_lock_message(self)
1708            return
1709        try:
1710            deadline = grok.getSite()['configuration'][
1711                str(self.context.level_session)].coursereg_deadline
1712        except (TypeError, KeyError):
1713            deadline = None
1714        if deadline and deadline < datetime.now(pytz.utc):
1715            self.flash(_(
1716                "Course registration has ended. "
1717                "Unregistration is disabled."), type="warning")
1718        elif str(self.context.__parent__.current_level) != self.context.__name__:
1719            self.flash(_('This is not your current level.'), type="danger")
1720        elif self.context.student.state == REGISTERED:
1721            IWorkflowInfo(self.context.student).fireTransition('reset7')
1722            message = _('Course list has been unregistered.')
1723            self.flash(message)
1724        else:
1725            self.flash(_('You are in the wrong state.'), type="warning")
1726        self.redirect(self.url(self.context))
1727        return
1728
1729    def render(self):
1730        return
1731
1732class CourseTicketAddFormPage(KofaAddFormPage):
1733    """Add a course ticket.
1734    """
1735    grok.context(IStudentStudyLevel)
1736    grok.name('add')
1737    grok.require('waeup.manageStudent')
1738    label = _('Add course ticket')
1739    form_fields = grok.AutoFields(ICourseTicketAdd)
1740    pnav = 4
1741
1742    def update(self):
1743        if not self.context.__parent__.is_current \
1744            or self.context.student.studycourse_locked:
1745            emit_lock_message(self)
1746            return
1747        super(CourseTicketAddFormPage, self).update()
1748        return
1749
1750    @action(_('Add course ticket'), style='primary')
1751    def addCourseTicket(self, **data):
1752        course = data['course']
1753        success = addCourseTicket(self, course)
1754        if success:
1755            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1756        return
1757
1758    @action(_('Cancel'), validator=NullValidator)
1759    def cancel(self, **data):
1760        self.redirect(self.url(self.context))
1761
1762class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1763    """ Page to display course tickets
1764    """
1765    grok.context(ICourseTicket)
1766    grok.name('index')
1767    grok.require('waeup.viewStudent')
1768    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1769    grok.template('courseticketpage')
1770    pnav = 4
1771
1772    @property
1773    def label(self):
1774        return _('${a}: Course Ticket ${b}', mapping = {
1775            'a':self.context.student.display_fullname,
1776            'b':self.context.code})
1777
1778class CourseTicketManageFormPage(KofaEditFormPage):
1779    """ Page to manage course tickets
1780    """
1781    grok.context(ICourseTicket)
1782    grok.name('manage')
1783    grok.require('waeup.manageStudent')
1784    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1785    form_fields['title'].for_display = True
1786    form_fields['fcode'].for_display = True
1787    form_fields['dcode'].for_display = True
1788    form_fields['semester'].for_display = True
1789    form_fields['passmark'].for_display = True
1790    form_fields['credits'].for_display = True
1791    form_fields['mandatory'].for_display = False
1792    form_fields['automatic'].for_display = True
1793    form_fields['carry_over'].for_display = True
1794    pnav = 4
1795    grok.template('courseticketmanagepage')
1796
1797    def update(self):
1798        if not self.context.__parent__.__parent__.is_current \
1799            or self.context.student.studycourse_locked:
1800            emit_lock_message(self)
1801            return
1802        super(CourseTicketManageFormPage, self).update()
1803        return
1804
1805    @property
1806    def label(self):
1807        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1808
1809    @action('Save', style='primary')
1810    def save(self, **data):
1811        msave(self, **data)
1812        return
1813
1814class PaymentsManageFormPage(KofaEditFormPage):
1815    """ Page to manage the student payments
1816
1817    This manage form page is for both students and students officers.
1818    """
1819    grok.context(IStudentPaymentsContainer)
1820    grok.name('index')
1821    grok.require('waeup.viewStudent')
1822    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1823    grok.template('paymentsmanagepage')
1824    pnav = 4
1825
1826    @property
1827    def manage_payments_allowed(self):
1828        return checkPermission('waeup.payStudent', self.context)
1829
1830    def unremovable(self, ticket):
1831        usertype = getattr(self.request.principal, 'user_type', None)
1832        if not usertype:
1833            return False
1834        if not self.manage_payments_allowed:
1835            return True
1836        return (self.request.principal.user_type == 'student' and ticket.r_code)
1837
1838    @property
1839    def label(self):
1840        return _('${a}: Payments',
1841            mapping = {'a':self.context.__parent__.display_fullname})
1842
1843    @jsaction(_('Remove selected tickets'))
1844    def delPaymentTicket(self, **data):
1845        form = self.request.form
1846        if 'val_id' in form:
1847            child_id = form['val_id']
1848        else:
1849            self.flash(_('No payment selected.'), type="warning")
1850            self.redirect(self.url(self.context))
1851            return
1852        if not isinstance(child_id, list):
1853            child_id = [child_id]
1854        deleted = []
1855        for id in child_id:
1856            # Students are not allowed to remove used payment tickets
1857            ticket = self.context.get(id, None)
1858            if ticket is not None and not self.unremovable(ticket):
1859                del self.context[id]
1860                deleted.append(id)
1861        if len(deleted):
1862            self.flash(_('Successfully removed: ${a}',
1863                mapping = {'a': ', '.join(deleted)}))
1864            self.context.writeLogMessage(
1865                self,'removed: %s' % ', '.join(deleted))
1866        self.redirect(self.url(self.context))
1867        return
1868
1869    #@action(_('Add online payment ticket'))
1870    #def addPaymentTicket(self, **data):
1871    #    self.redirect(self.url(self.context, '@@addop'))
1872
1873class OnlinePaymentAddFormPage(KofaAddFormPage):
1874    """ Page to add an online payment ticket
1875    """
1876    grok.context(IStudentPaymentsContainer)
1877    grok.name('addop')
1878    grok.template('onlinepaymentaddform')
1879    grok.require('waeup.payStudent')
1880    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1881        'p_category')
1882    label = _('Add online payment')
1883    pnav = 4
1884
1885    @property
1886    def selectable_categories(self):
1887        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1888        return sorted(categories.items(), key=lambda value: value[1])
1889
1890    @action(_('Create ticket'), style='primary')
1891    def createTicket(self, **data):
1892        p_category = data['p_category']
1893        previous_session = data.get('p_session', None)
1894        previous_level = data.get('p_level', None)
1895        student = self.context.__parent__
1896        # The hostel_application payment category is temporarily used
1897        # by Uniben.
1898        if p_category in ('bed_allocation', 'hostel_application') and student[
1899            'studycourse'].current_session != grok.getSite()[
1900            'hostels'].accommodation_session:
1901                self.flash(
1902                    _('Your current session does not match ' + \
1903                    'accommodation session.'), type="danger")
1904                return
1905        if 'maintenance' in p_category:
1906            current_session = str(student['studycourse'].current_session)
1907            if not current_session in student['accommodation']:
1908                self.flash(_('You have not yet booked accommodation.'),
1909                           type="warning")
1910                return
1911        students_utils = getUtility(IStudentsUtils)
1912        error, payment = students_utils.setPaymentDetails(
1913            p_category, student, previous_session, previous_level)
1914        if error is not None:
1915            self.flash(error, type="danger")
1916            return
1917        if p_category == 'transfer':
1918            payment.p_item = self.request.form['new_programme']
1919        self.context[payment.p_id] = payment
1920        self.flash(_('Payment ticket created.'))
1921        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1922        self.redirect(self.url(self.context))
1923        return
1924
1925    @action(_('Cancel'), validator=NullValidator)
1926    def cancel(self, **data):
1927        self.redirect(self.url(self.context))
1928
1929class PreviousPaymentAddFormPage(KofaAddFormPage):
1930    """ Page to add an online payment ticket for previous sessions.
1931    """
1932    grok.context(IStudentPaymentsContainer)
1933    grok.name('addpp')
1934    grok.require('waeup.payStudent')
1935    form_fields = grok.AutoFields(IStudentPreviousPayment)
1936    label = _('Add previous session online payment')
1937    pnav = 4
1938
1939    def update(self):
1940        if self.context.student.before_payment:
1941            self.flash(_("No previous payment to be made."), type="warning")
1942            self.redirect(self.url(self.context))
1943        super(PreviousPaymentAddFormPage, self).update()
1944        return
1945
1946    @action(_('Create ticket'), style='primary')
1947    def createTicket(self, **data):
1948        p_category = data['p_category']
1949        previous_session = data.get('p_session', None)
1950        previous_level = data.get('p_level', None)
1951        student = self.context.__parent__
1952        students_utils = getUtility(IStudentsUtils)
1953        error, payment = students_utils.setPaymentDetails(
1954            p_category, student, previous_session, previous_level)
1955        if error is not None:
1956            self.flash(error, type="danger")
1957            return
1958        self.context[payment.p_id] = payment
1959        self.flash(_('Payment ticket created.'))
1960        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1961        self.redirect(self.url(self.context))
1962        return
1963
1964    @action(_('Cancel'), validator=NullValidator)
1965    def cancel(self, **data):
1966        self.redirect(self.url(self.context))
1967
1968class BalancePaymentAddFormPage(KofaAddFormPage):
1969    """ Page to add an online payment which can balance s previous session
1970    payment.
1971    """
1972    grok.context(IStudentPaymentsContainer)
1973    grok.name('addbp')
1974    grok.require('waeup.manageStudent')
1975    form_fields = grok.AutoFields(IStudentBalancePayment)
1976    label = _('Add balance')
1977    pnav = 4
1978
1979    @action(_('Create ticket'), style='primary')
1980    def createTicket(self, **data):
1981        p_category = data['p_category']
1982        balance_session = data.get('balance_session', None)
1983        balance_level = data.get('balance_level', None)
1984        balance_amount = data.get('balance_amount', None)
1985        student = self.context.__parent__
1986        students_utils = getUtility(IStudentsUtils)
1987        error, payment = students_utils.setBalanceDetails(
1988            p_category, student, balance_session,
1989            balance_level, balance_amount)
1990        if error is not None:
1991            self.flash(error, type="danger")
1992            return
1993        self.context[payment.p_id] = payment
1994        self.flash(_('Payment ticket created.'))
1995        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1996        self.redirect(self.url(self.context))
1997        return
1998
1999    @action(_('Cancel'), validator=NullValidator)
2000    def cancel(self, **data):
2001        self.redirect(self.url(self.context))
2002
2003class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
2004    """ Page to view an online payment ticket
2005    """
2006    grok.context(IStudentOnlinePayment)
2007    grok.name('index')
2008    grok.require('waeup.viewStudent')
2009    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
2010    form_fields[
2011        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2012    form_fields[
2013        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2014    pnav = 4
2015
2016    @property
2017    def label(self):
2018        return _('${a}: Online Payment Ticket ${b}', mapping = {
2019            'a':self.context.student.display_fullname,
2020            'b':self.context.p_id})
2021
2022class OnlinePaymentApproveView(UtilityView, grok.View):
2023    """ Callback view
2024    """
2025    grok.context(IStudentOnlinePayment)
2026    grok.name('approve')
2027    grok.require('waeup.managePortal')
2028
2029    def update(self):
2030        flashtype, msg, log = self.context.approveStudentPayment()
2031        if log is not None:
2032            # Add log message to students.log
2033            self.context.writeLogMessage(self,log)
2034            # Add log message to payments.log
2035            self.context.logger.info(
2036                '%s,%s,%s,%s,%s,,,,,,' % (
2037                self.context.student.student_id,
2038                self.context.p_id, self.context.p_category,
2039                self.context.amount_auth, self.context.r_code))
2040        self.flash(msg, type=flashtype)
2041        return
2042
2043    def render(self):
2044        self.redirect(self.url(self.context, '@@index'))
2045        return
2046
2047class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
2048    """ Approval view for students.
2049
2050    This view is used for browser tests only and
2051    must be neutralized in custom pages!
2052    """
2053    grok.name('fake_approve')
2054    grok.require('waeup.payStudent')
2055
2056class ExportPDFPaymentSlip(UtilityView, grok.View):
2057    """Deliver a PDF slip of the context.
2058    """
2059    grok.context(IStudentOnlinePayment)
2060    grok.name('payment_slip.pdf')
2061    grok.require('waeup.viewStudent')
2062    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
2063    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2064    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2065    prefix = 'form'
2066    note = None
2067    omit_fields = (
2068        'password', 'suspended', 'phone', 'date_of_birth',
2069        'adm_code', 'sex', 'suspended_comment', 'current_level',
2070        'flash_notice')
2071
2072    @property
2073    def title(self):
2074        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2075        return translate(_('Payment Data'), 'waeup.kofa',
2076            target_language=portal_language)
2077
2078    @property
2079    def label(self):
2080        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2081        return translate(_('Online Payment Slip'),
2082            'waeup.kofa', target_language=portal_language) \
2083            + ' %s' % self.context.p_id
2084
2085    def render(self):
2086        #if self.context.p_state != 'paid':
2087        #    self.flash('Ticket not yet paid.')
2088        #    self.redirect(self.url(self.context))
2089        #    return
2090        studentview = StudentBasePDFFormPage(self.context.student,
2091            self.request, self.omit_fields)
2092        students_utils = getUtility(IStudentsUtils)
2093        return students_utils.renderPDF(self, 'payment_slip.pdf',
2094            self.context.student, studentview, note=self.note,
2095            omit_fields=self.omit_fields)
2096
2097
2098class AccommodationManageFormPage(KofaEditFormPage):
2099    """ Page to manage bed tickets.
2100
2101    This manage form page is for both students and students officers.
2102    """
2103    grok.context(IStudentAccommodation)
2104    grok.name('index')
2105    grok.require('waeup.handleAccommodation')
2106    form_fields = grok.AutoFields(IStudentAccommodation)
2107    grok.template('accommodationmanagepage')
2108    pnav = 4
2109    with_hostel_selection = True
2110
2111    @property
2112    def actionsgroup1(self):
2113        if not self.with_hostel_selection:
2114            return []
2115        students_utils = getUtility(IStudentsUtils)
2116        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2117        error_message = students_utils.checkAccommodationRequirements(
2118            self.context.student, acc_details)
2119        if error_message:
2120            return []
2121        return [_('Save')]
2122
2123    @property
2124    def actionsgroup2(self):
2125        if getattr(self.request.principal, 'user_type', None) == 'student':
2126            return [_('Book accommodation')]
2127        return [_('Book accommodation'), _('Remove selected')]
2128
2129    @property
2130    def label(self):
2131        return _('${a}: Accommodation',
2132            mapping = {'a':self.context.__parent__.display_fullname})
2133
2134    @property
2135    def desired_hostel(self):
2136        if self.context.desired_hostel:
2137            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2138            if hostel is not None:
2139                return hostel.hostel_name
2140        return
2141
2142    def getHostels(self):
2143        """Get a list of all stored hostels.
2144        """
2145        yield(dict(name=None, title='--', selected=''))
2146        for val in grok.getSite()['hostels'].values():
2147            selected = ''
2148            if val.hostel_id == self.context.desired_hostel:
2149                selected = 'selected'
2150            yield(dict(name=val.hostel_id, title=val.hostel_name,
2151                       selected=selected))
2152
2153    @action(_('Save'), style='primary')
2154    def save(self):
2155        hostel = self.request.form.get('hostel', None)
2156        self.context.desired_hostel = hostel
2157        self.flash(_('Your selection has been saved.'))
2158        return
2159
2160    @action(_('Book accommodation'), style='primary')
2161    def bookAccommodation(self, **data):
2162        self.redirect(self.url(self.context, 'add'))
2163        return
2164
2165    @jsaction(_('Remove selected'))
2166    def delBedTickets(self, **data):
2167        if getattr(self.request.principal, 'user_type', None) == 'student':
2168            self.flash(_('You are not allowed to remove bed tickets.'),
2169                       type="warning")
2170            self.redirect(self.url(self.context))
2171            return
2172        form = self.request.form
2173        if 'val_id' in form:
2174            child_id = form['val_id']
2175        else:
2176            self.flash(_('No bed ticket selected.'), type="warning")
2177            self.redirect(self.url(self.context))
2178            return
2179        if not isinstance(child_id, list):
2180            child_id = [child_id]
2181        deleted = []
2182        for id in child_id:
2183            del self.context[id]
2184            deleted.append(id)
2185        if len(deleted):
2186            self.flash(_('Successfully removed: ${a}',
2187                mapping = {'a':', '.join(deleted)}))
2188            self.context.writeLogMessage(
2189                self,'removed: % s' % ', '.join(deleted))
2190        self.redirect(self.url(self.context))
2191        return
2192
2193class BedTicketAddPage(KofaPage):
2194    """ Page to add a bed ticket
2195    """
2196    grok.context(IStudentAccommodation)
2197    grok.name('add')
2198    grok.require('waeup.handleAccommodation')
2199    grok.template('enterpin')
2200    ac_prefix = 'HOS'
2201    label = _('Add bed ticket')
2202    pnav = 4
2203    buttonname = _('Create bed ticket')
2204    notice = ''
2205    with_ac = True
2206
2207    def update(self, SUBMIT=None):
2208        student = self.context.student
2209        students_utils = getUtility(IStudentsUtils)
2210        acc_details  = students_utils.getAccommodationDetails(student)
2211        error_message = students_utils.checkAccommodationRequirements(
2212            student, acc_details)
2213        if error_message:
2214            self.flash(error_message, type="warning")
2215            self.redirect(self.url(self.context))
2216            return
2217        if self.with_ac:
2218            self.ac_series = self.request.form.get('ac_series', None)
2219            self.ac_number = self.request.form.get('ac_number', None)
2220        if SUBMIT is None:
2221            return
2222        if self.with_ac:
2223            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2224            code = get_access_code(pin)
2225            if not code:
2226                self.flash(_('Activation code is invalid.'), type="warning")
2227                return
2228        # Search and book bed
2229        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2230        entries = cat.searchResults(
2231            owner=(student.student_id,student.student_id))
2232        if len(entries):
2233            # If bed space has been manually allocated use this bed
2234            manual = True
2235            bed = [entry for entry in entries][0]
2236            # Safety belt for paranoids: Does this bed really exist on portal?
2237            # XXX: Can be remove if nobody complains.
2238            if bed.__parent__.__parent__ is None:
2239                self.flash(_('System error: Please contact the adminsitrator.'),
2240                           type="danger")
2241                self.context.writeLogMessage(
2242                    self, 'fatal error: %s' % bed.bed_id)
2243                return
2244        else:
2245            # else search for other available beds
2246            manual = False
2247            entries = cat.searchResults(
2248                bed_type=(acc_details['bt'],acc_details['bt']))
2249            available_beds = [
2250                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2251            if available_beds:
2252                students_utils = getUtility(IStudentsUtils)
2253                bed = students_utils.selectBed(
2254                    available_beds, self.context.desired_hostel)
2255                if bed is None:
2256                    self.flash(_(
2257                        'There is no free bed in your desired hostel. '
2258                        'Please try another hostel.'),
2259                        type="warning")
2260                    self.redirect(self.url(self.context))
2261                    return
2262                # Safety belt for paranoids: Does this bed really exist
2263                # in portal?
2264                # XXX: Can be remove if nobody complains.
2265                if bed.__parent__.__parent__ is None:
2266                    self.flash(_(
2267                        'System error: Please contact the administrator.'),
2268                        type="warning")
2269                    self.context.writeLogMessage(
2270                        self, 'fatal error: %s' % bed.bed_id)
2271                    return
2272                bed.bookBed(student.student_id)
2273            else:
2274                self.flash(_('There is no free bed in your category ${a}.',
2275                    mapping = {'a':acc_details['bt']}), type="warning")
2276                self.redirect(self.url(self.context))
2277                return
2278        if self.with_ac:
2279            # Mark pin as used (this also fires a pin related transition)
2280            if code.state == USED:
2281                self.flash(_('Activation code has already been used.'),
2282                           type="warning")
2283                if not manual:
2284                    # Release the previously booked bed
2285                    bed.owner = NOT_OCCUPIED
2286                    # Catalog must be informed
2287                    notify(grok.ObjectModifiedEvent(bed))
2288                return
2289            else:
2290                comment = _(u'invalidated')
2291                # Here we know that the ac is in state initialized so we do not
2292                # expect an exception, but the owner might be different
2293                success = invalidate_accesscode(
2294                    pin, comment, self.context.student.student_id)
2295                if not success:
2296                    self.flash(_('You are not the owner of this access code.'),
2297                               type="warning")
2298                    if not manual:
2299                        # Release the previously booked bed
2300                        bed.owner = NOT_OCCUPIED
2301                        # Catalog must be informed
2302                        notify(grok.ObjectModifiedEvent(bed))
2303                    return
2304        # Create bed ticket
2305        bedticket = createObject(u'waeup.BedTicket')
2306        if self.with_ac:
2307            bedticket.booking_code = pin
2308        bedticket.booking_session = acc_details['booking_session']
2309        bedticket.bed_type = acc_details['bt']
2310        bedticket.bed = bed
2311        hall_title = bed.__parent__.hostel_name
2312        coordinates = bed.coordinates[1:]
2313        block, room_nr, bed_nr = coordinates
2314        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2315            'a':hall_title, 'b':block,
2316            'c':room_nr, 'd':bed_nr,
2317            'e':bed.bed_type})
2318        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2319        bedticket.bed_coordinates = translate(
2320            bc, 'waeup.kofa',target_language=portal_language)
2321        self.context.addBedTicket(bedticket)
2322        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2323        self.flash(_('Bed ticket created and bed booked: ${a}',
2324            mapping = {'a':bedticket.display_coordinates}))
2325        self.redirect(self.url(self.context))
2326        return
2327
2328class BedTicketDisplayFormPage(KofaDisplayFormPage):
2329    """ Page to display bed tickets
2330    """
2331    grok.context(IBedTicket)
2332    grok.name('index')
2333    grok.require('waeup.handleAccommodation')
2334    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2335    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2336    pnav = 4
2337
2338    @property
2339    def label(self):
2340        return _('Bed Ticket for Session ${a}',
2341            mapping = {'a':self.context.getSessionString()})
2342
2343class ExportPDFBedTicketSlip(UtilityView, grok.View):
2344    """Deliver a PDF slip of the context.
2345    """
2346    grok.context(IBedTicket)
2347    grok.name('bed_allocation_slip.pdf')
2348    grok.require('waeup.handleAccommodation')
2349    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2350    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2351    prefix = 'form'
2352    omit_fields = (
2353        'password', 'suspended', 'phone', 'adm_code',
2354        'suspended_comment', 'date_of_birth', 'current_level',
2355        'flash_notice')
2356
2357    @property
2358    def title(self):
2359        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2360        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2361            target_language=portal_language)
2362
2363    @property
2364    def label(self):
2365        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2366        #return translate(_('Bed Allocation: '),
2367        #    'waeup.kofa', target_language=portal_language) \
2368        #    + ' %s' % self.context.bed_coordinates
2369        return translate(_('Bed Allocation Slip'),
2370            'waeup.kofa', target_language=portal_language) \
2371            + ' %s' % self.context.getSessionString()
2372
2373    def render(self):
2374        studentview = StudentBasePDFFormPage(self.context.student,
2375            self.request, self.omit_fields)
2376        students_utils = getUtility(IStudentsUtils)
2377        return students_utils.renderPDF(
2378            self, 'bed_allocation_slip.pdf',
2379            self.context.student, studentview,
2380            omit_fields=self.omit_fields)
2381
2382class BedTicketRelocationView(UtilityView, grok.View):
2383    """ Callback view
2384    """
2385    grok.context(IBedTicket)
2386    grok.name('relocate')
2387    grok.require('waeup.manageHostels')
2388
2389    # Relocate student if student parameters have changed or the bed_type
2390    # of the bed has changed
2391    def update(self):
2392        success, msg = self.context.relocateStudent()
2393        if not success:
2394            self.flash(msg, type="warning")
2395        else:
2396            self.flash(msg)
2397        self.redirect(self.url(self.context))
2398        return
2399
2400    def render(self):
2401        return
2402
2403class StudentHistoryPage(KofaPage):
2404    """ Page to display student history
2405    """
2406    grok.context(IStudent)
2407    grok.name('history')
2408    grok.require('waeup.viewStudent')
2409    grok.template('studenthistory')
2410    pnav = 4
2411
2412    @property
2413    def label(self):
2414        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2415
2416# Pages for students only
2417
2418class StudentBaseEditFormPage(KofaEditFormPage):
2419    """ View to edit student base data
2420    """
2421    grok.context(IStudent)
2422    grok.name('edit_base')
2423    grok.require('waeup.handleStudent')
2424    form_fields = grok.AutoFields(IStudentBase).select(
2425        'email', 'phone')
2426    label = _('Edit base data')
2427    pnav = 4
2428
2429    @action(_('Save'), style='primary')
2430    def save(self, **data):
2431        msave(self, **data)
2432        return
2433
2434class StudentChangePasswordPage(KofaEditFormPage):
2435    """ View to edit student passwords
2436    """
2437    grok.context(IStudent)
2438    grok.name('change_password')
2439    grok.require('waeup.handleStudent')
2440    grok.template('change_password')
2441    label = _('Change password')
2442    pnav = 4
2443
2444    @action(_('Save'), style='primary')
2445    def save(self, **data):
2446        form = self.request.form
2447        password = form.get('change_password', None)
2448        password_ctl = form.get('change_password_repeat', None)
2449        if password:
2450            validator = getUtility(IPasswordValidator)
2451            errors = validator.validate_password(password, password_ctl)
2452            if not errors:
2453                IUserAccount(self.context).setPassword(password)
2454                # Unset temporary password
2455                self.context.temp_password = None
2456                self.context.writeLogMessage(self, 'saved: password')
2457                self.flash(_('Password changed.'))
2458            else:
2459                self.flash( ' '.join(errors), type="warning")
2460        return
2461
2462class StudentFilesUploadPage(KofaPage):
2463    """ View to upload files by student
2464    """
2465    grok.context(IStudent)
2466    grok.name('change_portrait')
2467    grok.require('waeup.uploadStudentFile')
2468    grok.template('filesuploadpage')
2469    label = _('Upload portrait')
2470    pnav = 4
2471
2472    def update(self):
2473        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2474        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2475            emit_lock_message(self)
2476            return
2477        super(StudentFilesUploadPage, self).update()
2478        return
2479
2480class StartClearancePage(KofaPage):
2481    grok.context(IStudent)
2482    grok.name('start_clearance')
2483    grok.require('waeup.handleStudent')
2484    grok.template('enterpin')
2485    label = _('Start clearance')
2486    ac_prefix = 'CLR'
2487    notice = ''
2488    pnav = 4
2489    buttonname = _('Start clearance now')
2490    with_ac = True
2491
2492    @property
2493    def all_required_fields_filled(self):
2494        if not self.context.email:
2495            return _("Email address is missing."), 'edit_base'
2496        if not self.context.phone:
2497            return _("Phone number is missing."), 'edit_base'
2498        return
2499
2500    @property
2501    def portrait_uploaded(self):
2502        store = getUtility(IExtFileStore)
2503        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2504            return True
2505        return False
2506
2507    def update(self, SUBMIT=None):
2508        if not self.context.state == ADMITTED:
2509            self.flash(_("Wrong state"), type="warning")
2510            self.redirect(self.url(self.context))
2511            return
2512        if not self.portrait_uploaded:
2513            self.flash(_("No portrait uploaded."), type="warning")
2514            self.redirect(self.url(self.context, 'change_portrait'))
2515            return
2516        if self.all_required_fields_filled:
2517            arf_warning = self.all_required_fields_filled[0]
2518            arf_redirect = self.all_required_fields_filled[1]
2519            self.flash(arf_warning, type="warning")
2520            self.redirect(self.url(self.context, arf_redirect))
2521            return
2522        if self.with_ac:
2523            self.ac_series = self.request.form.get('ac_series', None)
2524            self.ac_number = self.request.form.get('ac_number', None)
2525        if SUBMIT is None:
2526            return
2527        if self.with_ac:
2528            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2529            code = get_access_code(pin)
2530            if not code:
2531                self.flash(_('Activation code is invalid.'), type="warning")
2532                return
2533            if code.state == USED:
2534                self.flash(_('Activation code has already been used.'),
2535                           type="warning")
2536                return
2537            # Mark pin as used (this also fires a pin related transition)
2538            # and fire transition start_clearance
2539            comment = _(u"invalidated")
2540            # Here we know that the ac is in state initialized so we do not
2541            # expect an exception, but the owner might be different
2542            if not invalidate_accesscode(pin, comment, self.context.student_id):
2543                self.flash(_('You are not the owner of this access code.'),
2544                           type="warning")
2545                return
2546            self.context.clr_code = pin
2547        IWorkflowInfo(self.context).fireTransition('start_clearance')
2548        self.flash(_('Clearance process has been started.'))
2549        self.redirect(self.url(self.context,'cedit'))
2550        return
2551
2552class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2553    """ View to edit student clearance data by student
2554    """
2555    grok.context(IStudent)
2556    grok.name('cedit')
2557    grok.require('waeup.handleStudent')
2558    label = _('Edit clearance data')
2559
2560    @property
2561    def form_fields(self):
2562        if self.context.is_postgrad:
2563            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2564                'clr_code', 'officer_comment')
2565        else:
2566            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2567                'clr_code', 'officer_comment')
2568        return form_fields
2569
2570    def update(self):
2571        if self.context.clearance_locked:
2572            emit_lock_message(self)
2573            return
2574        return super(StudentClearanceEditFormPage, self).update()
2575
2576    @action(_('Save'), style='primary')
2577    def save(self, **data):
2578        self.applyData(self.context, **data)
2579        self.flash(_('Clearance form has been saved.'))
2580        return
2581
2582    def dataNotComplete(self):
2583        """To be implemented in the customization package.
2584        """
2585        return False
2586
2587    @action(_('Save and request clearance'), style='primary',
2588            warning=_('You can not edit your data after '
2589            'requesting clearance. You really want to request clearance now?'))
2590    def requestClearance(self, **data):
2591        self.applyData(self.context, **data)
2592        if self.dataNotComplete():
2593            self.flash(self.dataNotComplete(), type="warning")
2594            return
2595        self.flash(_('Clearance form has been saved.'))
2596        if self.context.clr_code:
2597            self.redirect(self.url(self.context, 'request_clearance'))
2598        else:
2599            # We bypass the request_clearance page if student
2600            # has been imported in state 'clearance started' and
2601            # no clr_code was entered before.
2602            state = IWorkflowState(self.context).getState()
2603            if state != CLEARANCE:
2604                # This shouldn't happen, but the application officer
2605                # might have forgotten to lock the form after changing the state
2606                self.flash(_('This form cannot be submitted. Wrong state!'),
2607                           type="danger")
2608                return
2609            IWorkflowInfo(self.context).fireTransition('request_clearance')
2610            self.flash(_('Clearance has been requested.'))
2611            self.redirect(self.url(self.context))
2612        return
2613
2614class RequestClearancePage(KofaPage):
2615    grok.context(IStudent)
2616    grok.name('request_clearance')
2617    grok.require('waeup.handleStudent')
2618    grok.template('enterpin')
2619    label = _('Request clearance')
2620    notice = _('Enter the CLR access code used for starting clearance.')
2621    ac_prefix = 'CLR'
2622    pnav = 4
2623    buttonname = _('Request clearance now')
2624    with_ac = True
2625
2626    def update(self, SUBMIT=None):
2627        if self.with_ac:
2628            self.ac_series = self.request.form.get('ac_series', None)
2629            self.ac_number = self.request.form.get('ac_number', None)
2630        if SUBMIT is None:
2631            return
2632        if self.with_ac:
2633            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2634            if self.context.clr_code and self.context.clr_code != pin:
2635                self.flash(_("This isn't your CLR access code."), type="danger")
2636                return
2637        state = IWorkflowState(self.context).getState()
2638        if state != CLEARANCE:
2639            # This shouldn't happen, but the application officer
2640            # might have forgotten to lock the form after changing the state
2641            self.flash(_('This form cannot be submitted. Wrong state!'),
2642                       type="danger")
2643            return
2644        IWorkflowInfo(self.context).fireTransition('request_clearance')
2645        self.flash(_('Clearance has been requested.'))
2646        self.redirect(self.url(self.context))
2647        return
2648
2649class StartSessionPage(KofaPage):
2650    grok.context(IStudentStudyCourse)
2651    grok.name('start_session')
2652    grok.require('waeup.handleStudent')
2653    grok.template('enterpin')
2654    label = _('Start session')
2655    ac_prefix = 'SFE'
2656    notice = ''
2657    pnav = 4
2658    buttonname = _('Start now')
2659    with_ac = True
2660
2661    def update(self, SUBMIT=None):
2662        if not self.context.is_current:
2663            emit_lock_message(self)
2664            return
2665        super(StartSessionPage, self).update()
2666        if not self.context.next_session_allowed:
2667            self.flash(_("You are not entitled to start session."),
2668                       type="warning")
2669            self.redirect(self.url(self.context))
2670            return
2671        if self.with_ac:
2672            self.ac_series = self.request.form.get('ac_series', None)
2673            self.ac_number = self.request.form.get('ac_number', None)
2674        if SUBMIT is None:
2675            return
2676        if self.with_ac:
2677            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2678            code = get_access_code(pin)
2679            if not code:
2680                self.flash(_('Activation code is invalid.'), type="warning")
2681                return
2682            # Mark pin as used (this also fires a pin related transition)
2683            if code.state == USED:
2684                self.flash(_('Activation code has already been used.'),
2685                           type="warning")
2686                return
2687            else:
2688                comment = _(u"invalidated")
2689                # Here we know that the ac is in state initialized so we do not
2690                # expect an error, but the owner might be different
2691                if not invalidate_accesscode(
2692                    pin,comment,self.context.student.student_id):
2693                    self.flash(_('You are not the owner of this access code.'),
2694                               type="warning")
2695                    return
2696        try:
2697            if self.context.student.state == CLEARED:
2698                IWorkflowInfo(self.context.student).fireTransition(
2699                    'pay_first_school_fee')
2700            elif self.context.student.state == RETURNING:
2701                IWorkflowInfo(self.context.student).fireTransition(
2702                    'pay_school_fee')
2703            elif self.context.student.state == PAID:
2704                IWorkflowInfo(self.context.student).fireTransition(
2705                    'pay_pg_fee')
2706        except ConstraintNotSatisfied:
2707            self.flash(_('An error occurred, please contact the system administrator.'),
2708                       type="danger")
2709            return
2710        self.flash(_('Session started.'))
2711        self.redirect(self.url(self.context))
2712        return
2713
2714class AddStudyLevelFormPage(KofaEditFormPage):
2715    """ Page for students to add current study levels
2716    """
2717    grok.context(IStudentStudyCourse)
2718    grok.name('add')
2719    grok.require('waeup.handleStudent')
2720    grok.template('studyleveladdpage')
2721    form_fields = grok.AutoFields(IStudentStudyCourse)
2722    pnav = 4
2723
2724    @property
2725    def label(self):
2726        studylevelsource = StudyLevelSource().factory
2727        code = self.context.current_level
2728        title = studylevelsource.getTitle(self.context, code)
2729        return _('Add current level ${a}', mapping = {'a':title})
2730
2731    def update(self):
2732        if not self.context.is_current \
2733            or self.context.student.studycourse_locked:
2734            emit_lock_message(self)
2735            return
2736        if self.context.student.state != PAID:
2737            emit_lock_message(self)
2738            return
2739        code = self.context.current_level
2740        if code is None:
2741            self.flash(_('Your data are incomplete'), type="danger")
2742            self.redirect(self.url(self.context))
2743            return
2744        super(AddStudyLevelFormPage, self).update()
2745        return
2746
2747    @action(_('Create course list now'), style='primary')
2748    def addStudyLevel(self, **data):
2749        studylevel = createObject(u'waeup.StudentStudyLevel')
2750        studylevel.level = self.context.current_level
2751        studylevel.level_session = self.context.current_session
2752        try:
2753            self.context.addStudentStudyLevel(
2754                self.context.certificate,studylevel)
2755        except KeyError:
2756            self.flash(_('This level exists.'), type="warning")
2757            self.redirect(self.url(self.context))
2758            return
2759        except RequiredMissing:
2760            self.flash(_('Your data are incomplete.'), type="danger")
2761            self.redirect(self.url(self.context))
2762            return
2763        self.flash(_('You successfully created a new course list.'))
2764        self.redirect(self.url(self.context, str(studylevel.level)))
2765        return
2766
2767class StudyLevelEditFormPage(KofaEditFormPage):
2768    """ Page to edit the student study level data by students
2769    """
2770    grok.context(IStudentStudyLevel)
2771    grok.name('edit')
2772    grok.require('waeup.editStudyLevel')
2773    grok.template('studyleveleditpage')
2774    pnav = 4
2775    placeholder = _('Enter valid course code')
2776
2777    def update(self, ADD=None, course=None):
2778        if not self.context.__parent__.is_current:
2779            emit_lock_message(self)
2780            return
2781        if self.context.student.state != PAID or \
2782            not self.context.is_current_level:
2783            emit_lock_message(self)
2784            return
2785        super(StudyLevelEditFormPage, self).update()
2786        if ADD is not None:
2787            if not course:
2788                self.flash(_('No valid course code entered.'), type="warning")
2789                return
2790            cat = queryUtility(ICatalog, name='courses_catalog')
2791            result = cat.searchResults(code=(course, course))
2792            if len(result) != 1:
2793                self.flash(_('Course not found.'), type="warning")
2794                return
2795            course = list(result)[0]
2796            addCourseTicket(self, course)
2797        return
2798
2799    @property
2800    def label(self):
2801        # Here we know that the cookie has been set
2802        lang = self.request.cookies.get('kofa.language')
2803        level_title = translate(self.context.level_title, 'waeup.kofa',
2804            target_language=lang)
2805        return _('Edit course list of ${a}',
2806            mapping = {'a':level_title})
2807
2808    @property
2809    def translated_values(self):
2810        return translated_values(self)
2811
2812    def _delCourseTicket(self, **data):
2813        form = self.request.form
2814        if 'val_id' in form:
2815            child_id = form['val_id']
2816        else:
2817            self.flash(_('No ticket selected.'), type="warning")
2818            self.redirect(self.url(self.context, '@@edit'))
2819            return
2820        if not isinstance(child_id, list):
2821            child_id = [child_id]
2822        deleted = []
2823        for id in child_id:
2824            # Students are not allowed to remove core tickets
2825            if id in self.context and \
2826                self.context[id].removable_by_student:
2827                del self.context[id]
2828                deleted.append(id)
2829        if len(deleted):
2830            self.flash(_('Successfully removed: ${a}',
2831                mapping = {'a':', '.join(deleted)}))
2832            self.context.writeLogMessage(
2833                self,'removed: %s at %s' %
2834                (', '.join(deleted), self.context.level))
2835        self.redirect(self.url(self.context, u'@@edit'))
2836        return
2837
2838    @jsaction(_('Remove selected tickets'))
2839    def delCourseTicket(self, **data):
2840        self._delCourseTicket(**data)
2841        return
2842
2843    def _updateTickets(self, **data):
2844        cat = queryUtility(ICatalog, name='courses_catalog')
2845        invalidated = list()
2846        for value in self.context.values():
2847            result = cat.searchResults(code=(value.code, value.code))
2848            if len(result) != 1:
2849                course = None
2850            else:
2851                course = list(result)[0]
2852            invalid = self.context.updateCourseTicket(value, course)
2853            if invalid:
2854                invalidated.append(invalid)
2855        if invalidated:
2856            invalidated_string = ', '.join(invalidated)
2857            self.context.writeLogMessage(
2858                self, 'course tickets invalidated: %s' % invalidated_string)
2859        self.flash(_('All course tickets updated.'))
2860        return
2861
2862    @action(_('Update all tickets'),
2863        tooltip=_('Update all course parameters including course titles.'))
2864    def updateTickets(self, **data):
2865        self._updateTickets(**data)
2866        return
2867
2868    def _registerCourses(self, **data):
2869        if self.context.student.is_postgrad and \
2870            not self.context.student.is_special_postgrad:
2871            self.flash(_(
2872                "You are a postgraduate student, "
2873                "your course list can't bee registered."), type="warning")
2874            self.redirect(self.url(self.context))
2875            return
2876        students_utils = getUtility(IStudentsUtils)
2877        warning = students_utils.warnCreditsOOR(self.context)
2878        if warning:
2879            self.flash(warning, type="warning")
2880            return
2881        msg = self.context.course_registration_forbidden
2882        if msg:
2883            self.flash(msg, type="warning")
2884            return
2885        IWorkflowInfo(self.context.student).fireTransition(
2886            'register_courses')
2887        self.flash(_('Course list has been registered.'))
2888        self.redirect(self.url(self.context))
2889        return
2890
2891    @action(_('Register course list'), style='primary',
2892        warning=_('You can not edit your course list after registration.'
2893            ' You really want to register?'))
2894    def registerCourses(self, **data):
2895        self._registerCourses(**data)
2896        return
2897
2898class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2899    """Add a course ticket by student.
2900    """
2901    grok.name('ctadd')
2902    grok.require('waeup.handleStudent')
2903    form_fields = grok.AutoFields(ICourseTicketAdd)
2904
2905    def update(self):
2906        if self.context.student.state != PAID or \
2907            not self.context.is_current_level:
2908            emit_lock_message(self)
2909            return
2910        super(CourseTicketAddFormPage2, self).update()
2911        return
2912
2913    @action(_('Add course ticket'))
2914    def addCourseTicket(self, **data):
2915        # Safety belt
2916        if self.context.student.state != PAID:
2917            return
2918        course = data['course']
2919        success = addCourseTicket(self, course)
2920        if success:
2921            self.redirect(self.url(self.context, u'@@edit'))
2922        return
2923
2924class SetPasswordPage(KofaPage):
2925    grok.context(IKofaObject)
2926    grok.name('setpassword')
2927    grok.require('waeup.Anonymous')
2928    grok.template('setpassword')
2929    label = _('Set password for first-time login')
2930    ac_prefix = 'PWD'
2931    pnav = 0
2932    set_button = _('Set')
2933
2934    def update(self, SUBMIT=None):
2935        self.reg_number = self.request.form.get('reg_number', None)
2936        self.ac_series = self.request.form.get('ac_series', None)
2937        self.ac_number = self.request.form.get('ac_number', None)
2938
2939        if SUBMIT is None:
2940            return
2941        hitlist = search(query=self.reg_number,
2942            searchtype='reg_number', view=self)
2943        if not hitlist:
2944            self.flash(_('No student found.'), type="warning")
2945            return
2946        if len(hitlist) != 1:   # Cannot happen but anyway
2947            self.flash(_('More than one student found.'), type="warning")
2948            return
2949        student = hitlist[0].context
2950        self.student_id = student.student_id
2951        student_pw = student.password
2952        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2953        code = get_access_code(pin)
2954        if not code:
2955            self.flash(_('Access code is invalid.'), type="warning")
2956            return
2957        if student_pw and pin == student.adm_code:
2958            self.flash(_(
2959                'Password has already been set. Your Student Id is ${a}',
2960                mapping = {'a':self.student_id}))
2961            return
2962        elif student_pw:
2963            self.flash(
2964                _('Password has already been set. You are using the ' +
2965                'wrong Access Code.'), type="warning")
2966            return
2967        # Mark pin as used (this also fires a pin related transition)
2968        # and set student password
2969        if code.state == USED:
2970            self.flash(_('Access code has already been used.'), type="warning")
2971            return
2972        else:
2973            comment = _(u"invalidated")
2974            # Here we know that the ac is in state initialized so we do not
2975            # expect an exception
2976            invalidate_accesscode(pin,comment)
2977            IUserAccount(student).setPassword(self.ac_number)
2978            student.adm_code = pin
2979        self.flash(_('Password has been set. Your Student Id is ${a}',
2980            mapping = {'a':self.student_id}))
2981        return
2982
2983class StudentRequestPasswordPage(KofaAddFormPage):
2984    """Captcha'd request password page for students.
2985    """
2986    grok.name('requestpw')
2987    grok.require('waeup.Anonymous')
2988    grok.template('requestpw')
2989    form_fields = grok.AutoFields(IStudentRequestPW).select(
2990        'lastname','number','email')
2991    label = _('Request password for first-time login')
2992
2993    def update(self):
2994        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2995        if blocker:
2996            self.flash(_('The portal is in maintenance mode. '
2997                        'Password request forms are temporarily disabled.'),
2998                       type='warning')
2999            self.redirect(self.url(self.context))
3000            return
3001        # Handle captcha
3002        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3003        self.captcha_result = self.captcha.verify(self.request)
3004        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3005        return
3006
3007    def _redirect(self, email, password, student_id):
3008        # Forward only email to landing page in base package.
3009        self.redirect(self.url(self.context, 'requestpw_complete',
3010            data = dict(email=email)))
3011        return
3012
3013    def _redirect_no_student(self):
3014        # No record found, this is the truth. We do not redirect here.
3015        # We are using this method in custom packages
3016        # for redirecting alumni to the application section.
3017        self.flash(_('No student record found.'), type="warning")
3018        return
3019
3020    def _pw_used(self):
3021        # XXX: False if password has not been used. We need an extra
3022        #      attribute which remembers if student logged in.
3023        return True
3024
3025    @action(_('Send login credentials to email address'), style='primary')
3026    def get_credentials(self, **data):
3027        if not self.captcha_result.is_valid:
3028            # Captcha will display error messages automatically.
3029            # No need to flash something.
3030            return
3031        number = data.get('number','')
3032        lastname = data.get('lastname','')
3033        cat = getUtility(ICatalog, name='students_catalog')
3034        results = list(
3035            cat.searchResults(reg_number=(number, number)))
3036        if not results:
3037            results = list(
3038                cat.searchResults(matric_number=(number, number)))
3039        if results:
3040            student = results[0]
3041            if getattr(student,'lastname',None) is None:
3042                self.flash(_('An error occurred.'), type="danger")
3043                return
3044            elif student.lastname.lower() != lastname.lower():
3045                # Don't tell the truth here. Anonymous must not
3046                # know that a record was found and only the lastname
3047                # verification failed.
3048                self.flash(_('No student record found.'), type="warning")
3049                return
3050            elif student.password is not None and self._pw_used:
3051                self.flash(_('Your password has already been set and used. '
3052                             'Please proceed to the login page.'),
3053                           type="warning")
3054                return
3055            # Store email address but nothing else.
3056            student.email = data['email']
3057            notify(grok.ObjectModifiedEvent(student))
3058        else:
3059            self._redirect_no_student()
3060            return
3061
3062        kofa_utils = getUtility(IKofaUtils)
3063        password = kofa_utils.genPassword()
3064        mandate = PasswordMandate()
3065        mandate.params['password'] = password
3066        mandate.params['user'] = student
3067        site = grok.getSite()
3068        site['mandates'].addMandate(mandate)
3069        # Send email with credentials
3070        args = {'mandate_id':mandate.mandate_id}
3071        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3072        url_info = u'Confirmation link: %s' % mandate_url
3073        msg = _('You have successfully requested a password for the')
3074        if kofa_utils.sendCredentials(IUserAccount(student),
3075            password, url_info, msg):
3076            email_sent = student.email
3077        else:
3078            email_sent = None
3079        self._redirect(email=email_sent, password=password,
3080            student_id=student.student_id)
3081        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3082        self.context.logger.info(
3083            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3084        return
3085
3086class StudentRequestPasswordEmailSent(KofaPage):
3087    """Landing page after successful password request.
3088
3089    """
3090    grok.name('requestpw_complete')
3091    grok.require('waeup.Public')
3092    grok.template('requestpwmailsent')
3093    label = _('Your password request was successful.')
3094
3095    def update(self, email=None, student_id=None, password=None):
3096        self.email = email
3097        self.password = password
3098        self.student_id = student_id
3099        return
3100
3101class FilterStudentsInDepartmentPage(KofaPage):
3102    """Page that filters and lists students.
3103    """
3104    grok.context(IDepartment)
3105    grok.require('waeup.showStudents')
3106    grok.name('students')
3107    grok.template('filterstudentspage')
3108    pnav = 1
3109    session_label = _('Current Session')
3110    level_label = _('Current Level')
3111
3112    def label(self):
3113        return 'Students in %s' % self.context.longtitle
3114
3115    def _set_session_values(self):
3116        vocab_terms = academic_sessions_vocab.by_value.values()
3117        self.sessions = sorted(
3118            [(x.title, x.token) for x in vocab_terms], reverse=True)
3119        self.sessions += [('All Sessions', 'all')]
3120        return
3121
3122    def _set_level_values(self):
3123        vocab_terms = course_levels.by_value.values()
3124        self.levels = sorted(
3125            [(x.title, x.token) for x in vocab_terms])
3126        self.levels += [('All Levels', 'all')]
3127        return
3128
3129    def _searchCatalog(self, session, level):
3130        if level not in (10, 999, None):
3131            start_level = 100 * (level // 100)
3132            end_level = start_level + 90
3133        else:
3134            start_level = end_level = level
3135        cat = queryUtility(ICatalog, name='students_catalog')
3136        students = cat.searchResults(
3137            current_session=(session, session),
3138            current_level=(start_level, end_level),
3139            depcode=(self.context.code, self.context.code)
3140            )
3141        hitlist = []
3142        for student in students:
3143            hitlist.append(StudentQueryResultItem(student, view=self))
3144        return hitlist
3145
3146    def update(self, SHOW=None, session=None, level=None):
3147        self.parent_url = self.url(self.context.__parent__)
3148        self._set_session_values()
3149        self._set_level_values()
3150        self.hitlist = []
3151        self.session_default = session
3152        self.level_default = level
3153        if SHOW is not None:
3154            if session != 'all':
3155                self.session = int(session)
3156                self.session_string = '%s %s/%s' % (
3157                    self.session_label, self.session, self.session+1)
3158            else:
3159                self.session = None
3160                self.session_string = _('in any session')
3161            if level != 'all':
3162                self.level = int(level)
3163                self.level_string = '%s %s' % (self.level_label, self.level)
3164            else:
3165                self.level = None
3166                self.level_string = _('at any level')
3167            self.hitlist = self._searchCatalog(self.session, self.level)
3168            if not self.hitlist:
3169                self.flash(_('No student found.'), type="warning")
3170        return
3171
3172class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3173    """Page that filters and lists students.
3174    """
3175    grok.context(ICertificate)
3176
3177    def label(self):
3178        return 'Students studying %s' % self.context.longtitle
3179
3180    def _searchCatalog(self, session, level):
3181        if level not in (10, 999, None):
3182            start_level = 100 * (level // 100)
3183            end_level = start_level + 90
3184        else:
3185            start_level = end_level = level
3186        cat = queryUtility(ICatalog, name='students_catalog')
3187        students = cat.searchResults(
3188            current_session=(session, session),
3189            current_level=(start_level, end_level),
3190            certcode=(self.context.code, self.context.code)
3191            )
3192        hitlist = []
3193        for student in students:
3194            hitlist.append(StudentQueryResultItem(student, view=self))
3195        return hitlist
3196
3197class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3198    """Page that filters and lists students.
3199    """
3200    grok.context(ICourse)
3201    grok.require('waeup.viewStudent')
3202
3203    session_label = _('Session')
3204    level_label = _('Level')
3205
3206    def label(self):
3207        return 'Students registered for %s' % self.context.longtitle
3208
3209    def _searchCatalog(self, session, level):
3210        if level not in (10, 999, None):
3211            start_level = 100 * (level // 100)
3212            end_level = start_level + 90
3213        else:
3214            start_level = end_level = level
3215        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3216        coursetickets = cat.searchResults(
3217            session=(session, session),
3218            level=(start_level, end_level),
3219            code=(self.context.code, self.context.code)
3220            )
3221        hitlist = []
3222        for ticket in coursetickets:
3223            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3224        return list(set(hitlist))
3225
3226class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3227    """ Clear all students of a department in state 'clearance requested'.
3228    """
3229    grok.context(IDepartment)
3230    grok.name('clearallstudents')
3231    grok.require('waeup.clearAllStudents')
3232
3233    def update(self):
3234        cat = queryUtility(ICatalog, name='students_catalog')
3235        students = cat.searchResults(
3236            depcode=(self.context.code, self.context.code),
3237            state=(REQUESTED, REQUESTED)
3238            )
3239        num = 0
3240        for student in students:
3241            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3242                continue
3243            IWorkflowInfo(student).fireTransition('clear')
3244            num += 1
3245        self.flash(_('%d students have been cleared.' % num))
3246        self.redirect(self.url(self.context))
3247        return
3248
3249    def render(self):
3250        return
3251
3252
3253class EditScoresPage(KofaPage):
3254    """Page that allows to edit batches of scores.
3255    """
3256    grok.context(ICourse)
3257    grok.require('waeup.editScores')
3258    grok.name('edit_scores')
3259    grok.template('editscorespage')
3260    pnav = 1
3261    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3262
3263    def label(self):
3264        return '%s tickets in academic session %s' % (
3265            self.context.code, self.session_title)
3266
3267    def _searchCatalog(self, session):
3268        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3269        coursetickets = cat.searchResults(
3270            session=(session, session),
3271            code=(self.context.code, self.context.code)
3272            )
3273        return list(coursetickets)
3274
3275    def _extract_uploadfile(self, uploadfile):
3276        """Get a mapping of student-ids to scores.
3277
3278        The mapping is constructed by reading contents from `uploadfile`.
3279
3280        We expect uploadfile to be a regular CSV file with columns
3281        ``student_id`` and ``score`` (other cols are ignored).
3282        """
3283        result = dict()
3284        data = StringIO(uploadfile.read())  # ensure we have something seekable
3285        reader = csv.DictReader(data)
3286        for row in reader:
3287            if not 'student_id' in row or not 'score' in row:
3288                continue
3289            result[row['student_id']] = row['score']
3290        return result
3291
3292    def _update_scores(self, form):
3293        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3294        error = ''
3295        if 'UPDATE_FILE' in form:
3296            if form['uploadfile']:
3297                try:
3298                    formvals = self._extract_uploadfile(form['uploadfile'])
3299                except:
3300                    self.flash(
3301                        _('Uploaded file contains illegal data. Ignored'),
3302                        type="danger")
3303                    return False
3304            else:
3305                self.flash(
3306                    _('No file provided.'), type="danger")
3307                return False
3308        else:
3309            formvals = dict(zip(form['sids'], form['scores']))
3310        for ticket in self.editable_tickets:
3311            score = ticket.score
3312            sid = ticket.student.student_id
3313            if sid not in formvals:
3314                continue
3315            if formvals[sid] == '':
3316                score = None
3317            else:
3318                try:
3319                    score = int(formvals[sid])
3320                except ValueError:
3321                    error += '%s, ' % ticket.student.display_fullname
3322            if ticket.score != score:
3323                ticket.score = score
3324                ticket.student.__parent__.logger.info(
3325                    '%s - %s %s/%s score updated (%s)' % (
3326                        ob_class, ticket.student.student_id,
3327                        ticket.level, ticket.code, score)
3328                    )
3329        if error:
3330            self.flash(
3331                _('Error: Score(s) of following students have not been '
3332                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3333                type="danger")
3334        return True
3335
3336    def update(self,  *args, **kw):
3337        form = self.request.form
3338        self.current_academic_session = grok.getSite()[
3339            'configuration'].current_academic_session
3340        if self.context.__parent__.__parent__.score_editing_disabled:
3341            self.flash(_('Score editing disabled.'), type="warning")
3342            self.redirect(self.url(self.context))
3343            return
3344        if not self.current_academic_session:
3345            self.flash(_('Current academic session not set.'), type="warning")
3346            self.redirect(self.url(self.context))
3347            return
3348        self.session_title = academic_sessions_vocab.getTerm(
3349            self.current_academic_session).title
3350        self.tickets = self._searchCatalog(self.current_academic_session)
3351        if not self.tickets:
3352            self.flash(_('No student found.'), type="warning")
3353            self.redirect(self.url(self.context))
3354            return
3355        self.editable_tickets = [
3356            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3357        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3358            return
3359        if not self.editable_tickets:
3360            return
3361        success = self._update_scores(form)
3362        if success:
3363            self.flash(_('You successfully updated course results.'))
3364        return
3365
3366
3367class DownloadScoresView(UtilityView, grok.View):
3368    """View that exports scores.
3369    """
3370    grok.context(ICourse)
3371    grok.require('waeup.editScores')
3372    grok.name('download_scores')
3373
3374    def update(self):
3375        self.current_academic_session = grok.getSite()[
3376            'configuration'].current_academic_session
3377        if self.context.__parent__.__parent__.score_editing_disabled:
3378            self.flash(_('Score editing disabled.'), type="warning")
3379            self.redirect(self.url(self.context))
3380            return
3381        if not self.current_academic_session:
3382            self.flash(_('Current academic session not set.'), type="warning")
3383            self.redirect(self.url(self.context))
3384            return
3385        site = grok.getSite()
3386        exporter = getUtility(ICSVExporter, name='lecturer')
3387        self.csv = exporter.export_filtered(site, filepath=None,
3388                                 catalog='coursetickets',
3389                                 session=self.current_academic_session,
3390                                 level=None,
3391                                 code=self.context.code)
3392        return
3393
3394    def render(self):
3395        filename = 'results_%s_%s.csv' % (
3396            self.context.code, self.current_academic_session)
3397        self.response.setHeader(
3398            'Content-Type', 'text/csv; charset=UTF-8')
3399        self.response.setHeader(
3400            'Content-Disposition:', 'attachment; filename="%s' % filename)
3401        return self.csv
3402
3403class ExportPDFScoresSlip(UtilityView, grok.View,
3404    LocalRoleAssignmentUtilityView):
3405    """Deliver a PDF slip of course tickets for a lecturer.
3406    """
3407    grok.context(ICourse)
3408    grok.name('coursetickets.pdf')
3409    grok.require('waeup.editScores')
3410
3411    def data(self, session):
3412        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3413        coursetickets = cat.searchResults(
3414            session=(session, session),
3415            code=(self.context.code, self.context.code)
3416            )
3417        header = [[_('Matric No.'),
3418                   _('Reg. No.'),
3419                   _('Fullname'),
3420                   _('Status'),
3421                   _('Course of Studies'),
3422                   _('Level'),
3423                   _('Score') ],]
3424        tickets = []
3425        for ticket in list(coursetickets):
3426            row = [ticket.student.matric_number,
3427                  ticket.student.reg_number,
3428                  ticket.student.display_fullname,
3429                  ticket.student.translated_state,
3430                  ticket.student.certcode,
3431                  ticket.level,
3432                  ticket.score]
3433            tickets.append(row)
3434        return header + sorted(tickets, key=lambda value: value[0]), None
3435
3436    def render(self):
3437        session = grok.getSite()['configuration'].current_academic_session
3438        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3439                     if i['local_role'] == 'waeup.local.Lecturer']
3440        lecturers =  ', '.join(lecturers)
3441        students_utils = getUtility(IStudentsUtils)
3442        return students_utils.renderPDFCourseticketsOverview(
3443            self, session, self.data(session), lecturers, 'landscape')
3444
3445class ExportJobContainerOverview(KofaPage):
3446    """Page that lists active student data export jobs and provides links
3447    to discard or download CSV files.
3448
3449    """
3450    grok.context(VirtualExportJobContainer)
3451    grok.require('waeup.showStudents')
3452    grok.name('index.html')
3453    grok.template('exportjobsindex')
3454    label = _('Student Data Exports')
3455    pnav = 1
3456    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3457
3458    def update(self, CREATE=None, DISCARD=None, job_id=None):
3459        if CREATE:
3460            self.redirect(self.url('@@exportconfig'))
3461            return
3462        if DISCARD and job_id:
3463            entry = self.context.entry_from_job_id(job_id)
3464            self.context.delete_export_entry(entry)
3465            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3466            self.context.logger.info(
3467                '%s - discarded: job_id=%s' % (ob_class, job_id))
3468            self.flash(_('Discarded export') + ' %s' % job_id)
3469        self.entries = doll_up(self, user=self.request.principal.id)
3470        return
3471
3472class ExportJobContainerJobConfig(KofaPage):
3473    """Page that configures a students export job.
3474
3475    This is a baseclass.
3476    """
3477    grok.baseclass()
3478    grok.name('exportconfig')
3479    grok.require('waeup.showStudents')
3480    grok.template('exportconfig')
3481    label = _('Configure student data export')
3482    pnav = 1
3483    redirect_target = ''
3484    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3485
3486    def _set_session_values(self):
3487        vocab_terms = academic_sessions_vocab.by_value.values()
3488        self.sessions = [(_('All Sessions'), 'all')]
3489        self.sessions += sorted(
3490            [(x.title, x.token) for x in vocab_terms], reverse=True)
3491        return
3492
3493    def _set_level_values(self):
3494        vocab_terms = course_levels.by_value.values()
3495        self.levels = [(_('All Levels'), 'all')]
3496        self.levels += sorted(
3497            [(x.title, x.token) for x in vocab_terms])
3498        return
3499
3500    def _set_mode_values(self):
3501        utils = getUtility(IKofaUtils)
3502        self.modes =[(_('All Modes'), 'all')]
3503        self.modes += sorted([(value, key) for key, value in
3504                      utils.STUDY_MODES_DICT.items()])
3505        return
3506
3507    def _set_paycat_values(self):
3508        utils = getUtility(IKofaUtils)
3509        self.paycats =[(_('All Payment Categories'), 'all')]
3510        self.paycats += sorted([(value, key) for key, value in
3511                      utils.PAYMENT_CATEGORIES.items()])
3512        return
3513
3514    def _set_exporter_values(self):
3515        # We provide all student exporters, nothing else, yet.
3516        # Bursary or Department Officers don't have the general exportData
3517        # permission and are only allowed to export bursary or payments
3518        # overview data respectively. This is the only place where
3519        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3520        # are used.
3521        exporters = []
3522        if not checkPermission('waeup.exportData', self.context):
3523            if checkPermission('waeup.exportBursaryData', self.context):
3524                exporters += [('Bursary Data', 'bursary')]
3525            if checkPermission('waeup.exportPaymentsOverview', self.context):
3526                exporters += [('School Fee Payments Overview',
3527                               'sfpaymentsoverview'),
3528                              ('Session Payments Overview',
3529                               'sessionpaymentsoverview')]
3530            self.exporters = exporters
3531            return
3532        STUDENT_EXPORTER_NAMES = getUtility(
3533            IStudentsUtils).STUDENT_EXPORTER_NAMES
3534        for name in STUDENT_EXPORTER_NAMES:
3535            util = getUtility(ICSVExporter, name=name)
3536            exporters.append((util.title, name),)
3537        self.exporters = exporters
3538        return
3539
3540    @property
3541    def faccode(self):
3542        return None
3543
3544    @property
3545    def depcode(self):
3546        return None
3547
3548    @property
3549    def certcode(self):
3550        return None
3551
3552    def update(self, START=None, session=None, level=None, mode=None,
3553               payments_start=None, payments_end=None, ct_level=None,
3554               ct_session=None, paycat=None, paysession=None, exporter=None):
3555        self._set_session_values()
3556        self._set_level_values()
3557        self._set_mode_values()
3558        self._set_paycat_values()
3559        self._set_exporter_values()
3560        if START is None:
3561            return
3562        ena = exports_not_allowed(self)
3563        if ena:
3564            self.flash(ena, type='danger')
3565            return
3566        if payments_start or payments_end:
3567            date_format = '%d/%m/%Y'
3568            try:
3569                datetime.strptime(payments_start, date_format)
3570                datetime.strptime(payments_end, date_format)
3571            except ValueError:
3572                self.flash(_('Payment dates do not match format d/m/Y.'),
3573                           type="danger")
3574                return
3575        if session == 'all':
3576            session=None
3577        if level == 'all':
3578            level = None
3579        if mode == 'all':
3580            mode = None
3581        if (mode,
3582            level,
3583            session,
3584            self.faccode,
3585            self.depcode,
3586            self.certcode) == (None, None, None, None, None, None):
3587            # Export all students including those without certificate
3588            job_id = self.context.start_export_job(exporter,
3589                                          self.request.principal.id,
3590                                          payments_start = payments_start,
3591                                          payments_end = payments_end,
3592                                          paycat=paycat,
3593                                          paysession=paysession,
3594                                          ct_level = ct_level,
3595                                          ct_session = ct_session,
3596                                          )
3597        else:
3598            job_id = self.context.start_export_job(exporter,
3599                                          self.request.principal.id,
3600                                          current_session=session,
3601                                          current_level=level,
3602                                          current_mode=mode,
3603                                          faccode=self.faccode,
3604                                          depcode=self.depcode,
3605                                          certcode=self.certcode,
3606                                          payments_start = payments_start,
3607                                          payments_end = payments_end,
3608                                          paycat=paycat,
3609                                          paysession=paysession,
3610                                          ct_level = ct_level,
3611                                          ct_session = ct_session,)
3612        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3613        self.context.logger.info(
3614            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3615            % (ob_class, exporter, session, level, mode, self.faccode,
3616            self.depcode, self.certcode, payments_start, payments_end,
3617            ct_level, ct_session, paycat, paysession, job_id))
3618        self.flash(_('Export started for students with') +
3619                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3620                   session, level, mode))
3621        self.redirect(self.url(self.redirect_target))
3622        return
3623
3624class ExportJobContainerDownload(ExportCSVView):
3625    """Page that downloads a students export csv file.
3626
3627    """
3628    grok.context(VirtualExportJobContainer)
3629    grok.require('waeup.showStudents')
3630
3631class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3632    """Page that configures a students export job in datacenter.
3633
3634    """
3635    grok.context(IDataCenter)
3636    redirect_target = '@@export'
3637
3638class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3639    """Page that configures a students export job in datacenter.
3640
3641    """
3642    grok.name('exportselected')
3643    grok.context(IDataCenter)
3644    redirect_target = '@@export'
3645    grok.template('exportselected')
3646    label = _('Configure student data export')
3647
3648    def update(self, START=None, students=None, exporter=None):
3649        self._set_exporter_values()
3650        if START is None:
3651            return
3652        ena = exports_not_allowed(self)
3653        if ena:
3654            self.flash(ena, type='danger')
3655            return
3656        try:
3657            ids = students.replace(',', ' ').split()
3658        except:
3659            self.flash(sys.exc_info()[1])
3660            self.redirect(self.url(self.redirect_target))
3661            return
3662        job_id = self.context.start_export_job(
3663            exporter, self.request.principal.id, selected=ids)
3664        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3665        self.context.logger.info(
3666            '%s - selected students exported: %s, job_id=%s' %
3667            (ob_class, exporter, job_id))
3668        self.flash(_('Export of selected students started.'))
3669        self.redirect(self.url(self.redirect_target))
3670        return
3671
3672class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3673    """Page that configures a students export job in facultiescontainer.
3674
3675    """
3676    grok.context(VirtualFacultiesExportJobContainer)
3677
3678
3679class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3680    """Page that configures a students export job in faculties.
3681
3682    """
3683    grok.context(VirtualFacultyExportJobContainer)
3684
3685    @property
3686    def faccode(self):
3687        return self.context.__parent__.code
3688
3689class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3690    """Page that configures a students export job in departments.
3691
3692    """
3693    grok.context(VirtualDepartmentExportJobContainer)
3694
3695    @property
3696    def depcode(self):
3697        return self.context.__parent__.code
3698
3699class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3700    """Page that configures a students export job for certificates.
3701
3702    """
3703    grok.context(VirtualCertificateExportJobContainer)
3704    grok.template('exportconfig_certificate')
3705
3706    @property
3707    def certcode(self):
3708        return self.context.__parent__.code
3709
3710class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3711    """Page that configures a students export job for courses.
3712
3713    In contrast to department or certificate student data exports the
3714    coursetickets_catalog is searched here. Therefore the update
3715    method from the base class is customized.
3716    """
3717    grok.context(VirtualCourseExportJobContainer)
3718    grok.template('exportconfig_course')
3719
3720    def _set_exporter_values(self):
3721        # We provide only the 'coursetickets' and 'lecturer' exporter
3722        # but can add more.
3723        exporters = []
3724        for name in ('coursetickets', 'lecturer'):
3725            util = getUtility(ICSVExporter, name=name)
3726            exporters.append((util.title, name),)
3727        self.exporters = exporters
3728
3729    def _set_session_values(self):
3730        # We allow only current academic session
3731        academic_session = grok.getSite()['configuration'].current_academic_session
3732        if not academic_session:
3733            self.sessions = []
3734            return
3735        x = academic_sessions_vocab.getTerm(academic_session)
3736        self.sessions = [(x.title, x.token)]
3737        return
3738
3739    def update(self, START=None, session=None, level=None, mode=None,
3740               exporter=None):
3741        self._set_session_values()
3742        self._set_level_values()
3743        self._set_mode_values()
3744        self._set_exporter_values()
3745        if not self.sessions:
3746            self.flash(
3747                _('Academic session not set. '
3748                  'Please contact the administrator.'),
3749                type='danger')
3750            self.redirect(self.url(self.context))
3751            return
3752        if START is None:
3753            return
3754        ena = exports_not_allowed(self)
3755        if ena:
3756            self.flash(ena, type='danger')
3757            return
3758        if session == 'all':
3759            session = None
3760        if level == 'all':
3761            level = None
3762        job_id = self.context.start_export_job(exporter,
3763                                      self.request.principal.id,
3764                                      # Use a different catalog and
3765                                      # pass different keywords than
3766                                      # for the (default) students_catalog
3767                                      catalog='coursetickets',
3768                                      session=session,
3769                                      level=level,
3770                                      code=self.context.__parent__.code)
3771        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3772        self.context.logger.info(
3773            '%s - exported: %s (%s, %s, %s), job_id=%s'
3774            % (ob_class, exporter, session, level,
3775            self.context.__parent__.code, job_id))
3776        self.flash(_('Export started for course tickets with') +
3777                   ' level_session=%s, level=%s' % (
3778                   session, level))
3779        self.redirect(self.url(self.redirect_target))
3780        return
Note: See TracBrowser for help on using the repository browser.