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

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

Be more verbose.

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