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

Last change on this file since 10439 was 10439, checked in by Henrik Bettermann, 11 years ago

Ease customization and define tabletitle outside render method.

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