source: main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py @ 14587

Last change on this file since 14587 was 14584, checked in by Henrik Bettermann, 8 years ago

Replace maxCreditsExceeded by warnCreditsOOR.

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