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

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

Reorganise interfaces.

Transcript processing views and viewlets are now in the context of studycourses. Officers can now validate, sign and release transcripts directly on the transcript page.

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