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

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

The academic year is generally divided into terms not semesters. A semester structure is a special case. The base package uses the semester system.

Rename semester to term. Use values instead of keys of the SEMESTER_DICT.

  • Property svn:keywords set to Id
File size: 113.8 KB
Line 
1## $Id: browser.py 10436 2013-07-29 15:10:02Z 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 content_title_1(self):
1263        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1264        return translate(_('1st Semester Courses'), 'waeup.kofa',
1265            target_language=portal_language)
1266
1267    @property
1268    def content_title_2(self):
1269        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1270        return translate(_('2nd Semester Courses'), 'waeup.kofa',
1271            target_language=portal_language)
1272
1273    @property
1274    def content_title_3(self):
1275        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1276        return translate(_('Level Courses'), 'waeup.kofa',
1277            target_language=portal_language)
1278
1279    @property
1280    def label(self):
1281        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1282        lang = self.request.cookies.get('kofa.language', portal_language)
1283        level_title = translate(self.context.level_title, 'waeup.kofa',
1284            target_language=lang)
1285        return translate(_('Course Registration Slip'),
1286            'waeup.kofa', target_language=portal_language) \
1287            + ' %s' % level_title
1288
1289    def render(self):
1290        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1291        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1292        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1293        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1294        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1295        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1296        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1297        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1298        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1299        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1300        studentview = StudentBasePDFFormPage(self.context.student,
1301            self.request, self.omit_fields)
1302        students_utils = getUtility(IStudentsUtils)
1303        tabledata_1 = sorted(
1304            [value for value in self.context.values() if value.semester == 1],
1305            key=lambda value: str(value.semester) + value.code)
1306        tabledata_2 = sorted(
1307            [value for value in self.context.values() if value.semester == 2],
1308            key=lambda value: str(value.semester) + value.code)
1309        tabledata_3 = sorted(
1310            [value for value in self.context.values() if value.semester == 3],
1311            key=lambda value: str(value.semester) + value.code)
1312        tableheader = [(Code,'code', 2.5),
1313                         (Title,'title', 5),
1314                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1315                         (Cred, 'credits', 1.5),
1316                         #(Mand, 'mandatory', 1.5),
1317                         (Score, 'score', 1.5),
1318                         (Grade, 'grade', 1.5),
1319                         #('Auto', 'automatic', 1.5)
1320                         ]
1321        return students_utils.renderPDF(
1322            self, 'course_registration_slip.pdf',
1323            self.context.student, studentview,
1324            tableheader_1=tableheader,
1325            tabledata_1=tabledata_1,
1326            tableheader_2=tableheader,
1327            tabledata_2=tabledata_2,
1328            tableheader_3=tableheader,
1329            tabledata_3=tabledata_3,
1330            omit_fields=self.omit_fields
1331            )
1332
1333class StudyLevelManageFormPage(KofaEditFormPage):
1334    """ Page to edit the student study level data
1335    """
1336    grok.context(IStudentStudyLevel)
1337    grok.name('manage')
1338    grok.require('waeup.manageStudent')
1339    grok.template('studylevelmanagepage')
1340    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1341        'validation_date', 'validated_by', 'total_credits')
1342    pnav = 4
1343    taboneactions = [_('Save'),_('Cancel')]
1344    tabtwoactions = [_('Add course ticket'),
1345        _('Remove selected tickets'),_('Cancel')]
1346
1347    def update(self, ADD=None, course=None):
1348        if not self.context.__parent__.is_current:
1349            emit_lock_message(self)
1350            return
1351        super(StudyLevelManageFormPage, self).update()
1352        tabs.need()
1353        self.tab1 = self.tab2 = ''
1354        qs = self.request.get('QUERY_STRING', '')
1355        if not qs:
1356            qs = 'tab1'
1357        setattr(self, qs, 'active')
1358        warning.need()
1359        datatable.need()
1360        if ADD is not None:
1361            if not course:
1362                self.flash(_('No valid course code entered.'))
1363                self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1364                return
1365            cat = queryUtility(ICatalog, name='courses_catalog')
1366            result = cat.searchResults(code=(course, course))
1367            if len(result) != 1:
1368                self.flash(_('Course not found.'))
1369            else:
1370                course = list(result)[0]
1371                addCourseTicket(self, course)
1372            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1373        return
1374
1375    @property
1376    def translated_values(self):
1377        return translated_values(self)
1378
1379    @property
1380    def label(self):
1381        # Here we know that the cookie has been set
1382        lang = self.request.cookies.get('kofa.language')
1383        level_title = translate(self.context.level_title, 'waeup.kofa',
1384            target_language=lang)
1385        return _('Manage study level ${a}',
1386            mapping = {'a':level_title})
1387
1388    @action(_('Save'), style='primary')
1389    def save(self, **data):
1390        msave(self, **data)
1391        return
1392
1393    @jsaction(_('Remove selected tickets'))
1394    def delCourseTicket(self, **data):
1395        form = self.request.form
1396        if 'val_id' in form:
1397            child_id = form['val_id']
1398        else:
1399            self.flash(_('No ticket selected.'))
1400            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1401            return
1402        if not isinstance(child_id, list):
1403            child_id = [child_id]
1404        deleted = []
1405        for id in child_id:
1406            del self.context[id]
1407            deleted.append(id)
1408        if len(deleted):
1409            self.flash(_('Successfully removed: ${a}',
1410                mapping = {'a':', '.join(deleted)}))
1411            self.context.writeLogMessage(
1412                self,'removed: %s at %s' %
1413                (', '.join(deleted), self.context.level))
1414        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1415        return
1416
1417class ValidateCoursesPage(UtilityView, grok.View):
1418    """ Validate course list by course adviser
1419    """
1420    grok.context(IStudentStudyLevel)
1421    grok.name('validate_courses')
1422    grok.require('waeup.validateStudent')
1423
1424    def update(self):
1425        if not self.context.__parent__.is_current:
1426            emit_lock_message(self)
1427            return
1428        if str(self.context.__parent__.current_level) != self.context.__name__:
1429            self.flash(_('This level does not correspond current level.'))
1430        elif self.context.student.state == REGISTERED:
1431            IWorkflowInfo(self.context.student).fireTransition(
1432                'validate_courses')
1433            self.flash(_('Course list has been validated.'))
1434        else:
1435            self.flash(_('Student is in the wrong state.'))
1436        self.redirect(self.url(self.context))
1437        return
1438
1439    def render(self):
1440        return
1441
1442class RejectCoursesPage(UtilityView, grok.View):
1443    """ Reject course list by course adviser
1444    """
1445    grok.context(IStudentStudyLevel)
1446    grok.name('reject_courses')
1447    grok.require('waeup.validateStudent')
1448
1449    def update(self):
1450        if not self.context.__parent__.is_current:
1451            emit_lock_message(self)
1452            return
1453        if str(self.context.__parent__.current_level) != self.context.__name__:
1454            self.flash(_('This level does not correspond current level.'))
1455            self.redirect(self.url(self.context))
1456            return
1457        elif self.context.student.state == VALIDATED:
1458            IWorkflowInfo(self.context.student).fireTransition('reset8')
1459            message = _('Course list request has been annulled.')
1460            self.flash(message)
1461        elif self.context.student.state == REGISTERED:
1462            IWorkflowInfo(self.context.student).fireTransition('reset7')
1463            message = _('Course list request has been rejected:')
1464            self.flash(message)
1465        else:
1466            self.flash(_('Student is in the wrong state.'))
1467            self.redirect(self.url(self.context))
1468            return
1469        args = {'subject':message}
1470        self.redirect(self.url(self.context.student) +
1471            '/contactstudent?%s' % urlencode(args))
1472        return
1473
1474    def render(self):
1475        return
1476
1477class CourseTicketAddFormPage(KofaAddFormPage):
1478    """Add a course ticket.
1479    """
1480    grok.context(IStudentStudyLevel)
1481    grok.name('add')
1482    grok.require('waeup.manageStudent')
1483    label = _('Add course ticket')
1484    form_fields = grok.AutoFields(ICourseTicketAdd)
1485    pnav = 4
1486
1487    def update(self):
1488        if not self.context.__parent__.is_current:
1489            emit_lock_message(self)
1490            return
1491        super(CourseTicketAddFormPage, self).update()
1492        return
1493
1494    @action(_('Add course ticket'))
1495    def addCourseTicket(self, **data):
1496        course = data['course']
1497        success = addCourseTicket(self, course)
1498        if success:
1499            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1500        return
1501
1502    @action(_('Cancel'), validator=NullValidator)
1503    def cancel(self, **data):
1504        self.redirect(self.url(self.context))
1505
1506class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1507    """ Page to display course tickets
1508    """
1509    grok.context(ICourseTicket)
1510    grok.name('index')
1511    grok.require('waeup.viewStudent')
1512    form_fields = grok.AutoFields(ICourseTicket)
1513    grok.template('courseticketpage')
1514    pnav = 4
1515
1516    @property
1517    def label(self):
1518        return _('${a}: Course Ticket ${b}', mapping = {
1519            'a':self.context.student.display_fullname,
1520            'b':self.context.code})
1521
1522class CourseTicketManageFormPage(KofaEditFormPage):
1523    """ Page to manage course tickets
1524    """
1525    grok.context(ICourseTicket)
1526    grok.name('manage')
1527    grok.require('waeup.manageStudent')
1528    form_fields = grok.AutoFields(ICourseTicket)
1529    form_fields['title'].for_display = True
1530    form_fields['fcode'].for_display = True
1531    form_fields['dcode'].for_display = True
1532    form_fields['semester'].for_display = True
1533    form_fields['passmark'].for_display = True
1534    form_fields['credits'].for_display = True
1535    form_fields['mandatory'].for_display = False
1536    form_fields['automatic'].for_display = True
1537    form_fields['carry_over'].for_display = True
1538    pnav = 4
1539    grok.template('courseticketmanagepage')
1540
1541    @property
1542    def label(self):
1543        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1544
1545    @action('Save', style='primary')
1546    def save(self, **data):
1547        msave(self, **data)
1548        return
1549
1550class PaymentsManageFormPage(KofaEditFormPage):
1551    """ Page to manage the student payments
1552
1553    This manage form page is for both students and students officers.
1554    """
1555    grok.context(IStudentPaymentsContainer)
1556    grok.name('index')
1557    grok.require('waeup.viewStudent')
1558    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1559    grok.template('paymentsmanagepage')
1560    pnav = 4
1561
1562    @property
1563    def manage_payments_allowed(self):
1564        return checkPermission('waeup.payStudent', self.context)
1565
1566    def unremovable(self, ticket):
1567        usertype = getattr(self.request.principal, 'user_type', None)
1568        if not usertype:
1569            return False
1570        if not self.manage_payments_allowed:
1571            return True
1572        return (self.request.principal.user_type == 'student' and ticket.r_code)
1573
1574    @property
1575    def label(self):
1576        return _('${a}: Payments',
1577            mapping = {'a':self.context.__parent__.display_fullname})
1578
1579    def update(self):
1580        super(PaymentsManageFormPage, self).update()
1581        datatable.need()
1582        warning.need()
1583        return
1584
1585    @jsaction(_('Remove selected tickets'))
1586    def delPaymentTicket(self, **data):
1587        form = self.request.form
1588        if 'val_id' in form:
1589            child_id = form['val_id']
1590        else:
1591            self.flash(_('No payment selected.'))
1592            self.redirect(self.url(self.context))
1593            return
1594        if not isinstance(child_id, list):
1595            child_id = [child_id]
1596        deleted = []
1597        for id in child_id:
1598            # Students are not allowed to remove used payment tickets
1599            ticket = self.context.get(id, None)
1600            if ticket is not None and not self.unremovable(ticket):
1601                del self.context[id]
1602                deleted.append(id)
1603        if len(deleted):
1604            self.flash(_('Successfully removed: ${a}',
1605                mapping = {'a': ', '.join(deleted)}))
1606            self.context.writeLogMessage(
1607                self,'removed: %s' % ', '.join(deleted))
1608        self.redirect(self.url(self.context))
1609        return
1610
1611    #@action(_('Add online payment ticket'))
1612    #def addPaymentTicket(self, **data):
1613    #    self.redirect(self.url(self.context, '@@addop'))
1614
1615class OnlinePaymentAddFormPage(KofaAddFormPage):
1616    """ Page to add an online payment ticket
1617    """
1618    grok.context(IStudentPaymentsContainer)
1619    grok.name('addop')
1620    grok.template('onlinepaymentaddform')
1621    grok.require('waeup.payStudent')
1622    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1623        'p_category')
1624    label = _('Add online payment')
1625    pnav = 4
1626
1627    @property
1628    def selectable_categories(self):
1629        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1630        return sorted(categories.items())
1631
1632    @action(_('Create ticket'), style='primary')
1633    def createTicket(self, **data):
1634        p_category = data['p_category']
1635        previous_session = data.get('p_session', None)
1636        previous_level = data.get('p_level', None)
1637        student = self.context.__parent__
1638        if p_category == 'bed_allocation' and student[
1639            'studycourse'].current_session != grok.getSite()[
1640            'hostels'].accommodation_session:
1641                self.flash(
1642                    _('Your current session does not match ' + \
1643                    'accommodation session.'))
1644                return
1645        if 'maintenance' in p_category:
1646            current_session = str(student['studycourse'].current_session)
1647            if not current_session in student['accommodation']:
1648                self.flash(_('You have not yet booked accommodation.'))
1649                return
1650        students_utils = getUtility(IStudentsUtils)
1651        error, payment = students_utils.setPaymentDetails(
1652            p_category, student, previous_session, previous_level)
1653        if error is not None:
1654            self.flash(error)
1655            return
1656        self.context[payment.p_id] = payment
1657        self.flash(_('Payment ticket created.'))
1658        self.redirect(self.url(self.context))
1659        return
1660
1661    @action(_('Cancel'), validator=NullValidator)
1662    def cancel(self, **data):
1663        self.redirect(self.url(self.context))
1664
1665class PreviousPaymentAddFormPage(KofaAddFormPage):
1666    """ Page to add an online payment ticket for previous sessions
1667    """
1668    grok.context(IStudentPaymentsContainer)
1669    grok.name('addpp')
1670    grok.require('waeup.payStudent')
1671    form_fields = grok.AutoFields(IStudentPreviousPayment)
1672    label = _('Add previous session online payment')
1673    pnav = 4
1674
1675    def update(self):
1676        if self.context.student.before_payment:
1677            self.flash(_("No previous payment to be made."))
1678            self.redirect(self.url(self.context))
1679        super(PreviousPaymentAddFormPage, self).update()
1680        return
1681
1682    @action(_('Create ticket'), style='primary')
1683    def createTicket(self, **data):
1684        p_category = data['p_category']
1685        previous_session = data.get('p_session', None)
1686        previous_level = data.get('p_level', None)
1687        student = self.context.__parent__
1688        students_utils = getUtility(IStudentsUtils)
1689        error, payment = students_utils.setPaymentDetails(
1690            p_category, student, previous_session, previous_level)
1691        if error is not None:
1692            self.flash(error)
1693            return
1694        self.context[payment.p_id] = payment
1695        self.flash(_('Payment ticket created.'))
1696        self.redirect(self.url(self.context))
1697        return
1698
1699    @action(_('Cancel'), validator=NullValidator)
1700    def cancel(self, **data):
1701        self.redirect(self.url(self.context))
1702
1703class BalancePaymentAddFormPage(KofaAddFormPage):
1704    """ Page to add an online payment ticket for balance sessions
1705    """
1706    grok.context(IStudentPaymentsContainer)
1707    grok.name('addbp')
1708    grok.require('waeup.manageStudent')
1709    form_fields = grok.AutoFields(IStudentBalancePayment)
1710    label = _('Add balance')
1711    pnav = 4
1712
1713    @action(_('Create ticket'), style='primary')
1714    def createTicket(self, **data):
1715        p_category = data['p_category']
1716        balance_session = data.get('balance_session', None)
1717        balance_level = data.get('balance_level', None)
1718        balance_amount = data.get('balance_amount', None)
1719        student = self.context.__parent__
1720        students_utils = getUtility(IStudentsUtils)
1721        error, payment = students_utils.setBalanceDetails(
1722            p_category, student, balance_session,
1723            balance_level, balance_amount)
1724        if error is not None:
1725            self.flash(error)
1726            return
1727        self.context[payment.p_id] = payment
1728        self.flash(_('Payment ticket created.'))
1729        self.redirect(self.url(self.context))
1730        return
1731
1732    @action(_('Cancel'), validator=NullValidator)
1733    def cancel(self, **data):
1734        self.redirect(self.url(self.context))
1735
1736class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1737    """ Page to view an online payment ticket
1738    """
1739    grok.context(IStudentOnlinePayment)
1740    grok.name('index')
1741    grok.require('waeup.viewStudent')
1742    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1743    form_fields[
1744        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1745    form_fields[
1746        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1747    pnav = 4
1748
1749    @property
1750    def label(self):
1751        return _('${a}: Online Payment Ticket ${b}', mapping = {
1752            'a':self.context.student.display_fullname,
1753            'b':self.context.p_id})
1754
1755class OnlinePaymentApprovePage(UtilityView, grok.View):
1756    """ Callback view
1757    """
1758    grok.context(IStudentOnlinePayment)
1759    grok.name('approve')
1760    grok.require('waeup.managePortal')
1761
1762    def update(self):
1763        success, msg, log = self.context.approveStudentPayment()
1764        if log is not None:
1765            # Add log message to students.log
1766            self.context.writeLogMessage(self,log)
1767            # Add log message to payments.log
1768            self.context.logger.info(
1769                '%s,%s,%s,%s,%s,,,,,,' % (
1770                self.context.student.student_id,
1771                self.context.p_id, self.context.p_category,
1772                self.context.amount_auth, self.context.r_code))
1773        self.flash(msg)
1774        return
1775
1776    def render(self):
1777        self.redirect(self.url(self.context, '@@index'))
1778        return
1779
1780class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1781    """ Approval view for students.
1782
1783    This view is used for browser tests only and
1784    must be neutralized in custom pages!
1785    """
1786
1787    grok.name('fake_approve')
1788    grok.require('waeup.payStudent')
1789
1790class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1791    """Deliver a PDF slip of the context.
1792    """
1793    grok.context(IStudentOnlinePayment)
1794    grok.name('payment_slip.pdf')
1795    grok.require('waeup.viewStudent')
1796    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1797    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1798    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1799    prefix = 'form'
1800    note = None
1801    omit_fields = (
1802        'password', 'suspended', 'phone', 'date_of_birth',
1803        'adm_code', 'sex', 'suspended_comment')
1804
1805    @property
1806    def title(self):
1807        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1808        return translate(_('Payment Data'), 'waeup.kofa',
1809            target_language=portal_language)
1810
1811    @property
1812    def label(self):
1813        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1814        return translate(_('Online Payment Slip'),
1815            'waeup.kofa', target_language=portal_language) \
1816            + ' %s' % self.context.p_id
1817
1818    def render(self):
1819        #if self.context.p_state != 'paid':
1820        #    self.flash('Ticket not yet paid.')
1821        #    self.redirect(self.url(self.context))
1822        #    return
1823        studentview = StudentBasePDFFormPage(self.context.student,
1824            self.request, self.omit_fields)
1825        students_utils = getUtility(IStudentsUtils)
1826        return students_utils.renderPDF(self, 'payment_slip.pdf',
1827            self.context.student, studentview, note=self.note,
1828            omit_fields=self.omit_fields)
1829
1830
1831class AccommodationManageFormPage(KofaEditFormPage):
1832    """ Page to manage bed tickets.
1833
1834    This manage form page is for both students and students officers.
1835    """
1836    grok.context(IStudentAccommodation)
1837    grok.name('index')
1838    grok.require('waeup.handleAccommodation')
1839    form_fields = grok.AutoFields(IStudentAccommodation)
1840    grok.template('accommodationmanagepage')
1841    pnav = 4
1842    officers_only_actions = [_('Remove selected')]
1843
1844    @property
1845    def label(self):
1846        return _('${a}: Accommodation',
1847            mapping = {'a':self.context.__parent__.display_fullname})
1848
1849    def update(self):
1850        super(AccommodationManageFormPage, self).update()
1851        datatable.need()
1852        warning.need()
1853        return
1854
1855    @jsaction(_('Remove selected'))
1856    def delBedTickets(self, **data):
1857        if getattr(self.request.principal, 'user_type', None) == 'student':
1858            self.flash(_('You are not allowed to remove bed tickets.'))
1859            self.redirect(self.url(self.context))
1860            return
1861        form = self.request.form
1862        if 'val_id' in form:
1863            child_id = form['val_id']
1864        else:
1865            self.flash(_('No bed ticket selected.'))
1866            self.redirect(self.url(self.context))
1867            return
1868        if not isinstance(child_id, list):
1869            child_id = [child_id]
1870        deleted = []
1871        for id in child_id:
1872            del self.context[id]
1873            deleted.append(id)
1874        if len(deleted):
1875            self.flash(_('Successfully removed: ${a}',
1876                mapping = {'a':', '.join(deleted)}))
1877            self.context.writeLogMessage(
1878                self,'removed: % s' % ', '.join(deleted))
1879        self.redirect(self.url(self.context))
1880        return
1881
1882    @property
1883    def selected_actions(self):
1884        if getattr(self.request.principal, 'user_type', None) == 'student':
1885            return [action for action in self.actions
1886                    if not action.label in self.officers_only_actions]
1887        return self.actions
1888
1889class BedTicketAddPage(KofaPage):
1890    """ Page to add an online payment ticket
1891    """
1892    grok.context(IStudentAccommodation)
1893    grok.name('add')
1894    grok.require('waeup.handleAccommodation')
1895    grok.template('enterpin')
1896    ac_prefix = 'HOS'
1897    label = _('Add bed ticket')
1898    pnav = 4
1899    buttonname = _('Create bed ticket')
1900    notice = ''
1901    with_ac = True
1902
1903    def update(self, SUBMIT=None):
1904        student = self.context.student
1905        students_utils = getUtility(IStudentsUtils)
1906        acc_details  = students_utils.getAccommodationDetails(student)
1907        if acc_details.get('expired', False):
1908            startdate = acc_details.get('startdate')
1909            enddate = acc_details.get('enddate')
1910            if startdate and enddate:
1911                tz = getUtility(IKofaUtils).tzinfo
1912                startdate = to_timezone(
1913                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1914                enddate = to_timezone(
1915                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1916                self.flash(_("Outside booking period: ${a} - ${b}",
1917                    mapping = {'a': startdate, 'b': enddate}))
1918            else:
1919                self.flash(_("Outside booking period."))
1920            self.redirect(self.url(self.context))
1921            return
1922        if not acc_details:
1923            self.flash(_("Your data are incomplete."))
1924            self.redirect(self.url(self.context))
1925            return
1926        if not student.state in acc_details['allowed_states']:
1927            self.flash(_("You are in the wrong registration state."))
1928            self.redirect(self.url(self.context))
1929            return
1930        if student['studycourse'].current_session != acc_details[
1931            'booking_session']:
1932            self.flash(
1933                _('Your current session does not match accommodation session.'))
1934            self.redirect(self.url(self.context))
1935            return
1936        if str(acc_details['booking_session']) in self.context.keys():
1937            self.flash(
1938                _('You already booked a bed space in current ' \
1939                    + 'accommodation session.'))
1940            self.redirect(self.url(self.context))
1941            return
1942        if self.with_ac:
1943            self.ac_series = self.request.form.get('ac_series', None)
1944            self.ac_number = self.request.form.get('ac_number', None)
1945        if SUBMIT is None:
1946            return
1947        if self.with_ac:
1948            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1949            code = get_access_code(pin)
1950            if not code:
1951                self.flash(_('Activation code is invalid.'))
1952                return
1953        # Search and book bed
1954        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1955        entries = cat.searchResults(
1956            owner=(student.student_id,student.student_id))
1957        if len(entries):
1958            # If bed space has been manually allocated use this bed
1959            bed = [entry for entry in entries][0]
1960            # Safety belt for paranoids: Does this bed really exist on portal?
1961            # XXX: Can be remove if nobody complains.
1962            if bed.__parent__.__parent__ is None:
1963                self.flash(_('System error: Please contact the adminsitrator.'))
1964                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1965                return
1966        else:
1967            # else search for other available beds
1968            entries = cat.searchResults(
1969                bed_type=(acc_details['bt'],acc_details['bt']))
1970            available_beds = [
1971                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1972            if available_beds:
1973                students_utils = getUtility(IStudentsUtils)
1974                bed = students_utils.selectBed(available_beds)
1975                # Safety belt for paranoids: Does this bed really exist in portal?
1976                # XXX: Can be remove if nobody complains.
1977                if bed.__parent__.__parent__ is None:
1978                    self.flash(_('System error: Please contact the adminsitrator.'))
1979                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1980                    return
1981                bed.bookBed(student.student_id)
1982            else:
1983                self.flash(_('There is no free bed in your category ${a}.',
1984                    mapping = {'a':acc_details['bt']}))
1985                return
1986        if self.with_ac:
1987            # Mark pin as used (this also fires a pin related transition)
1988            if code.state == USED:
1989                self.flash(_('Activation code has already been used.'))
1990                return
1991            else:
1992                comment = _(u'invalidated')
1993                # Here we know that the ac is in state initialized so we do not
1994                # expect an exception, but the owner might be different
1995                if not invalidate_accesscode(
1996                    pin,comment,self.context.student.student_id):
1997                    self.flash(_('You are not the owner of this access code.'))
1998                    return
1999        # Create bed ticket
2000        bedticket = createObject(u'waeup.BedTicket')
2001        if self.with_ac:
2002            bedticket.booking_code = pin
2003        bedticket.booking_session = acc_details['booking_session']
2004        bedticket.bed_type = acc_details['bt']
2005        bedticket.bed = bed
2006        hall_title = bed.__parent__.hostel_name
2007        coordinates = bed.coordinates[1:]
2008        block, room_nr, bed_nr = coordinates
2009        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2010            'a':hall_title, 'b':block,
2011            'c':room_nr, 'd':bed_nr,
2012            'e':bed.bed_type})
2013        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2014        bedticket.bed_coordinates = translate(
2015            bc, 'waeup.kofa',target_language=portal_language)
2016        self.context.addBedTicket(bedticket)
2017        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2018        self.flash(_('Bed ticket created and bed booked: ${a}',
2019            mapping = {'a':bedticket.display_coordinates}))
2020        self.redirect(self.url(self.context))
2021        return
2022
2023class BedTicketDisplayFormPage(KofaDisplayFormPage):
2024    """ Page to display bed tickets
2025    """
2026    grok.context(IBedTicket)
2027    grok.name('index')
2028    grok.require('waeup.handleAccommodation')
2029    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2030    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2031    pnav = 4
2032
2033    @property
2034    def label(self):
2035        return _('Bed Ticket for Session ${a}',
2036            mapping = {'a':self.context.getSessionString()})
2037
2038class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
2039    """Deliver a PDF slip of the context.
2040    """
2041    grok.context(IBedTicket)
2042    grok.name('bed_allocation_slip.pdf')
2043    grok.require('waeup.handleAccommodation')
2044    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2045    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2046    prefix = 'form'
2047    omit_fields = (
2048        'password', 'suspended', 'phone', 'adm_code',
2049        'suspended_comment', 'date_of_birth')
2050
2051    @property
2052    def title(self):
2053        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2054        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2055            target_language=portal_language)
2056
2057    @property
2058    def label(self):
2059        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2060        #return translate(_('Bed Allocation: '),
2061        #    'waeup.kofa', target_language=portal_language) \
2062        #    + ' %s' % self.context.bed_coordinates
2063        return translate(_('Bed Allocation Slip'),
2064            'waeup.kofa', target_language=portal_language) \
2065            + ' %s' % self.context.getSessionString()
2066
2067    def render(self):
2068        studentview = StudentBasePDFFormPage(self.context.student,
2069            self.request, self.omit_fields)
2070        students_utils = getUtility(IStudentsUtils)
2071        return students_utils.renderPDF(
2072            self, 'bed_allocation_slip.pdf',
2073            self.context.student, studentview,
2074            omit_fields=self.omit_fields)
2075
2076class BedTicketRelocationPage(UtilityView, grok.View):
2077    """ Callback view
2078    """
2079    grok.context(IBedTicket)
2080    grok.name('relocate')
2081    grok.require('waeup.manageHostels')
2082
2083    # Relocate student if student parameters have changed or the bed_type
2084    # of the bed has changed
2085    def update(self):
2086        student = self.context.student
2087        students_utils = getUtility(IStudentsUtils)
2088        acc_details  = students_utils.getAccommodationDetails(student)
2089        if self.context.bed != None and \
2090              'reserved' in self.context.bed.bed_type:
2091            self.flash(_("Students in reserved beds can't be relocated."))
2092            self.redirect(self.url(self.context))
2093            return
2094        if acc_details['bt'] == self.context.bed_type and \
2095                self.context.bed != None and \
2096                self.context.bed.bed_type == self.context.bed_type:
2097            self.flash(_("Student can't be relocated."))
2098            self.redirect(self.url(self.context))
2099            return
2100        # Search a bed
2101        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2102        entries = cat.searchResults(
2103            owner=(student.student_id,student.student_id))
2104        if len(entries) and self.context.bed == None:
2105            # If booking has been cancelled but other bed space has been
2106            # manually allocated after cancellation use this bed
2107            new_bed = [entry for entry in entries][0]
2108        else:
2109            # Search for other available beds
2110            entries = cat.searchResults(
2111                bed_type=(acc_details['bt'],acc_details['bt']))
2112            available_beds = [
2113                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2114            if available_beds:
2115                students_utils = getUtility(IStudentsUtils)
2116                new_bed = students_utils.selectBed(available_beds)
2117                new_bed.bookBed(student.student_id)
2118            else:
2119                self.flash(_('There is no free bed in your category ${a}.',
2120                    mapping = {'a':acc_details['bt']}))
2121                self.redirect(self.url(self.context))
2122                return
2123        # Release old bed if exists
2124        if self.context.bed != None:
2125            self.context.bed.owner = NOT_OCCUPIED
2126            notify(grok.ObjectModifiedEvent(self.context.bed))
2127        # Alocate new bed
2128        self.context.bed_type = acc_details['bt']
2129        self.context.bed = new_bed
2130        hall_title = new_bed.__parent__.hostel_name
2131        coordinates = new_bed.coordinates[1:]
2132        block, room_nr, bed_nr = coordinates
2133        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2134            'a':hall_title, 'b':block,
2135            'c':room_nr, 'd':bed_nr,
2136            'e':new_bed.bed_type})
2137        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2138        self.context.bed_coordinates = translate(
2139            bc, 'waeup.kofa',target_language=portal_language)
2140        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
2141        self.flash(_('Student relocated: ${a}',
2142            mapping = {'a':self.context.display_coordinates}))
2143        self.redirect(self.url(self.context))
2144        return
2145
2146    def render(self):
2147        return
2148
2149class StudentHistoryPage(KofaPage):
2150    """ Page to display student clearance data
2151    """
2152    grok.context(IStudent)
2153    grok.name('history')
2154    grok.require('waeup.viewStudent')
2155    grok.template('studenthistory')
2156    pnav = 4
2157
2158    @property
2159    def label(self):
2160        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2161
2162# Pages for students only
2163
2164class StudentBaseEditFormPage(KofaEditFormPage):
2165    """ View to edit student base data
2166    """
2167    grok.context(IStudent)
2168    grok.name('edit_base')
2169    grok.require('waeup.handleStudent')
2170    form_fields = grok.AutoFields(IStudentBase).select(
2171        'email', 'phone')
2172    label = _('Edit base data')
2173    pnav = 4
2174
2175    @action(_('Save'), style='primary')
2176    def save(self, **data):
2177        msave(self, **data)
2178        return
2179
2180class StudentChangePasswordPage(KofaEditFormPage):
2181    """ View to manage student base data
2182    """
2183    grok.context(IStudent)
2184    grok.name('change_password')
2185    grok.require('waeup.handleStudent')
2186    grok.template('change_password')
2187    label = _('Change password')
2188    pnav = 4
2189
2190    @action(_('Save'), style='primary')
2191    def save(self, **data):
2192        form = self.request.form
2193        password = form.get('change_password', None)
2194        password_ctl = form.get('change_password_repeat', None)
2195        if password:
2196            validator = getUtility(IPasswordValidator)
2197            errors = validator.validate_password(password, password_ctl)
2198            if not errors:
2199                IUserAccount(self.context).setPassword(password)
2200                self.context.writeLogMessage(self, 'saved: password')
2201                self.flash(_('Password changed.'))
2202            else:
2203                self.flash( ' '.join(errors))
2204        return
2205
2206class StudentFilesUploadPage(KofaPage):
2207    """ View to upload files by student
2208    """
2209    grok.context(IStudent)
2210    grok.name('change_portrait')
2211    grok.require('waeup.uploadStudentFile')
2212    grok.template('filesuploadpage')
2213    label = _('Upload portrait')
2214    pnav = 4
2215
2216    def update(self):
2217        if self.context.student.state != ADMITTED:
2218            emit_lock_message(self)
2219            return
2220        super(StudentFilesUploadPage, self).update()
2221        return
2222
2223class StartClearancePage(KofaPage):
2224    grok.context(IStudent)
2225    grok.name('start_clearance')
2226    grok.require('waeup.handleStudent')
2227    grok.template('enterpin')
2228    label = _('Start clearance')
2229    ac_prefix = 'CLR'
2230    notice = ''
2231    pnav = 4
2232    buttonname = _('Start clearance now')
2233    with_ac = True
2234
2235    @property
2236    def all_required_fields_filled(self):
2237        if self.context.email and self.context.phone:
2238            return True
2239        return False
2240
2241    @property
2242    def portrait_uploaded(self):
2243        store = getUtility(IExtFileStore)
2244        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2245            return True
2246        return False
2247
2248    def update(self, SUBMIT=None):
2249        if not self.context.state == ADMITTED:
2250            self.flash(_("Wrong state"))
2251            self.redirect(self.url(self.context))
2252            return
2253        if not self.portrait_uploaded:
2254            self.flash(_("No portrait uploaded."))
2255            self.redirect(self.url(self.context, 'change_portrait'))
2256            return
2257        if not self.all_required_fields_filled:
2258            self.flash(_("Not all required fields filled."))
2259            self.redirect(self.url(self.context, 'edit_base'))
2260            return
2261        if self.with_ac:
2262            self.ac_series = self.request.form.get('ac_series', None)
2263            self.ac_number = self.request.form.get('ac_number', None)
2264        if SUBMIT is None:
2265            return
2266        if self.with_ac:
2267            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2268            code = get_access_code(pin)
2269            if not code:
2270                self.flash(_('Activation code is invalid.'))
2271                return
2272            if code.state == USED:
2273                self.flash(_('Activation code has already been used.'))
2274                return
2275            # Mark pin as used (this also fires a pin related transition)
2276            # and fire transition start_clearance
2277            comment = _(u"invalidated")
2278            # Here we know that the ac is in state initialized so we do not
2279            # expect an exception, but the owner might be different
2280            if not invalidate_accesscode(pin, comment, self.context.student_id):
2281                self.flash(_('You are not the owner of this access code.'))
2282                return
2283            self.context.clr_code = pin
2284        IWorkflowInfo(self.context).fireTransition('start_clearance')
2285        self.flash(_('Clearance process has been started.'))
2286        self.redirect(self.url(self.context,'cedit'))
2287        return
2288
2289class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2290    """ View to edit student clearance data by student
2291    """
2292    grok.context(IStudent)
2293    grok.name('cedit')
2294    grok.require('waeup.handleStudent')
2295    label = _('Edit clearance data')
2296
2297    @property
2298    def form_fields(self):
2299        if self.context.is_postgrad:
2300            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2301                'clearance_locked', 'clr_code', 'officer_comment')
2302        else:
2303            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2304                'clearance_locked', 'clr_code', 'officer_comment')
2305        return form_fields
2306
2307    def update(self):
2308        if self.context.clearance_locked:
2309            emit_lock_message(self)
2310            return
2311        return super(StudentClearanceEditFormPage, self).update()
2312
2313    @action(_('Save'), style='primary')
2314    def save(self, **data):
2315        self.applyData(self.context, **data)
2316        self.flash(_('Clearance form has been saved.'))
2317        return
2318
2319    def dataNotComplete(self):
2320        """To be implemented in the customization package.
2321        """
2322        return False
2323
2324    @action(_('Save and request clearance'), style='primary')
2325    def requestClearance(self, **data):
2326        self.applyData(self.context, **data)
2327        if self.dataNotComplete():
2328            self.flash(self.dataNotComplete())
2329            return
2330        self.flash(_('Clearance form has been saved.'))
2331        if self.context.clr_code:
2332            self.redirect(self.url(self.context, 'request_clearance'))
2333        else:
2334            # We bypass the request_clearance page if student
2335            # has been imported in state 'clearance started' and
2336            # no clr_code was entered before.
2337            state = IWorkflowState(self.context).getState()
2338            if state != CLEARANCE:
2339                # This shouldn't happen, but the application officer
2340                # might have forgotten to lock the form after changing the state
2341                self.flash(_('This form cannot be submitted. Wrong state!'))
2342                return
2343            IWorkflowInfo(self.context).fireTransition('request_clearance')
2344            self.flash(_('Clearance has been requested.'))
2345            self.redirect(self.url(self.context))
2346        return
2347
2348class RequestClearancePage(KofaPage):
2349    grok.context(IStudent)
2350    grok.name('request_clearance')
2351    grok.require('waeup.handleStudent')
2352    grok.template('enterpin')
2353    label = _('Request clearance')
2354    notice = _('Enter the CLR access code used for starting clearance.')
2355    ac_prefix = 'CLR'
2356    pnav = 4
2357    buttonname = _('Request clearance now')
2358    with_ac = True
2359
2360    def update(self, SUBMIT=None):
2361        if self.with_ac:
2362            self.ac_series = self.request.form.get('ac_series', None)
2363            self.ac_number = self.request.form.get('ac_number', None)
2364        if SUBMIT is None:
2365            return
2366        if self.with_ac:
2367            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2368            if self.context.clr_code and self.context.clr_code != pin:
2369                self.flash(_("This isn't your CLR access code."))
2370                return
2371        state = IWorkflowState(self.context).getState()
2372        if state != CLEARANCE:
2373            # This shouldn't happen, but the application officer
2374            # might have forgotten to lock the form after changing the state
2375            self.flash(_('This form cannot be submitted. Wrong state!'))
2376            return
2377        IWorkflowInfo(self.context).fireTransition('request_clearance')
2378        self.flash(_('Clearance has been requested.'))
2379        self.redirect(self.url(self.context))
2380        return
2381
2382class StartSessionPage(KofaPage):
2383    grok.context(IStudentStudyCourse)
2384    grok.name('start_session')
2385    grok.require('waeup.handleStudent')
2386    grok.template('enterpin')
2387    label = _('Start session')
2388    ac_prefix = 'SFE'
2389    notice = ''
2390    pnav = 4
2391    buttonname = _('Start now')
2392    with_ac = True
2393
2394    def update(self, SUBMIT=None):
2395        if not self.context.is_current:
2396            emit_lock_message(self)
2397            return
2398        super(StartSessionPage, self).update()
2399        if not self.context.next_session_allowed:
2400            self.flash(_("You are not entitled to start session."))
2401            self.redirect(self.url(self.context))
2402            return
2403        if self.with_ac:
2404            self.ac_series = self.request.form.get('ac_series', None)
2405            self.ac_number = self.request.form.get('ac_number', None)
2406        if SUBMIT is None:
2407            return
2408        if self.with_ac:
2409            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2410            code = get_access_code(pin)
2411            if not code:
2412                self.flash(_('Activation code is invalid.'))
2413                return
2414            # Mark pin as used (this also fires a pin related transition)
2415            if code.state == USED:
2416                self.flash(_('Activation code has already been used.'))
2417                return
2418            else:
2419                comment = _(u"invalidated")
2420                # Here we know that the ac is in state initialized so we do not
2421                # expect an error, but the owner might be different
2422                if not invalidate_accesscode(
2423                    pin,comment,self.context.student.student_id):
2424                    self.flash(_('You are not the owner of this access code.'))
2425                    return
2426        try:
2427            if self.context.student.state == CLEARED:
2428                IWorkflowInfo(self.context.student).fireTransition(
2429                    'pay_first_school_fee')
2430            elif self.context.student.state == RETURNING:
2431                IWorkflowInfo(self.context.student).fireTransition(
2432                    'pay_school_fee')
2433            elif self.context.student.state == PAID:
2434                IWorkflowInfo(self.context.student).fireTransition(
2435                    'pay_pg_fee')
2436        except ConstraintNotSatisfied:
2437            self.flash(_('An error occurred, please contact the system administrator.'))
2438            return
2439        self.flash(_('Session started.'))
2440        self.redirect(self.url(self.context))
2441        return
2442
2443class AddStudyLevelFormPage(KofaEditFormPage):
2444    """ Page for students to add current study levels
2445    """
2446    grok.context(IStudentStudyCourse)
2447    grok.name('add')
2448    grok.require('waeup.handleStudent')
2449    grok.template('studyleveladdpage')
2450    form_fields = grok.AutoFields(IStudentStudyCourse)
2451    pnav = 4
2452
2453    @property
2454    def label(self):
2455        studylevelsource = StudyLevelSource().factory
2456        code = self.context.current_level
2457        title = studylevelsource.getTitle(self.context, code)
2458        return _('Add current level ${a}', mapping = {'a':title})
2459
2460    def update(self):
2461        if not self.context.is_current:
2462            emit_lock_message(self)
2463            return
2464        if self.context.student.state != PAID:
2465            emit_lock_message(self)
2466            return
2467        super(AddStudyLevelFormPage, self).update()
2468        return
2469
2470    @action(_('Create course list now'), style='primary')
2471    def addStudyLevel(self, **data):
2472        studylevel = createObject(u'waeup.StudentStudyLevel')
2473        studylevel.level = self.context.current_level
2474        studylevel.level_session = self.context.current_session
2475        try:
2476            self.context.addStudentStudyLevel(
2477                self.context.certificate,studylevel)
2478        except KeyError:
2479            self.flash(_('This level exists.'))
2480        except RequiredMissing:
2481            self.flash(_('Your data are incomplete'))
2482        self.redirect(self.url(self.context))
2483        return
2484
2485class StudyLevelEditFormPage(KofaEditFormPage):
2486    """ Page to edit the student study level data by students
2487    """
2488    grok.context(IStudentStudyLevel)
2489    grok.name('edit')
2490    grok.require('waeup.editStudyLevel')
2491    grok.template('studyleveleditpage')
2492    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2493        'level_session', 'level_verdict')
2494    pnav = 4
2495
2496    def update(self, ADD=None, course=None):
2497        if not self.context.__parent__.is_current:
2498            emit_lock_message(self)
2499            return
2500        if self.context.student.state != PAID or \
2501            not self.context.is_current_level:
2502            emit_lock_message(self)
2503            return
2504        super(StudyLevelEditFormPage, self).update()
2505        datatable.need()
2506        warning.need()
2507        if ADD is not None:
2508            if not course:
2509                self.flash(_('No valid course code entered.'))
2510                return
2511            cat = queryUtility(ICatalog, name='courses_catalog')
2512            result = cat.searchResults(code=(course, course))
2513            if len(result) != 1:
2514                self.flash(_('Course not found.'))
2515                return
2516            course = list(result)[0]
2517            addCourseTicket(self, course)
2518        return
2519
2520    @property
2521    def label(self):
2522        # Here we know that the cookie has been set
2523        lang = self.request.cookies.get('kofa.language')
2524        level_title = translate(self.context.level_title, 'waeup.kofa',
2525            target_language=lang)
2526        return _('Edit course list of ${a}',
2527            mapping = {'a':level_title})
2528
2529    @property
2530    def translated_values(self):
2531        return translated_values(self)
2532
2533    def _delCourseTicket(self, **data):
2534        form = self.request.form
2535        if 'val_id' in form:
2536            child_id = form['val_id']
2537        else:
2538            self.flash(_('No ticket selected.'))
2539            self.redirect(self.url(self.context, '@@edit'))
2540            return
2541        if not isinstance(child_id, list):
2542            child_id = [child_id]
2543        deleted = []
2544        for id in child_id:
2545            # Students are not allowed to remove core tickets
2546            if id in self.context and \
2547                self.context[id].removable_by_student:
2548                del self.context[id]
2549                deleted.append(id)
2550        if len(deleted):
2551            self.flash(_('Successfully removed: ${a}',
2552                mapping = {'a':', '.join(deleted)}))
2553            self.context.writeLogMessage(
2554                self,'removed: %s at %s' %
2555                (', '.join(deleted), self.context.level))
2556        self.redirect(self.url(self.context, u'@@edit'))
2557        return
2558
2559    @jsaction(_('Remove selected tickets'))
2560    def delCourseTicket(self, **data):
2561        self._delCourseTicket(**data)
2562        return
2563
2564    def _registerCourses(self, **data):
2565        if self.context.student.is_postgrad and \
2566            not self.context.student.is_special_postgrad:
2567            self.flash(_(
2568                "You are a postgraduate student, "
2569                "your course list can't bee registered."))
2570            self.redirect(self.url(self.context))
2571            return
2572        students_utils = getUtility(IStudentsUtils)
2573        max_credits = students_utils.maxCredits(self.context)
2574        if self.context.total_credits > max_credits:
2575            self.flash(_('Maximum credits of ${a} exceeded.',
2576                mapping = {'a':max_credits}))
2577            return
2578        IWorkflowInfo(self.context.student).fireTransition(
2579            'register_courses')
2580        self.flash(_('Course list has been registered.'))
2581        self.redirect(self.url(self.context))
2582        return
2583
2584    @action(_('Register course list'))
2585    def registerCourses(self, **data):
2586        self._registerCourses(**data)
2587        return
2588
2589class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2590    """Add a course ticket by student.
2591    """
2592    grok.name('ctadd')
2593    grok.require('waeup.handleStudent')
2594    form_fields = grok.AutoFields(ICourseTicketAdd)
2595
2596    def update(self):
2597        if self.context.student.state != PAID or \
2598            not self.context.is_current_level:
2599            emit_lock_message(self)
2600            return
2601        super(CourseTicketAddFormPage2, self).update()
2602        return
2603
2604    @action(_('Add course ticket'))
2605    def addCourseTicket(self, **data):
2606        # Safety belt
2607        if self.context.student.state != PAID:
2608            return
2609        course = data['course']
2610        success = addCourseTicket(self, course)
2611        if success:
2612            self.redirect(self.url(self.context, u'@@edit'))
2613        return
2614
2615class SetPasswordPage(KofaPage):
2616    grok.context(IKofaObject)
2617    grok.name('setpassword')
2618    grok.require('waeup.Anonymous')
2619    grok.template('setpassword')
2620    label = _('Set password for first-time login')
2621    ac_prefix = 'PWD'
2622    pnav = 0
2623    set_button = _('Set')
2624
2625    def update(self, SUBMIT=None):
2626        self.reg_number = self.request.form.get('reg_number', None)
2627        self.ac_series = self.request.form.get('ac_series', None)
2628        self.ac_number = self.request.form.get('ac_number', None)
2629
2630        if SUBMIT is None:
2631            return
2632        hitlist = search(query=self.reg_number,
2633            searchtype='reg_number', view=self)
2634        if not hitlist:
2635            self.flash(_('No student found.'))
2636            return
2637        if len(hitlist) != 1:   # Cannot happen but anyway
2638            self.flash(_('More than one student found.'))
2639            return
2640        student = hitlist[0].context
2641        self.student_id = student.student_id
2642        student_pw = student.password
2643        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2644        code = get_access_code(pin)
2645        if not code:
2646            self.flash(_('Access code is invalid.'))
2647            return
2648        if student_pw and pin == student.adm_code:
2649            self.flash(_(
2650                'Password has already been set. Your Student Id is ${a}',
2651                mapping = {'a':self.student_id}))
2652            return
2653        elif student_pw:
2654            self.flash(
2655                _('Password has already been set. You are using the ' +
2656                'wrong Access Code.'))
2657            return
2658        # Mark pin as used (this also fires a pin related transition)
2659        # and set student password
2660        if code.state == USED:
2661            self.flash(_('Access code has already been used.'))
2662            return
2663        else:
2664            comment = _(u"invalidated")
2665            # Here we know that the ac is in state initialized so we do not
2666            # expect an exception
2667            invalidate_accesscode(pin,comment)
2668            IUserAccount(student).setPassword(self.ac_number)
2669            student.adm_code = pin
2670        self.flash(_('Password has been set. Your Student Id is ${a}',
2671            mapping = {'a':self.student_id}))
2672        return
2673
2674class StudentRequestPasswordPage(KofaAddFormPage):
2675    """Captcha'd registration page for applicants.
2676    """
2677    grok.name('requestpw')
2678    grok.require('waeup.Anonymous')
2679    grok.template('requestpw')
2680    form_fields = grok.AutoFields(IStudentRequestPW).select(
2681        'firstname','number','email')
2682    label = _('Request password for first-time login')
2683
2684    def update(self):
2685        # Handle captcha
2686        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2687        self.captcha_result = self.captcha.verify(self.request)
2688        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2689        return
2690
2691    def _redirect(self, email, password, student_id):
2692        # Forward only email to landing page in base package.
2693        self.redirect(self.url(self.context, 'requestpw_complete',
2694            data = dict(email=email)))
2695        return
2696
2697    def _pw_used(self):
2698        # XXX: False if password has not been used. We need an extra
2699        #      attribute which remembers if student logged in.
2700        return True
2701
2702    @action(_('Send login credentials to email address'), style='primary')
2703    def get_credentials(self, **data):
2704        if not self.captcha_result.is_valid:
2705            # Captcha will display error messages automatically.
2706            # No need to flash something.
2707            return
2708        number = data.get('number','')
2709        firstname = data.get('firstname','')
2710        cat = getUtility(ICatalog, name='students_catalog')
2711        results = list(
2712            cat.searchResults(reg_number=(number, number)))
2713        if not results:
2714            results = list(
2715                cat.searchResults(matric_number=(number, number)))
2716        if results:
2717            student = results[0]
2718            if getattr(student,'firstname',None) is None:
2719                self.flash(_('An error occurred.'))
2720                return
2721            elif student.firstname.lower() != firstname.lower():
2722                # Don't tell the truth here. Anonymous must not
2723                # know that a record was found and only the firstname
2724                # verification failed.
2725                self.flash(_('No student record found.'))
2726                return
2727            elif student.password is not None and self._pw_used:
2728                self.flash(_('Your password has already been set and used. '
2729                             'Please proceed to the login page.'))
2730                return
2731            # Store email address but nothing else.
2732            student.email = data['email']
2733            notify(grok.ObjectModifiedEvent(student))
2734        else:
2735            # No record found, this is the truth.
2736            self.flash(_('No student record found.'))
2737            return
2738
2739        kofa_utils = getUtility(IKofaUtils)
2740        password = kofa_utils.genPassword()
2741        mandate = PasswordMandate()
2742        mandate.params['password'] = password
2743        mandate.params['user'] = student
2744        site = grok.getSite()
2745        site['mandates'].addMandate(mandate)
2746        # Send email with credentials
2747        args = {'mandate_id':mandate.mandate_id}
2748        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2749        url_info = u'Confirmation link: %s' % mandate_url
2750        msg = _('You have successfully requested a password for the')
2751        if kofa_utils.sendCredentials(IUserAccount(student),
2752            password, url_info, msg):
2753            email_sent = student.email
2754        else:
2755            email_sent = None
2756        self._redirect(email=email_sent, password=password,
2757            student_id=student.student_id)
2758        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2759        self.context.logger.info(
2760            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2761        return
2762
2763class StudentRequestPasswordEmailSent(KofaPage):
2764    """Landing page after successful password request.
2765
2766    """
2767    grok.name('requestpw_complete')
2768    grok.require('waeup.Public')
2769    grok.template('requestpwmailsent')
2770    label = _('Your password request was successful.')
2771
2772    def update(self, email=None, student_id=None, password=None):
2773        self.email = email
2774        self.password = password
2775        self.student_id = student_id
2776        return
2777
2778class FilterStudentsInDepartmentPage(KofaPage):
2779    """Page that filters and lists students.
2780    """
2781    grok.context(IDepartment)
2782    grok.require('waeup.showStudents')
2783    grok.name('students')
2784    grok.template('filterstudentspage')
2785    pnav = 1
2786    session_label = _('Current Session')
2787    level_label = _('Current Level')
2788
2789    def label(self):
2790        return 'Students in %s' % self.context.longtitle()
2791
2792    def _set_session_values(self):
2793        vocab_terms = academic_sessions_vocab.by_value.values()
2794        self.sessions = sorted(
2795            [(x.title, x.token) for x in vocab_terms], reverse=True)
2796        self.sessions += [('All Sessions', 'all')]
2797        return
2798
2799    def _set_level_values(self):
2800        vocab_terms = course_levels.by_value.values()
2801        self.levels = sorted(
2802            [(x.title, x.token) for x in vocab_terms])
2803        self.levels += [('All Levels', 'all')]
2804        return
2805
2806    def _searchCatalog(self, session, level):
2807        if level not in (10, 999, None):
2808            start_level = 100 * (level // 100)
2809            end_level = start_level + 90
2810        else:
2811            start_level = end_level = level
2812        cat = queryUtility(ICatalog, name='students_catalog')
2813        students = cat.searchResults(
2814            current_session=(session, session),
2815            current_level=(start_level, end_level),
2816            depcode=(self.context.code, self.context.code)
2817            )
2818        hitlist = []
2819        for student in students:
2820            hitlist.append(StudentQueryResultItem(student, view=self))
2821        return hitlist
2822
2823    def update(self, SHOW=None, session=None, level=None):
2824        datatable.need()
2825        self.parent_url = self.url(self.context.__parent__)
2826        self._set_session_values()
2827        self._set_level_values()
2828        self.hitlist = []
2829        self.session_default = session
2830        self.level_default = level
2831        if SHOW is not None:
2832            if session != 'all':
2833                self.session = int(session)
2834                self.session_string = '%s %s/%s' % (
2835                    self.session_label, self.session, self.session+1)
2836            else:
2837                self.session = None
2838                self.session_string = _('in any session')
2839            if level != 'all':
2840                self.level = int(level)
2841                self.level_string = '%s %s' % (self.level_label, self.level)
2842            else:
2843                self.level = None
2844                self.level_string = _('at any level')
2845            self.hitlist = self._searchCatalog(self.session, self.level)
2846            if not self.hitlist:
2847                self.flash(_('No student found.'))
2848        return
2849
2850class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2851    """Page that filters and lists students.
2852    """
2853    grok.context(ICertificate)
2854
2855    def label(self):
2856        return 'Students studying %s' % self.context.longtitle()
2857
2858    def _searchCatalog(self, session, level):
2859        if level not in (10, 999, None):
2860            start_level = 100 * (level // 100)
2861            end_level = start_level + 90
2862        else:
2863            start_level = end_level = level
2864        cat = queryUtility(ICatalog, name='students_catalog')
2865        students = cat.searchResults(
2866            current_session=(session, session),
2867            current_level=(start_level, end_level),
2868            certcode=(self.context.code, self.context.code)
2869            )
2870        hitlist = []
2871        for student in students:
2872            hitlist.append(StudentQueryResultItem(student, view=self))
2873        return hitlist
2874
2875class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2876    """Page that filters and lists students.
2877    """
2878    grok.context(ICourse)
2879
2880    session_label = _('Session')
2881    level_label = _('Level')
2882
2883    def label(self):
2884        return 'Students registered for %s' % self.context.longtitle()
2885
2886    def _searchCatalog(self, session, level):
2887        if level not in (10, 999, None):
2888            start_level = 100 * (level // 100)
2889            end_level = start_level + 90
2890        else:
2891            start_level = end_level = level
2892        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2893        coursetickets = cat.searchResults(
2894            session=(session, session),
2895            level=(start_level, end_level),
2896            code=(self.context.code, self.context.code)
2897            )
2898        hitlist = []
2899        for ticket in coursetickets:
2900            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
2901        return list(set(hitlist))
2902
2903class ExportJobContainerOverview(KofaPage):
2904    """Page that lists active student data export jobs and provides links
2905    to discard or download CSV files.
2906
2907    """
2908    grok.context(VirtualExportJobContainer)
2909    grok.require('waeup.showStudents')
2910    grok.name('index.html')
2911    grok.template('exportjobsindex')
2912    label = _('Student Data Exports')
2913    pnav = 1
2914
2915    def update(self, CREATE=None, DISCARD=None, job_id=None):
2916        if CREATE:
2917            self.redirect(self.url('@@exportconfig'))
2918            return
2919        if DISCARD and job_id:
2920            entry = self.context.entry_from_job_id(job_id)
2921            self.context.delete_export_entry(entry)
2922            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2923            self.context.logger.info(
2924                '%s - discarded: job_id=%s' % (ob_class, job_id))
2925            self.flash(_('Discarded export') + ' %s' % job_id)
2926        self.entries = doll_up(self, user=self.request.principal.id)
2927        return
2928
2929class ExportJobContainerJobConfig(KofaPage):
2930    """Page that configures a students export job.
2931
2932    This is a baseclass.
2933    """
2934    grok.baseclass()
2935    grok.name('exportconfig')
2936    grok.require('waeup.showStudents')
2937    grok.template('exportconfig')
2938    label = _('Configure student data export')
2939    pnav = 1
2940    redirect_target = ''
2941
2942    def _set_session_values(self):
2943        vocab_terms = academic_sessions_vocab.by_value.values()
2944        self.sessions = sorted(
2945            [(x.title, x.token) for x in vocab_terms], reverse=True)
2946        self.sessions += [(_('All Sessions'), 'all')]
2947        return
2948
2949    def _set_level_values(self):
2950        vocab_terms = course_levels.by_value.values()
2951        self.levels = sorted(
2952            [(x.title, x.token) for x in vocab_terms])
2953        self.levels += [(_('All Levels'), 'all')]
2954        return
2955
2956    def _set_mode_values(self):
2957        utils = getUtility(IKofaUtils)
2958        self.modes = sorted([(value, key) for key, value in
2959                      utils.STUDY_MODES_DICT.items()])
2960        self.modes +=[(_('All Modes'), 'all')]
2961        return
2962
2963    def _set_exporter_values(self):
2964        # We provide all student exporters, nothing else, yet.
2965        # Bursary or Department Officers don't have the general exportData
2966        # permission and are only allowed to export bursary or payments
2967        # overview data respectively. This is the only place where
2968        # waeup.exportBursaryData and waeup.exportPaymentsOverview
2969        # are used.
2970        exporters = []
2971        if not checkPermission('waeup.exportData', self.context):
2972            if checkPermission('waeup.exportBursaryData', self.context):
2973                exporters += [('Bursary Data', 'bursary')]
2974            if checkPermission('waeup.exportPaymentsOverview', self.context):
2975                exporters += [('Student Payments Overview', 'paymentsoverview')]
2976            self.exporters = exporters
2977            return
2978        for name in EXPORTER_NAMES:
2979            util = getUtility(ICSVExporter, name=name)
2980            exporters.append((util.title, name),)
2981        self.exporters = exporters
2982        return
2983
2984    @property
2985    def depcode(self):
2986        return None
2987
2988    @property
2989    def certcode(self):
2990        return None
2991
2992    def update(self, START=None, session=None, level=None, mode=None,
2993               exporter=None):
2994        self._set_session_values()
2995        self._set_level_values()
2996        self._set_mode_values()
2997        self._set_exporter_values()
2998        if START is None:
2999            return
3000        if session == 'all':
3001            session=None
3002        if level == 'all':
3003            level = None
3004        if mode == 'all':
3005            mode = None
3006        if (mode, level, session,
3007            self.depcode, self.certcode) == (None, None, None, None, None):
3008            # Export all students including those without certificate
3009            job_id = self.context.start_export_job(exporter,
3010                                          self.request.principal.id)
3011        else:
3012            job_id = self.context.start_export_job(exporter,
3013                                          self.request.principal.id,
3014                                          current_session=session,
3015                                          current_level=level,
3016                                          current_mode=mode,
3017                                          depcode=self.depcode,
3018                                          certcode=self.certcode)
3019        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3020        self.context.logger.info(
3021            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
3022            % (ob_class, exporter, session, level, mode, self.depcode,
3023            self.certcode, job_id))
3024        self.flash(_('Export started for students with') +
3025                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3026                   session, level, mode))
3027        self.redirect(self.url(self.redirect_target))
3028        return
3029
3030class ExportJobContainerDownload(ExportCSVView):
3031    """Page that downloads a students export csv file.
3032
3033    """
3034    grok.context(VirtualExportJobContainer)
3035    grok.require('waeup.showStudents')
3036
3037class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3038    """Page that configures a students export job in datacenter.
3039
3040    """
3041    grok.context(IDataCenter)
3042    redirect_target = '@@export'
3043
3044class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3045    """Page that configures a students export job in facultiescontainer.
3046
3047    """
3048    grok.context(VirtualFacultiesExportJobContainer)
3049
3050class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3051    """Page that configures a students export job in departments.
3052
3053    """
3054    grok.context(VirtualDepartmentExportJobContainer)
3055
3056    @property
3057    def depcode(self):
3058        return self.context.__parent__.code
3059
3060class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3061    """Page that configures a students export job for certificates.
3062
3063    """
3064    grok.context(VirtualCertificateExportJobContainer)
3065    grok.template('exportconfig_certificate')
3066
3067    @property
3068    def certcode(self):
3069        return self.context.__parent__.code
3070
3071class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3072    """Page that configures a students export job for courses.
3073
3074    In contrast to department or certificate student data exports the
3075    coursetickets_catalog is searched here. Therefore the update
3076    method from the base class is customized.
3077    """
3078    grok.context(VirtualCourseExportJobContainer)
3079    grok.template('exportconfig_course')
3080
3081    def _set_exporter_values(self):
3082        # We provide only two exporters.
3083        exporters = []
3084        for name in ('students', 'coursetickets'):
3085            util = getUtility(ICSVExporter, name=name)
3086            exporters.append((util.title, name),)
3087        self.exporters = exporters
3088
3089    def update(self, START=None, session=None, level=None, mode=None,
3090               exporter=None):
3091        self._set_session_values()
3092        self._set_level_values()
3093        self._set_mode_values()
3094        self._set_exporter_values()
3095        if START is None:
3096            return
3097        if session == 'all':
3098            session = None
3099        if level == 'all':
3100            level = None
3101        job_id = self.context.start_export_job(exporter,
3102                                      self.request.principal.id,
3103                                      # Use a different catalog and
3104                                      # pass different keywords than
3105                                      # for the (default) students_catalog
3106                                      catalog='coursetickets',
3107                                      session=session,
3108                                      level=level,
3109                                      code=self.context.__parent__.code)
3110        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3111        self.context.logger.info(
3112            '%s - exported: %s (%s, %s, %s), job_id=%s'
3113            % (ob_class, exporter, session, level,
3114            self.context.__parent__.code, job_id))
3115        self.flash(_('Export started for course tickets with') +
3116                   ' level_session=%s, level=%s' % (
3117                   session, level))
3118        self.redirect(self.url(self.redirect_target))
3119        return
Note: See TracBrowser for help on using the repository browser.