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

Last change on this file since 9334 was 9332, checked in by Henrik Bettermann, 12 years ago

Log the removal of study levels or course tickets.

  • Property svn:keywords set to Id
File size: 81.9 KB
Line 
1## $Id: browser.py 9332 2012-10-12 16:40:52Z 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 copy import deepcopy
25from zope.event import notify
26from zope.i18n import translate
27from zope.catalog.interfaces import ICatalog
28from zope.component import queryUtility, getUtility, createObject
29from zope.schema.interfaces import ConstraintNotSatisfied
30from zope.formlib.textwidgets import BytesDisplayWidget
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.pages import ContactAdminForm
39from waeup.kofa.browser.breadcrumbs import Breadcrumb
40from waeup.kofa.browser.resources import datepicker, datatable, tabs, warning
41from waeup.kofa.browser.layout import jsaction, action, UtilityView
42from waeup.kofa.browser.interfaces import ICaptchaManager
43from waeup.kofa.interfaces import (
44    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
45    IKofaUtils, IUniversity, IObjectHistory)
46from waeup.kofa.interfaces import MessageFactory as _
47from waeup.kofa.widgets.datewidget import (
48    FriendlyDateWidget, FriendlyDateDisplayWidget,
49    FriendlyDatetimeDisplayWidget)
50from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
51from waeup.kofa.students.interfaces import (
52    IStudentsContainer, IStudent,
53    IUGStudentClearance,IPGStudentClearance,
54    IStudentPersonal, IStudentBase, IStudentStudyCourse,
55    IStudentStudyCourseTransfer,
56    IStudentAccommodation, IStudentStudyLevel,
57    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
58    IStudentOnlinePayment, IStudentPreviousPayment,
59    IBedTicket, IStudentsUtils, IStudentRequestPW
60    )
61from waeup.kofa.students.catalog import search
62from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
63    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
64    FORBIDDEN_POSTGRAD_TRANS)
65from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
66from waeup.kofa.students.vocabularies import StudyLevelSource
67from waeup.kofa.browser.resources import toggleall
68from waeup.kofa.hostels.hostel import NOT_OCCUPIED
69from waeup.kofa.utils.helpers import get_current_principal, to_timezone
70from waeup.kofa.mandates.mandate import PasswordMandate
71
72grok.context(IKofaObject) # Make IKofaObject the default context
73
74# Save function used for save methods in pages
75def msave(view, **data):
76    changed_fields = view.applyData(view.context, **data)
77    # Turn list of lists into single list
78    if changed_fields:
79        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
80    # Inform catalog if certificate has changed
81    # (applyData does this only for the context)
82    if 'certificate' in changed_fields:
83        notify(grok.ObjectModifiedEvent(view.context.student))
84    fields_string = ' + '.join(changed_fields)
85    view.flash(_('Form has been saved.'))
86    if fields_string:
87        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
88    return
89
90def emit_lock_message(view):
91    """Flash a lock message.
92    """
93    view.flash(_('The requested form is locked (read-only).'))
94    view.redirect(view.url(view.context))
95    return
96
97def translated_values(view):
98    lang = view.request.cookies.get('kofa.language')
99    for value in view.context.values():
100        # We have to unghostify (according to Tres Seaver) the __dict__
101        # by activating the object, otherwise value_dict will be empty
102        # when calling the first time.
103        value._p_activate()
104        value_dict = dict([i for i in value.__dict__.items()])
105        value_dict['mandatory_bool'] = value.mandatory
106        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
107            target_language=lang)
108        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
109            target_language=lang)
110        value_dict['automatic'] = translate(str(value.automatic), 'zope',
111            target_language=lang)
112        yield value_dict
113
114class StudentsBreadcrumb(Breadcrumb):
115    """A breadcrumb for the students container.
116    """
117    grok.context(IStudentsContainer)
118    title = _('Students')
119
120    @property
121    def target(self):
122        user = get_current_principal()
123        if getattr(user, 'user_type', None) == 'student':
124            return None
125        return self.viewname
126
127class StudentBreadcrumb(Breadcrumb):
128    """A breadcrumb for the student container.
129    """
130    grok.context(IStudent)
131
132    def title(self):
133        return self.context.display_fullname
134
135class SudyCourseBreadcrumb(Breadcrumb):
136    """A breadcrumb for the student study course.
137    """
138    grok.context(IStudentStudyCourse)
139
140    def title(self):
141        if self.context.is_current:
142            return _('Study Course')
143        else:
144            return _('Previous Study Course')
145
146class PaymentsBreadcrumb(Breadcrumb):
147    """A breadcrumb for the student payments folder.
148    """
149    grok.context(IStudentPaymentsContainer)
150    title = _('Payments')
151
152class OnlinePaymentBreadcrumb(Breadcrumb):
153    """A breadcrumb for payments.
154    """
155    grok.context(IStudentOnlinePayment)
156
157    @property
158    def title(self):
159        return self.context.p_id
160
161class AccommodationBreadcrumb(Breadcrumb):
162    """A breadcrumb for the student accommodation folder.
163    """
164    grok.context(IStudentAccommodation)
165    title = _('Accommodation')
166
167class BedTicketBreadcrumb(Breadcrumb):
168    """A breadcrumb for bed tickets.
169    """
170    grok.context(IBedTicket)
171
172    @property
173    def title(self):
174        return _('Bed Ticket ${a}',
175            mapping = {'a':self.context.getSessionString()})
176
177class StudyLevelBreadcrumb(Breadcrumb):
178    """A breadcrumb for course lists.
179    """
180    grok.context(IStudentStudyLevel)
181
182    @property
183    def title(self):
184        return self.context.level_title
185
186class StudentsContainerPage(KofaPage):
187    """The standard view for student containers.
188    """
189    grok.context(IStudentsContainer)
190    grok.name('index')
191    grok.require('waeup.viewStudentsContainer')
192    grok.template('containerpage')
193    label = _('Student Section')
194    search_button = _('Search')
195    pnav = 4
196
197    def update(self, *args, **kw):
198        datatable.need()
199        form = self.request.form
200        self.hitlist = []
201        if 'searchterm' in form and form['searchterm']:
202            self.searchterm = form['searchterm']
203            self.searchtype = form['searchtype']
204        elif 'old_searchterm' in form:
205            self.searchterm = form['old_searchterm']
206            self.searchtype = form['old_searchtype']
207        else:
208            if 'search' in form:
209                self.flash(_('Empty search string'))
210            return
211        if self.searchtype == 'current_session':
212            try:
213                self.searchterm = int(self.searchterm)
214            except ValueError:
215                self.flash(_('Only year dates allowed (e.g. 2011).'))
216                return
217        self.hitlist = search(query=self.searchterm,
218            searchtype=self.searchtype, view=self)
219        if not self.hitlist:
220            self.flash(_('No student found.'))
221        return
222
223class StudentsContainerManagePage(KofaPage):
224    """The manage page for student containers.
225    """
226    grok.context(IStudentsContainer)
227    grok.name('manage')
228    grok.require('waeup.manageStudent')
229    grok.template('containermanagepage')
230    pnav = 4
231    label = _('Manage student section')
232    search_button = _('Search')
233    remove_button = _('Remove selected')
234
235    def update(self, *args, **kw):
236        datatable.need()
237        toggleall.need()
238        warning.need()
239        form = self.request.form
240        self.hitlist = []
241        if 'searchterm' in form and form['searchterm']:
242            self.searchterm = form['searchterm']
243            self.searchtype = form['searchtype']
244        elif 'old_searchterm' in form:
245            self.searchterm = form['old_searchterm']
246            self.searchtype = form['old_searchtype']
247        else:
248            if 'search' in form:
249                self.flash(_('Empty search string'))
250            return
251        if self.searchtype == 'current_session':
252            try:
253                self.searchterm = int(self.searchterm)
254            except ValueError:
255                self.flash('Only year dates allowed (e.g. 2011).')
256                return
257        if not 'entries' in form:
258            self.hitlist = search(query=self.searchterm,
259                searchtype=self.searchtype, view=self)
260            if not self.hitlist:
261                self.flash(_('No student found.'))
262            if 'remove' in form:
263                self.flash(_('No item selected.'))
264            return
265        entries = form['entries']
266        if isinstance(entries, basestring):
267            entries = [entries]
268        deleted = []
269        for entry in entries:
270            if 'remove' in form:
271                del self.context[entry]
272                deleted.append(entry)
273        self.hitlist = search(query=self.searchterm,
274            searchtype=self.searchtype, view=self)
275        if len(deleted):
276            self.flash(_('Successfully removed: ${a}',
277                mapping = {'a':', '.join(deleted)}))
278        return
279
280class StudentAddFormPage(KofaAddFormPage):
281    """Add-form to add a student.
282    """
283    grok.context(IStudentsContainer)
284    grok.require('waeup.manageStudent')
285    grok.name('addstudent')
286    form_fields = grok.AutoFields(IStudent).select(
287        'firstname', 'middlename', 'lastname', 'reg_number')
288    label = _('Add student')
289    pnav = 4
290
291    @action(_('Create student record'), style='primary')
292    def addStudent(self, **data):
293        student = createObject(u'waeup.Student')
294        self.applyData(student, **data)
295        self.context.addStudent(student)
296        self.flash(_('Student record created.'))
297        self.redirect(self.url(self.context[student.student_id], 'index'))
298        return
299
300class StudentBaseDisplayFormPage(KofaDisplayFormPage):
301    """ Page to display student base data
302    """
303    grok.context(IStudent)
304    grok.name('index')
305    grok.require('waeup.viewStudent')
306    grok.template('basepage')
307    form_fields = grok.AutoFields(IStudentBase).omit('password', 'suspended')
308    pnav = 4
309
310    @property
311    def label(self):
312        if self.context.suspended:
313            return _('${a}: Base Data (account deactivated)',
314                mapping = {'a':self.context.display_fullname})
315        return  _('${a}: Base Data',
316            mapping = {'a':self.context.display_fullname})
317
318    @property
319    def hasPassword(self):
320        if self.context.password:
321            return _('set')
322        return _('unset')
323
324class StudentBasePDFFormPage(KofaDisplayFormPage):
325    """ Page to display student base data in pdf files.
326    """
327    form_fields = grok.AutoFields(IStudentBase).omit(
328        'password', 'suspended', 'phone', 'adm_code', 'sex')
329
330class ContactStudentForm(ContactAdminForm):
331    grok.context(IStudent)
332    grok.name('contactstudent')
333    grok.require('waeup.viewStudent')
334    pnav = 4
335    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
336
337    def update(self, subject=u''):
338        self.form_fields.get('subject').field.default = subject
339        self.subject = subject
340        return
341
342    def label(self):
343        return _(u'Send message to ${a}',
344            mapping = {'a':self.context.display_fullname})
345
346    @action('Send message now', style='primary')
347    def send(self, *args, **data):
348        try:
349            email = self.request.principal.email
350        except AttributeError:
351            email = self.config.email_admin
352        usertype = getattr(self.request.principal,
353                           'user_type', 'system').title()
354        kofa_utils = getUtility(IKofaUtils)
355        success = kofa_utils.sendContactForm(
356                self.request.principal.title,email,
357                self.context.display_fullname,self.context.email,
358                self.request.principal.id,usertype,
359                self.config.name,
360                data['body'],data['subject'])
361        if success:
362            self.flash(_('Your message has been sent.'))
363        else:
364            self.flash(_('An smtp server error occurred.'))
365        return
366
367class ExportPDFAdmissionSlipPage(UtilityView, grok.View):
368    """Deliver a PDF Admission slip.
369    """
370    grok.context(IStudent)
371    grok.name('admission_slip.pdf')
372    grok.require('waeup.viewStudent')
373    prefix = 'form'
374
375    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
376
377    @property
378    def label(self):
379        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
380        return translate(_('Admission Letter of'),
381            'waeup.kofa', target_language=portal_language) \
382            + ' %s' % self.context.display_fullname
383
384    def render(self):
385        students_utils = getUtility(IStudentsUtils)
386        return students_utils.renderPDFAdmissionLetter(self,
387            self.context.student)
388
389class StudentBaseManageFormPage(KofaEditFormPage):
390    """ View to manage student base data
391    """
392    grok.context(IStudent)
393    grok.name('manage_base')
394    grok.require('waeup.manageStudent')
395    form_fields = grok.AutoFields(IStudentBase).omit(
396        'student_id', 'adm_code', 'suspended')
397    grok.template('basemanagepage')
398    label = _('Manage base data')
399    pnav = 4
400
401    def update(self):
402        datepicker.need() # Enable jQuery datepicker in date fields.
403        tabs.need()
404        self.tab1 = self.tab2 = ''
405        qs = self.request.get('QUERY_STRING', '')
406        if not qs:
407            qs = 'tab1'
408        setattr(self, qs, 'active')
409        super(StudentBaseManageFormPage, self).update()
410        self.wf_info = IWorkflowInfo(self.context)
411        return
412
413    @action(_('Save'), style='primary')
414    def save(self, **data):
415        form = self.request.form
416        password = form.get('password', None)
417        password_ctl = form.get('control_password', None)
418        if password:
419            validator = getUtility(IPasswordValidator)
420            errors = validator.validate_password(password, password_ctl)
421            if errors:
422                self.flash( ' '.join(errors))
423                return
424        changed_fields = self.applyData(self.context, **data)
425        # Turn list of lists into single list
426        if changed_fields:
427            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
428        else:
429            changed_fields = []
430        if password:
431            # Now we know that the form has no errors and can set password
432            IUserAccount(self.context).setPassword(password)
433            changed_fields.append('password')
434        fields_string = ' + '.join(changed_fields)
435        self.flash(_('Form has been saved.'))
436        if fields_string:
437            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
438        return
439
440class StudentTriggerTransitionFormPage(KofaEditFormPage):
441    """ View to manage student base data
442    """
443    grok.context(IStudent)
444    grok.name('trigtrans')
445    grok.require('waeup.triggerTransition')
446    grok.template('trigtrans')
447    label = _('Trigger registration transition')
448    pnav = 4
449
450    def getTransitions(self):
451        """Return a list of dicts of allowed transition ids and titles.
452
453        Each list entry provides keys ``name`` and ``title`` for
454        internal name and (human readable) title of a single
455        transition.
456        """
457        wf_info = IWorkflowInfo(self.context)
458        allowed_transitions = [t for t in wf_info.getManualTransitions()
459            if not t[0].startswith('pay')]
460        if self.context.is_postgrad:
461            allowed_transitions = [t for t in allowed_transitions
462                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
463        return [dict(name='', title=_('No transition'))] +[
464            dict(name=x, title=y) for x, y in allowed_transitions]
465
466    @action(_('Save'), style='primary')
467    def save(self, **data):
468        form = self.request.form
469        if form.has_key('transition') and form['transition']:
470            transition_id = form['transition']
471            wf_info = IWorkflowInfo(self.context)
472            wf_info.fireTransition(transition_id)
473        return
474
475class StudentActivatePage(UtilityView, grok.View):
476    """ Activate student account
477    """
478    grok.context(IStudent)
479    grok.name('activate')
480    grok.require('waeup.manageStudent')
481
482    def update(self):
483        self.context.suspended = False
484        self.context.writeLogMessage(self, 'account activated')
485        history = IObjectHistory(self.context)
486        history.addMessage('Student account activated')
487        self.flash(_('Student account has been activated.'))
488        self.redirect(self.url(self.context))
489        return
490
491    def render(self):
492        return
493
494class StudentDeactivatePage(UtilityView, grok.View):
495    """ Deactivate student account
496    """
497    grok.context(IStudent)
498    grok.name('deactivate')
499    grok.require('waeup.manageStudent')
500
501    def update(self):
502        self.context.suspended = True
503        self.context.writeLogMessage(self, 'account deactivated')
504        history = IObjectHistory(self.context)
505        history.addMessage('Student account deactivated')
506        self.flash(_('Student account has been deactivated.'))
507        self.redirect(self.url(self.context))
508        return
509
510    def render(self):
511        return
512
513class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
514    """ Page to display student clearance data
515    """
516    grok.context(IStudent)
517    grok.name('view_clearance')
518    grok.require('waeup.viewStudent')
519    pnav = 4
520
521    @property
522    def separators(self):
523        return getUtility(IStudentsUtils).SEPARATORS_DICT
524
525    @property
526    def form_fields(self):
527        if self.context.is_postgrad:
528            form_fields = grok.AutoFields(
529                IPGStudentClearance).omit('clearance_locked')
530        else:
531            form_fields = grok.AutoFields(
532                IUGStudentClearance).omit('clearance_locked')
533        return form_fields
534
535    @property
536    def label(self):
537        return _('${a}: Clearance Data',
538            mapping = {'a':self.context.display_fullname})
539
540class ExportPDFClearanceSlipPage(grok.View):
541    """Deliver a PDF slip of the context.
542    """
543    grok.context(IStudent)
544    grok.name('clearance.pdf')
545    grok.require('waeup.viewStudent')
546    prefix = 'form'
547
548    @property
549    def form_fields(self):
550        if self.context.is_postgrad:
551            form_fields = grok.AutoFields(
552                IPGStudentClearance).omit('clearance_locked')
553        else:
554            form_fields = grok.AutoFields(
555                IUGStudentClearance).omit('clearance_locked')
556        return form_fields
557
558    @property
559    def title(self):
560        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
561        return translate(_('Clearance Data'), 'waeup.kofa',
562            target_language=portal_language)
563
564    @property
565    def label(self):
566        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
567        return translate(_('Clearance Slip of'),
568            'waeup.kofa', target_language=portal_language) \
569            + ' %s' % self.context.display_fullname
570
571    def _signatures(self):
572        if self.context.state == CLEARED:
573            return (_('Student Signature'), _('Clearance Officer Signature'))
574        return
575
576    def render(self):
577        studentview = StudentBasePDFFormPage(self.context.student,
578            self.request)
579        students_utils = getUtility(IStudentsUtils)
580        return students_utils.renderPDF(
581            self, 'clearance.pdf',
582            self.context.student, studentview, signatures=self._signatures())
583
584class StudentClearanceManageFormPage(KofaEditFormPage):
585    """ Page to manage student clearance data
586    """
587    grok.context(IStudent)
588    grok.name('manage_clearance')
589    grok.require('waeup.manageStudent')
590    grok.template('clearanceeditpage')
591    label = _('Manage clearance data')
592    pnav = 4
593
594    @property
595    def separators(self):
596        return getUtility(IStudentsUtils).SEPARATORS_DICT
597
598    @property
599    def form_fields(self):
600        if self.context.is_postgrad:
601            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
602        else:
603            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
604        return form_fields
605
606    def update(self):
607        datepicker.need() # Enable jQuery datepicker in date fields.
608        tabs.need()
609        self.tab1 = self.tab2 = ''
610        qs = self.request.get('QUERY_STRING', '')
611        if not qs:
612            qs = 'tab1'
613        setattr(self, qs, 'active')
614        return super(StudentClearanceManageFormPage, self).update()
615
616    @action(_('Save'), style='primary')
617    def save(self, **data):
618        msave(self, **data)
619        return
620
621class StudentClearPage(UtilityView, grok.View):
622    """ Clear student by clearance officer
623    """
624    grok.context(IStudent)
625    grok.name('clear')
626    grok.require('waeup.clearStudent')
627
628    def update(self):
629        if self.context.state == REQUESTED:
630            IWorkflowInfo(self.context).fireTransition('clear')
631            self.flash(_('Student has been cleared.'))
632        else:
633            self.flash(_('Student is in wrong state.'))
634        self.redirect(self.url(self.context,'view_clearance'))
635        return
636
637    def render(self):
638        return
639
640class StudentRejectClearancePage(UtilityView, grok.View):
641    """ Reject clearance by clearance officers
642    """
643    grok.context(IStudent)
644    grok.name('reject_clearance')
645    grok.require('waeup.clearStudent')
646
647    def update(self):
648        if self.context.state == CLEARED:
649            IWorkflowInfo(self.context).fireTransition('reset4')
650            message = _('Clearance has been annulled.')
651            self.flash(message)
652        elif self.context.state == REQUESTED:
653            IWorkflowInfo(self.context).fireTransition('reset3')
654            message = _('Clearance request has been rejected.')
655            self.flash(message)
656        else:
657            self.flash(_('Student is in wrong state.'))
658            self.redirect(self.url(self.context,'view_clearance'))
659            return
660        args = {'subject':message}
661        self.redirect(self.url(self.context) +
662            '/contactstudent?%s' % urlencode(args))
663        return
664
665    def render(self):
666        return
667
668class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
669    """ Page to display student personal data
670    """
671    grok.context(IStudent)
672    grok.name('view_personal')
673    grok.require('waeup.viewStudent')
674    form_fields = grok.AutoFields(IStudentPersonal)
675    form_fields['perm_address'].custom_widget = BytesDisplayWidget
676    pnav = 4
677
678    @property
679    def label(self):
680        return _('${a}: Personal Data',
681            mapping = {'a':self.context.display_fullname})
682
683class StudentPersonalManageFormPage(KofaEditFormPage):
684    """ Page to manage personal data
685    """
686    grok.context(IStudent)
687    grok.name('manage_personal')
688    grok.require('waeup.manageStudent')
689    form_fields = grok.AutoFields(IStudentPersonal)
690    label = _('Manage personal data')
691    pnav = 4
692
693    @action(_('Save'), style='primary')
694    def save(self, **data):
695        msave(self, **data)
696        return
697
698class StudentPersonalEditFormPage(StudentPersonalManageFormPage):
699    """ Page to edit personal data
700    """
701    grok.name('edit_personal')
702    grok.require('waeup.handleStudent')
703    label = _('Edit personal data')
704    pnav = 4
705
706class StudyCourseDisplayFormPage(KofaDisplayFormPage):
707    """ Page to display the student study course data
708    """
709    grok.context(IStudentStudyCourse)
710    grok.name('index')
711    grok.require('waeup.viewStudent')
712    grok.template('studycoursepage')
713    pnav = 4
714
715    @property
716    def form_fields(self):
717        if self.context.is_postgrad:
718            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
719                'current_verdict', 'previous_verdict')
720        else:
721            form_fields = grok.AutoFields(IStudentStudyCourse)
722        return form_fields
723
724    @property
725    def label(self):
726        if self.context.is_current:
727            return _('${a}: Study Course',
728                mapping = {'a':self.context.__parent__.display_fullname})
729        else:
730            return _('${a}: Previous Study Course',
731                mapping = {'a':self.context.__parent__.display_fullname})
732
733    @property
734    def current_mode(self):
735        if self.context.certificate is not None:
736            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
737            return studymodes_dict[self.context.certificate.study_mode]
738        return
739
740    @property
741    def department(self):
742        if self.context.certificate is not None:
743            return self.context.certificate.__parent__.__parent__
744        return
745
746    @property
747    def faculty(self):
748        if self.context.certificate is not None:
749            return self.context.certificate.__parent__.__parent__.__parent__
750        return
751
752    @property
753    def prev_studycourses(self):
754        if self.context.is_current:
755            if self.context.__parent__.get('studycourse_2', None) is not None:
756                return (
757                        {'href':self.url(self.context.student) + '/studycourse_1',
758                        'title':_('First Study Course, ')},
759                        {'href':self.url(self.context.student) + '/studycourse_2',
760                        'title':_('Second Study Course')}
761                        )
762            if self.context.__parent__.get('studycourse_1', None) is not None:
763                return (
764                        {'href':self.url(self.context.student) + '/studycourse_1',
765                        'title':_('First Study Course')},
766                        )
767        return
768
769class StudyCourseManageFormPage(KofaEditFormPage):
770    """ Page to edit the student study course data
771    """
772    grok.context(IStudentStudyCourse)
773    grok.name('manage')
774    grok.require('waeup.manageStudent')
775    grok.template('studycoursemanagepage')
776    label = _('Manage study course')
777    pnav = 4
778    taboneactions = [_('Save'),_('Cancel')]
779    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
780    tabthreeactions = [_('Add study level')]
781
782    @property
783    def form_fields(self):
784        if self.context.is_postgrad:
785            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
786                'current_verdict', 'previous_verdict')
787        else:
788            form_fields = grok.AutoFields(IStudentStudyCourse)
789        return form_fields
790
791    def update(self):
792        if not self.context.is_current:
793            emit_lock_message(self)
794            return
795        super(StudyCourseManageFormPage, self).update()
796        tabs.need()
797        self.tab1 = self.tab2 = ''
798        qs = self.request.get('QUERY_STRING', '')
799        if not qs:
800            qs = 'tab1'
801        setattr(self, qs, 'active')
802        warning.need()
803        datatable.need()
804        return
805
806    @action(_('Save'), style='primary')
807    def save(self, **data):
808        try:
809            msave(self, **data)
810        except ConstraintNotSatisfied:
811            # The selected level might not exist in certificate
812            self.flash(_('Current level not available for certificate.'))
813            return
814        notify(grok.ObjectModifiedEvent(self.context.__parent__))
815        return
816
817    @property
818    def level_dict(self):
819        studylevelsource = StudyLevelSource().factory
820        for code in studylevelsource.getValues(self.context):
821            title = studylevelsource.getTitle(self.context, code)
822            yield(dict(code=code, title=title))
823
824    @action(_('Add study level'))
825    def addStudyLevel(self, **data):
826        level_code = self.request.form.get('addlevel', None)
827        studylevel = createObject(u'waeup.StudentStudyLevel')
828        studylevel.level = int(level_code)
829        studylevel.level_session = self.context.current_session
830        try:
831            self.context.addStudentStudyLevel(
832                self.context.certificate,studylevel)
833            self.flash(_('Study level has been added.'))
834        except KeyError:
835            self.flash(_('This level exists.'))
836        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
837        return
838
839    @jsaction(_('Remove selected levels'))
840    def delStudyLevels(self, **data):
841        form = self.request.form
842        if form.has_key('val_id'):
843            child_id = form['val_id']
844        else:
845            self.flash(_('No study level selected.'))
846            self.redirect(self.url(self.context, '@@manage')+'?tab2')
847            return
848        if not isinstance(child_id, list):
849            child_id = [child_id]
850        deleted = []
851        for id in child_id:
852            del self.context[id]
853            deleted.append(id)
854        if len(deleted):
855            self.flash(_('Successfully removed: ${a}',
856                mapping = {'a':', '.join(deleted)}))
857            self.context.writeLogMessage(
858                self,'removed: %s' % ', '.join(deleted))
859        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
860        return
861
862class StudentTransferFormPage(KofaAddFormPage):
863    """Page to transfer the student.
864    """
865    grok.context(IStudent)
866    grok.name('transfer')
867    grok.require('waeup.manageStudent')
868    label = _('Transfer student')
869    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
870        'entry_mode', 'entry_session')
871    pnav = 4
872
873    def update(self):
874        super(StudentTransferFormPage, self).update()
875        warning.need()
876        return
877
878    @jsaction(_('Transfer'))
879    def transferStudent(self, **data):
880        error = self.context.transfer(**data)
881        if error == -1:
882            self.flash(_('Current level does not match certificate levels.'))
883        elif error == -2:
884            self.flash(_('Former study course record incomplete.'))
885        elif error == -3:
886            self.flash(_('Maximum number of transfers exceeded.'))
887        else:
888            self.flash(_('Successfully transferred.'))
889        return
890
891class StudyLevelDisplayFormPage(KofaDisplayFormPage):
892    """ Page to display student study levels
893    """
894    grok.context(IStudentStudyLevel)
895    grok.name('index')
896    grok.require('waeup.viewStudent')
897    form_fields = grok.AutoFields(IStudentStudyLevel)
898    form_fields[
899        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
900    grok.template('studylevelpage')
901    pnav = 4
902
903    def update(self):
904        super(StudyLevelDisplayFormPage, self).update()
905        datatable.need()
906        return
907
908    @property
909    def translated_values(self):
910        return translated_values(self)
911
912    @property
913    def label(self):
914        # Here we know that the cookie has been set
915        lang = self.request.cookies.get('kofa.language')
916        level_title = translate(self.context.level_title, 'waeup.kofa',
917            target_language=lang)
918        return _('${a}: Study Level ${b}', mapping = {
919            'a':self.context.student.display_fullname,
920            'b':level_title})
921
922    @property
923    def total_credits(self):
924        total_credits = 0
925        for key, val in self.context.items():
926            total_credits += val.credits
927        return total_credits
928
929class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
930    """Deliver a PDF slip of the context.
931    """
932    grok.context(IStudentStudyLevel)
933    grok.name('course_registration.pdf')
934    grok.require('waeup.viewStudent')
935    form_fields = grok.AutoFields(IStudentStudyLevel)
936    prefix = 'form'
937
938    @property
939    def title(self):
940        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
941        return translate(_('Level Data'), 'waeup.kofa',
942            target_language=portal_language)
943
944    @property
945    def content_title(self):
946        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
947        return translate(_('Course List'), 'waeup.kofa',
948            target_language=portal_language)
949
950    @property
951    def label(self):
952        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
953        lang = self.request.cookies.get('kofa.language', portal_language)
954        level_title = translate(self.context.level_title, 'waeup.kofa',
955            target_language=lang)
956        return translate(_('Course Registration Slip'),
957            'waeup.kofa', target_language=portal_language) \
958            + ' %s' % level_title
959
960    def render(self):
961        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
962        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
963        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
964        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
965        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
966        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
967        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
968        Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
969        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
970        studentview = StudentBasePDFFormPage(self.context.student,
971            self.request)
972        students_utils = getUtility(IStudentsUtils)
973        tabledata = sorted(self.context.values(),
974            key=lambda value: str(value.semester) + value.code)
975        return students_utils.renderPDF(
976            self, 'course_registration.pdf',
977            self.context.student, studentview,
978            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
979                         (Title,'title', 5),
980                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
981                         (Cred, 'credits', 1.5),
982                         (Mand, 'mandatory', 1.5),
983                         (Score, 'score', 1.5),
984                         #('Auto', 'automatic', 1.5)
985                         ],
986            tabledata=tabledata)
987
988class StudyLevelManageFormPage(KofaEditFormPage):
989    """ Page to edit the student study level data
990    """
991    grok.context(IStudentStudyLevel)
992    grok.name('manage')
993    grok.require('waeup.manageStudent')
994    grok.template('studylevelmanagepage')
995    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
996        'validation_date', 'validated_by')
997    pnav = 4
998    taboneactions = [_('Save'),_('Cancel')]
999    tabtwoactions = [_('Add course ticket'),
1000        _('Remove selected tickets'),_('Cancel')]
1001
1002    def update(self):
1003        if not self.context.__parent__.is_current:
1004            emit_lock_message(self)
1005            return
1006        super(StudyLevelManageFormPage, self).update()
1007        tabs.need()
1008        self.tab1 = self.tab2 = ''
1009        qs = self.request.get('QUERY_STRING', '')
1010        if not qs:
1011            qs = 'tab1'
1012        setattr(self, qs, 'active')
1013        warning.need()
1014        datatable.need()
1015        return
1016
1017    @property
1018    def translated_values(self):
1019        return translated_values(self)
1020
1021    @property
1022    def label(self):
1023        # Here we know that the cookie has been set
1024        lang = self.request.cookies.get('kofa.language')
1025        level_title = translate(self.context.level_title, 'waeup.kofa',
1026            target_language=lang)
1027        return _('Manage study level ${a}',
1028            mapping = {'a':level_title})
1029
1030    @action(_('Save'), style='primary')
1031    def save(self, **data):
1032        msave(self, **data)
1033        return
1034
1035    @action(_('Add course ticket'))
1036    def addCourseTicket(self, **data):
1037        self.redirect(self.url(self.context, '@@add'))
1038
1039    @jsaction(_('Remove selected tickets'))
1040    def delCourseTicket(self, **data):
1041        form = self.request.form
1042        if form.has_key('val_id'):
1043            child_id = form['val_id']
1044        else:
1045            self.flash(_('No ticket selected.'))
1046            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1047            return
1048        if not isinstance(child_id, list):
1049            child_id = [child_id]
1050        deleted = []
1051        for id in child_id:
1052            del self.context[id]
1053            deleted.append(id)
1054        if len(deleted):
1055            self.flash(_('Successfully removed: ${a}',
1056                mapping = {'a':', '.join(deleted)}))
1057            self.context.writeLogMessage(
1058                self,'removed: %s' % ', '.join(deleted))
1059        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1060        return
1061
1062class ValidateCoursesPage(UtilityView, grok.View):
1063    """ Validate course list by course adviser
1064    """
1065    grok.context(IStudentStudyLevel)
1066    grok.name('validate_courses')
1067    grok.require('waeup.validateStudent')
1068
1069    def update(self):
1070        if not self.context.__parent__.is_current:
1071            emit_lock_message(self)
1072            return
1073        if str(self.context.__parent__.current_level) != self.context.__name__:
1074            self.flash(_('This level does not correspond current level.'))
1075        elif self.context.student.state == REGISTERED:
1076            IWorkflowInfo(self.context.student).fireTransition(
1077                'validate_courses')
1078            self.flash(_('Course list has been validated.'))
1079        else:
1080            self.flash(_('Student is in the wrong state.'))
1081        self.redirect(self.url(self.context))
1082        return
1083
1084    def render(self):
1085        return
1086
1087class RejectCoursesPage(UtilityView, grok.View):
1088    """ Reject course list by course adviser
1089    """
1090    grok.context(IStudentStudyLevel)
1091    grok.name('reject_courses')
1092    grok.require('waeup.validateStudent')
1093
1094    def update(self):
1095        if not self.context.__parent__.is_current:
1096            emit_lock_message(self)
1097            return
1098        if str(self.context.__parent__.current_level) != self.context.__name__:
1099            self.flash(_('This level does not correspond current level.'))
1100            self.redirect(self.url(self.context))
1101            return
1102        elif self.context.student.state == VALIDATED:
1103            IWorkflowInfo(self.context.student).fireTransition('reset8')
1104            message = _('Course list request has been annulled.')
1105            self.flash(message)
1106        elif self.context.student.state == REGISTERED:
1107            IWorkflowInfo(self.context.student).fireTransition('reset7')
1108            message = _('Course list request has been rejected:')
1109            self.flash(message)
1110        else:
1111            self.flash(_('Student is in the wrong state.'))
1112            self.redirect(self.url(self.context))
1113            return
1114        args = {'subject':message}
1115        self.redirect(self.url(self.context.student) +
1116            '/contactstudent?%s' % urlencode(args))
1117        return
1118
1119    def render(self):
1120        return
1121
1122class CourseTicketAddFormPage(KofaAddFormPage):
1123    """Add a course ticket.
1124    """
1125    grok.context(IStudentStudyLevel)
1126    grok.name('add')
1127    grok.require('waeup.manageStudent')
1128    label = _('Add course ticket')
1129    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1130        'score', 'automatic', 'carry_over', 'credits', 'passmark')
1131    pnav = 4
1132
1133    def update(self):
1134        if not self.context.__parent__.is_current:
1135            emit_lock_message(self)
1136            return
1137        super(CourseTicketAddFormPage, self).update()
1138        return
1139
1140    @action(_('Add course ticket'))
1141    def addCourseTicket(self, **data):
1142        ticket = createObject(u'waeup.CourseTicket')
1143        course = data['course']
1144        ticket.automatic = False
1145        ticket.carry_over = False
1146        try:
1147            self.context.addCourseTicket(ticket, course)
1148        except KeyError:
1149            self.flash(_('The ticket exists.'))
1150            return
1151        self.flash(_('Successfully added ${a}.',
1152            mapping = {'a':ticket.code}))
1153        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1154        return
1155
1156    @action(_('Cancel'), validator=NullValidator)
1157    def cancel(self, **data):
1158        self.redirect(self.url(self.context))
1159
1160class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1161    """ Page to display course tickets
1162    """
1163    grok.context(ICourseTicket)
1164    grok.name('index')
1165    grok.require('waeup.viewStudent')
1166    form_fields = grok.AutoFields(ICourseTicket)
1167    grok.template('courseticketpage')
1168    pnav = 4
1169
1170    @property
1171    def label(self):
1172        return _('${a}: Course Ticket ${b}', mapping = {
1173            'a':self.context.student.display_fullname,
1174            'b':self.context.code})
1175
1176class CourseTicketManageFormPage(KofaEditFormPage):
1177    """ Page to manage course tickets
1178    """
1179    grok.context(ICourseTicket)
1180    grok.name('manage')
1181    grok.require('waeup.manageStudent')
1182    form_fields = grok.AutoFields(ICourseTicket).omit('credits', 'passmark')
1183    grok.template('courseticketmanagepage')
1184    pnav = 4
1185
1186    @property
1187    def label(self):
1188        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1189
1190    @action('Save', style='primary')
1191    def save(self, **data):
1192        msave(self, **data)
1193        return
1194
1195class PaymentsManageFormPage(KofaEditFormPage):
1196    """ Page to manage the student payments
1197
1198    This manage form page is for both students and students officers.
1199    """
1200    grok.context(IStudentPaymentsContainer)
1201    grok.name('index')
1202    grok.require('waeup.payStudent')
1203    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1204    grok.template('paymentsmanagepage')
1205    pnav = 4
1206
1207    def unremovable(self, ticket):
1208        usertype = getattr(self.request.principal, 'user_type', None)
1209        if not usertype:
1210            return False
1211        return (self.request.principal.user_type == 'student' and ticket.r_code)
1212
1213    @property
1214    def label(self):
1215        return _('${a}: Payments',
1216            mapping = {'a':self.context.__parent__.display_fullname})
1217
1218    def update(self):
1219        super(PaymentsManageFormPage, self).update()
1220        datatable.need()
1221        warning.need()
1222        return
1223
1224    @jsaction(_('Remove selected tickets'))
1225    def delPaymentTicket(self, **data):
1226        form = self.request.form
1227        if form.has_key('val_id'):
1228            child_id = form['val_id']
1229        else:
1230            self.flash(_('No payment selected.'))
1231            self.redirect(self.url(self.context))
1232            return
1233        if not isinstance(child_id, list):
1234            child_id = [child_id]
1235        deleted = []
1236        for id in child_id:
1237            # Students are not allowed to remove used payment tickets
1238            if not self.unremovable(self.context[id]):
1239                del self.context[id]
1240                deleted.append(id)
1241        if len(deleted):
1242            self.flash(_('Successfully removed: ${a}',
1243                mapping = {'a': ', '.join(deleted)}))
1244            self.context.writeLogMessage(
1245                self,'removed: %s' % ', '.join(deleted))
1246        self.redirect(self.url(self.context))
1247        return
1248
1249    @action(_('Add online payment ticket'))
1250    def addPaymentTicket(self, **data):
1251        self.redirect(self.url(self.context, '@@addop'))
1252
1253class OnlinePaymentAddFormPage(KofaAddFormPage):
1254    """ Page to add an online payment ticket
1255    """
1256    grok.context(IStudentPaymentsContainer)
1257    grok.name('addop')
1258    grok.require('waeup.payStudent')
1259    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1260        'p_category')
1261    label = _('Add online payment')
1262    pnav = 4
1263
1264    @action(_('Create ticket'), style='primary')
1265    def createTicket(self, **data):
1266        p_category = data['p_category']
1267        previous_session = data.get('p_session', None)
1268        previous_level = data.get('p_level', None)
1269        student = self.context.__parent__
1270        if p_category == 'bed_allocation' and student[
1271            'studycourse'].current_session != grok.getSite()[
1272            'hostels'].accommodation_session:
1273                self.flash(
1274                    _('Your current session does not match ' + \
1275                    'accommodation session.'))
1276                self.redirect(self.url(self.context))
1277                return
1278        students_utils = getUtility(IStudentsUtils)
1279        error, payment = students_utils.setPaymentDetails(
1280            p_category, student, previous_session, previous_level)
1281        if error is not None:
1282            self.flash(error)
1283            if 'previous session' in error:
1284                self.redirect(self.url(self.context) + '/@@addpp')
1285                return
1286            self.redirect(self.url(self.context))
1287            return
1288        self.context[payment.p_id] = payment
1289        self.flash(_('Payment ticket created.'))
1290        self.redirect(self.url(self.context))
1291        return
1292
1293class PreviousPaymentAddFormPage(OnlinePaymentAddFormPage):
1294    """ Page to add an online payment ticket for previous sessions
1295    """
1296    grok.context(IStudentPaymentsContainer)
1297    grok.name('addpp')
1298    grok.require('waeup.payStudent')
1299    form_fields = grok.AutoFields(IStudentPreviousPayment).select(
1300        'p_category', 'p_session', 'p_level')
1301    label = _('Add previous session online payment')
1302    pnav = 4
1303
1304class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1305    """ Page to view an online payment ticket
1306    """
1307    grok.context(IStudentOnlinePayment)
1308    grok.name('index')
1309    grok.require('waeup.viewStudent')
1310    form_fields = grok.AutoFields(IStudentOnlinePayment)
1311    form_fields[
1312        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1313    form_fields[
1314        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1315    pnav = 4
1316
1317    @property
1318    def label(self):
1319        return _('${a}: Online Payment Ticket ${b}', mapping = {
1320            'a':self.context.student.display_fullname,
1321            'b':self.context.p_id})
1322
1323class OnlinePaymentApprovePage(UtilityView, grok.View):
1324    """ Callback view
1325    """
1326    grok.context(IStudentOnlinePayment)
1327    grok.name('approve')
1328    grok.require('waeup.managePortal')
1329
1330    def update(self):
1331        success, msg, log = self.context.approveStudentPayment()
1332        if log is not None:
1333            self.context.writeLogMessage(self,log)
1334        self.flash(msg)
1335        return
1336
1337    def render(self):
1338        self.redirect(self.url(self.context, '@@index'))
1339        return
1340
1341class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1342    """ Approval view for students.
1343
1344    This view is used for browser tests only and
1345    must be neutralized in custom pages!
1346    """
1347
1348    grok.name('fake_approve')
1349    grok.require('waeup.payStudent')
1350
1351class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1352    """Deliver a PDF slip of the context.
1353    """
1354    grok.context(IStudentOnlinePayment)
1355    grok.name('payment_slip.pdf')
1356    grok.require('waeup.viewStudent')
1357    form_fields = grok.AutoFields(IStudentOnlinePayment)
1358    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1359    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1360    prefix = 'form'
1361    note = None
1362
1363    @property
1364    def title(self):
1365        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1366        return translate(_('Payment Data'), 'waeup.kofa',
1367            target_language=portal_language)
1368
1369    @property
1370    def label(self):
1371        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1372        return translate(_('Online Payment Slip'),
1373            'waeup.kofa', target_language=portal_language) \
1374            + ' %s' % self.context.p_id
1375
1376    def render(self):
1377        #if self.context.p_state != 'paid':
1378        #    self.flash('Ticket not yet paid.')
1379        #    self.redirect(self.url(self.context))
1380        #    return
1381        studentview = StudentBasePDFFormPage(self.context.student,
1382            self.request)
1383        students_utils = getUtility(IStudentsUtils)
1384        return students_utils.renderPDF(self, 'payment_slip.pdf',
1385            self.context.student, studentview, note=self.note)
1386
1387
1388class AccommodationManageFormPage(KofaEditFormPage):
1389    """ Page to manage bed tickets.
1390
1391    This manage form page is for both students and students officers.
1392    """
1393    grok.context(IStudentAccommodation)
1394    grok.name('index')
1395    grok.require('waeup.handleAccommodation')
1396    form_fields = grok.AutoFields(IStudentAccommodation)
1397    grok.template('accommodationmanagepage')
1398    pnav = 4
1399    officers_only_actions = [_('Remove selected')]
1400
1401    @property
1402    def label(self):
1403        return _('${a}: Accommodation',
1404            mapping = {'a':self.context.__parent__.display_fullname})
1405
1406    def update(self):
1407        super(AccommodationManageFormPage, self).update()
1408        datatable.need()
1409        warning.need()
1410        return
1411
1412    @jsaction(_('Remove selected'))
1413    def delBedTickets(self, **data):
1414        if getattr(self.request.principal, 'user_type', None) == 'student':
1415            self.flash(_('You are not allowed to remove bed tickets.'))
1416            self.redirect(self.url(self.context))
1417            return
1418        form = self.request.form
1419        if form.has_key('val_id'):
1420            child_id = form['val_id']
1421        else:
1422            self.flash(_('No bed ticket selected.'))
1423            self.redirect(self.url(self.context))
1424            return
1425        if not isinstance(child_id, list):
1426            child_id = [child_id]
1427        deleted = []
1428        for id in child_id:
1429            del self.context[id]
1430            deleted.append(id)
1431        if len(deleted):
1432            self.flash(_('Successfully removed: ${a}',
1433                mapping = {'a':', '.join(deleted)}))
1434            self.context.writeLogMessage(
1435                self,'removed: % s' % ', '.join(deleted))
1436        self.redirect(self.url(self.context))
1437        return
1438
1439    @property
1440    def selected_actions(self):
1441        if getattr(self.request.principal, 'user_type', None) == 'student':
1442            return [action for action in self.actions
1443                    if not action.label in self.officers_only_actions]
1444        return self.actions
1445
1446class BedTicketAddPage(KofaPage):
1447    """ Page to add an online payment ticket
1448    """
1449    grok.context(IStudentAccommodation)
1450    grok.name('add')
1451    grok.require('waeup.handleAccommodation')
1452    grok.template('enterpin')
1453    ac_prefix = 'HOS'
1454    label = _('Add bed ticket')
1455    pnav = 4
1456    buttonname = _('Create bed ticket')
1457    notice = ''
1458    with_ac = True
1459
1460    def update(self, SUBMIT=None):
1461        student = self.context.student
1462        students_utils = getUtility(IStudentsUtils)
1463        acc_details  = students_utils.getAccommodationDetails(student)
1464        if acc_details.get('expired', False):
1465            startdate = acc_details.get('startdate')
1466            enddate = acc_details.get('enddate')
1467            if startdate and enddate:
1468                tz = getUtility(IKofaUtils).tzinfo
1469                startdate = to_timezone(
1470                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1471                enddate = to_timezone(
1472                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1473                self.flash(_("Outside booking period: ${a} - ${b}",
1474                    mapping = {'a': startdate, 'b': enddate}))
1475            else:
1476                self.flash(_("Outside booking period."))
1477            self.redirect(self.url(self.context))
1478            return
1479        if not acc_details:
1480            self.flash(_("Your data are incomplete."))
1481            self.redirect(self.url(self.context))
1482            return
1483        if not student.state in acc_details['allowed_states']:
1484            self.flash(_("You are in the wrong registration state."))
1485            self.redirect(self.url(self.context))
1486            return
1487        if student['studycourse'].current_session != acc_details[
1488            'booking_session']:
1489            self.flash(
1490                _('Your current session does not match accommodation session.'))
1491            self.redirect(self.url(self.context))
1492            return
1493        if str(acc_details['booking_session']) in self.context.keys():
1494            self.flash(
1495                _('You already booked a bed space in current ' \
1496                    + 'accommodation session.'))
1497            self.redirect(self.url(self.context))
1498            return
1499        if self.with_ac:
1500            self.ac_series = self.request.form.get('ac_series', None)
1501            self.ac_number = self.request.form.get('ac_number', None)
1502        if SUBMIT is None:
1503            return
1504        if self.with_ac:
1505            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1506            code = get_access_code(pin)
1507            if not code:
1508                self.flash(_('Activation code is invalid.'))
1509                return
1510        # Search and book bed
1511        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1512        entries = cat.searchResults(
1513            owner=(student.student_id,student.student_id))
1514        if len(entries):
1515            # If bed space has been manually allocated use this bed
1516            bed = [entry for entry in entries][0]
1517        else:
1518            # else search for other available beds
1519            entries = cat.searchResults(
1520                bed_type=(acc_details['bt'],acc_details['bt']))
1521            available_beds = [
1522                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1523            if available_beds:
1524                students_utils = getUtility(IStudentsUtils)
1525                bed = students_utils.selectBed(available_beds)
1526                bed.bookBed(student.student_id)
1527            else:
1528                self.flash(_('There is no free bed in your category ${a}.',
1529                    mapping = {'a':acc_details['bt']}))
1530                return
1531        if self.with_ac:
1532            # Mark pin as used (this also fires a pin related transition)
1533            if code.state == USED:
1534                self.flash(_('Activation code has already been used.'))
1535                return
1536            else:
1537                comment = _(u'invalidated')
1538                # Here we know that the ac is in state initialized so we do not
1539                # expect an exception, but the owner might be different
1540                if not invalidate_accesscode(
1541                    pin,comment,self.context.student.student_id):
1542                    self.flash(_('You are not the owner of this access code.'))
1543                    return
1544        # Create bed ticket
1545        bedticket = createObject(u'waeup.BedTicket')
1546        if self.with_ac:
1547            bedticket.booking_code = pin
1548        bedticket.booking_session = acc_details['booking_session']
1549        bedticket.bed_type = acc_details['bt']
1550        bedticket.bed = bed
1551        hall_title = bed.__parent__.hostel_name
1552        coordinates = bed.coordinates[1:]
1553        block, room_nr, bed_nr = coordinates
1554        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1555            'a':hall_title, 'b':block,
1556            'c':room_nr, 'd':bed_nr,
1557            'e':bed.bed_type})
1558        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1559        bedticket.bed_coordinates = translate(
1560            bc, 'waeup.kofa',target_language=portal_language)
1561        key = str(acc_details['booking_session'])
1562        self.context[key] = bedticket
1563        self.flash(_('Bed ticket created and bed booked: ${a}',
1564            mapping = {'a':bedticket.bed_coordinates}))
1565        self.redirect(self.url(self.context))
1566        return
1567
1568class BedTicketDisplayFormPage(KofaDisplayFormPage):
1569    """ Page to display bed tickets
1570    """
1571    grok.context(IBedTicket)
1572    grok.name('index')
1573    grok.require('waeup.handleAccommodation')
1574    form_fields = grok.AutoFields(IBedTicket)
1575    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1576    pnav = 4
1577
1578    @property
1579    def label(self):
1580        return _('Bed Ticket for Session ${a}',
1581            mapping = {'a':self.context.getSessionString()})
1582
1583class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1584    """Deliver a PDF slip of the context.
1585    """
1586    grok.context(IBedTicket)
1587    grok.name('bed_allocation.pdf')
1588    grok.require('waeup.handleAccommodation')
1589    form_fields = grok.AutoFields(IBedTicket)
1590    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1591    prefix = 'form'
1592
1593    @property
1594    def title(self):
1595        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1596        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1597            target_language=portal_language)
1598
1599    @property
1600    def label(self):
1601        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1602        #return translate(_('Bed Allocation: '),
1603        #    'waeup.kofa', target_language=portal_language) \
1604        #    + ' %s' % self.context.bed_coordinates
1605        return translate(_('Bed Allocation Slip'),
1606            'waeup.kofa', target_language=portal_language) \
1607            + ' %s' % self.context.getSessionString()
1608
1609    def render(self):
1610        studentview = StudentBasePDFFormPage(self.context.student,
1611            self.request)
1612        students_utils = getUtility(IStudentsUtils)
1613        return students_utils.renderPDF(
1614            self, 'bed_allocation.pdf',
1615            self.context.student, studentview)
1616
1617class BedTicketRelocationPage(UtilityView, grok.View):
1618    """ Callback view
1619    """
1620    grok.context(IBedTicket)
1621    grok.name('relocate')
1622    grok.require('waeup.manageHostels')
1623
1624    # Relocate student if student parameters have changed or the bed_type
1625    # of the bed has changed
1626    def update(self):
1627        student = self.context.student
1628        students_utils = getUtility(IStudentsUtils)
1629        acc_details  = students_utils.getAccommodationDetails(student)
1630        if self.context.bed != None and \
1631              'reserved' in self.context.bed.bed_type:
1632            self.flash(_("Students in reserved beds can't be relocated."))
1633            self.redirect(self.url(self.context))
1634            return
1635        if acc_details['bt'] == self.context.bed_type and \
1636                self.context.bed != None and \
1637                self.context.bed.bed_type == self.context.bed_type:
1638            self.flash(_("Student can't be relocated."))
1639            self.redirect(self.url(self.context))
1640            return
1641        # Search a bed
1642        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1643        entries = cat.searchResults(
1644            owner=(student.student_id,student.student_id))
1645        if len(entries) and self.context.bed == None:
1646            # If booking has been cancelled but other bed space has been
1647            # manually allocated after cancellation use this bed
1648            new_bed = [entry for entry in entries][0]
1649        else:
1650            # Search for other available beds
1651            entries = cat.searchResults(
1652                bed_type=(acc_details['bt'],acc_details['bt']))
1653            available_beds = [
1654                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1655            if available_beds:
1656                students_utils = getUtility(IStudentsUtils)
1657                new_bed = students_utils.selectBed(available_beds)
1658                new_bed.bookBed(student.student_id)
1659            else:
1660                self.flash(_('There is no free bed in your category ${a}.',
1661                    mapping = {'a':acc_details['bt']}))
1662                self.redirect(self.url(self.context))
1663                return
1664        # Release old bed if exists
1665        if self.context.bed != None:
1666            self.context.bed.owner = NOT_OCCUPIED
1667            notify(grok.ObjectModifiedEvent(self.context.bed))
1668        # Alocate new bed
1669        self.context.bed_type = acc_details['bt']
1670        self.context.bed = new_bed
1671        hall_title = new_bed.__parent__.hostel_name
1672        coordinates = new_bed.coordinates[1:]
1673        block, room_nr, bed_nr = coordinates
1674        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1675            'a':hall_title, 'b':block,
1676            'c':room_nr, 'd':bed_nr,
1677            'e':new_bed.bed_type})
1678        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1679        self.context.bed_coordinates = translate(
1680            bc, 'waeup.kofa',target_language=portal_language)
1681        self.flash(_('Student relocated: ${a}',
1682            mapping = {'a':self.context.bed_coordinates}))
1683        self.redirect(self.url(self.context))
1684        return
1685
1686    def render(self):
1687        return
1688
1689class StudentHistoryPage(KofaPage):
1690    """ Page to display student clearance data
1691    """
1692    grok.context(IStudent)
1693    grok.name('history')
1694    grok.require('waeup.viewStudent')
1695    grok.template('studenthistory')
1696    pnav = 4
1697
1698    @property
1699    def label(self):
1700        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1701
1702# Pages for students only
1703
1704class StudentBaseEditFormPage(KofaEditFormPage):
1705    """ View to edit student base data
1706    """
1707    grok.context(IStudent)
1708    grok.name('edit_base')
1709    grok.require('waeup.handleStudent')
1710    form_fields = grok.AutoFields(IStudentBase).select(
1711        'email', 'phone')
1712    label = _('Edit base data')
1713    pnav = 4
1714
1715    @action(_('Save'), style='primary')
1716    def save(self, **data):
1717        msave(self, **data)
1718        return
1719
1720class StudentChangePasswordPage(KofaEditFormPage):
1721    """ View to manage student base data
1722    """
1723    grok.context(IStudent)
1724    grok.name('change_password')
1725    grok.require('waeup.handleStudent')
1726    grok.template('change_password')
1727    label = _('Change password')
1728    pnav = 4
1729
1730    @action(_('Save'), style='primary')
1731    def save(self, **data):
1732        form = self.request.form
1733        password = form.get('change_password', None)
1734        password_ctl = form.get('change_password_repeat', None)
1735        if password:
1736            validator = getUtility(IPasswordValidator)
1737            errors = validator.validate_password(password, password_ctl)
1738            if not errors:
1739                IUserAccount(self.context).setPassword(password)
1740                self.context.writeLogMessage(self, 'saved: password')
1741                self.flash(_('Password changed.'))
1742            else:
1743                self.flash( ' '.join(errors))
1744        return
1745
1746class StudentFilesUploadPage(KofaPage):
1747    """ View to upload files by student
1748    """
1749    grok.context(IStudent)
1750    grok.name('change_portrait')
1751    grok.require('waeup.uploadStudentFile')
1752    grok.template('filesuploadpage')
1753    label = _('Upload portrait')
1754    pnav = 4
1755
1756    def update(self):
1757        if self.context.student.state != ADMITTED:
1758            emit_lock_message(self)
1759            return
1760        super(StudentFilesUploadPage, self).update()
1761        return
1762
1763class StartClearancePage(KofaPage):
1764    grok.context(IStudent)
1765    grok.name('start_clearance')
1766    grok.require('waeup.handleStudent')
1767    grok.template('enterpin')
1768    label = _('Start clearance')
1769    ac_prefix = 'CLR'
1770    notice = ''
1771    pnav = 4
1772    buttonname = _('Start clearance now')
1773
1774    @property
1775    def all_required_fields_filled(self):
1776        if self.context.email and self.context.phone:
1777            return True
1778        return False
1779
1780    @property
1781    def portrait_uploaded(self):
1782        store = getUtility(IExtFileStore)
1783        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1784            return True
1785        return False
1786
1787    def update(self, SUBMIT=None):
1788        if not self.context.state == ADMITTED:
1789            self.flash(_("Wrong state"))
1790            self.redirect(self.url(self.context))
1791            return
1792        if not self.portrait_uploaded:
1793            self.flash(_("No portrait uploaded."))
1794            self.redirect(self.url(self.context, 'change_portrait'))
1795            return
1796        if not self.all_required_fields_filled:
1797            self.flash(_("Not all required fields filled."))
1798            self.redirect(self.url(self.context, 'edit_base'))
1799            return
1800        self.ac_series = self.request.form.get('ac_series', None)
1801        self.ac_number = self.request.form.get('ac_number', None)
1802
1803        if SUBMIT is None:
1804            return
1805        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1806        code = get_access_code(pin)
1807        if not code:
1808            self.flash(_('Activation code is invalid.'))
1809            return
1810        if code.state == USED:
1811            self.flash(_('Activation code has already been used.'))
1812            return
1813        # Mark pin as used (this also fires a pin related transition)
1814        # and fire transition start_clearance
1815        comment = _(u"invalidated")
1816        # Here we know that the ac is in state initialized so we do not
1817        # expect an exception, but the owner might be different
1818        if not invalidate_accesscode(pin, comment, self.context.student_id):
1819            self.flash(_('You are not the owner of this access code.'))
1820            return
1821        self.context.clr_code = pin
1822        IWorkflowInfo(self.context).fireTransition('start_clearance')
1823        self.flash(_('Clearance process has been started.'))
1824        self.redirect(self.url(self.context,'cedit'))
1825        return
1826
1827class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1828    """ View to edit student clearance data by student
1829    """
1830    grok.context(IStudent)
1831    grok.name('cedit')
1832    grok.require('waeup.handleStudent')
1833    label = _('Edit clearance data')
1834
1835    @property
1836    def form_fields(self):
1837        if self.context.is_postgrad:
1838            form_fields = grok.AutoFields(IPGStudentClearance).omit(
1839                'clearance_locked', 'clr_code')
1840        else:
1841            form_fields = grok.AutoFields(IUGStudentClearance).omit(
1842                'clearance_locked', 'clr_code')
1843        return form_fields
1844
1845    def update(self):
1846        if self.context.clearance_locked:
1847            emit_lock_message(self)
1848            return
1849        return super(StudentClearanceEditFormPage, self).update()
1850
1851    @action(_('Save'), style='primary')
1852    def save(self, **data):
1853        self.applyData(self.context, **data)
1854        self.flash(_('Clearance form has been saved.'))
1855        return
1856
1857    def dataNotComplete(self):
1858        """To be implemented in the customization package.
1859        """
1860        return False
1861
1862    @action(_('Save and request clearance'), style='primary')
1863    def requestClearance(self, **data):
1864        self.applyData(self.context, **data)
1865        if self.dataNotComplete():
1866            self.flash(self.dataNotComplete())
1867            return
1868        self.flash(_('Clearance form has been saved.'))
1869        if self.context.clr_code:
1870            self.redirect(self.url(self.context, 'request_clearance'))
1871        else:
1872            # We bypass the request_clearance page if student
1873            # has been imported in state 'clearance started' and
1874            # no clr_code was entered before.
1875            state = IWorkflowState(self.context).getState()
1876            if state != CLEARANCE:
1877                # This shouldn't happen, but the application officer
1878                # might have forgotten to lock the form after changing the state
1879                self.flash(_('This form cannot be submitted. Wrong state!'))
1880                return
1881            IWorkflowInfo(self.context).fireTransition('request_clearance')
1882            self.flash(_('Clearance has been requested.'))
1883            self.redirect(self.url(self.context))
1884        return
1885
1886class RequestClearancePage(KofaPage):
1887    grok.context(IStudent)
1888    grok.name('request_clearance')
1889    grok.require('waeup.handleStudent')
1890    grok.template('enterpin')
1891    label = _('Request clearance')
1892    notice = _('Enter the CLR access code used for starting clearance.')
1893    ac_prefix = 'CLR'
1894    pnav = 4
1895    buttonname = _('Request clearance now')
1896
1897    def update(self, SUBMIT=None):
1898        self.ac_series = self.request.form.get('ac_series', None)
1899        self.ac_number = self.request.form.get('ac_number', None)
1900        if SUBMIT is None:
1901            return
1902        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1903        if self.context.clr_code and self.context.clr_code != pin:
1904            self.flash(_("This isn't your CLR access code."))
1905            return
1906        state = IWorkflowState(self.context).getState()
1907        if state != CLEARANCE:
1908            # This shouldn't happen, but the application officer
1909            # might have forgotten to lock the form after changing the state
1910            self.flash(_('This form cannot be submitted. Wrong state!'))
1911            return
1912        IWorkflowInfo(self.context).fireTransition('request_clearance')
1913        self.flash(_('Clearance has been requested.'))
1914        self.redirect(self.url(self.context))
1915        return
1916
1917class StartSessionPage(KofaPage):
1918    grok.context(IStudentStudyCourse)
1919    grok.name('start_session')
1920    grok.require('waeup.handleStudent')
1921    grok.template('enterpin')
1922    label = _('Start session')
1923    ac_prefix = 'SFE'
1924    notice = ''
1925    pnav = 4
1926    buttonname = _('Start now')
1927
1928    def update(self, SUBMIT=None):
1929        if not self.context.is_current:
1930            emit_lock_message(self)
1931            return
1932        super(StartSessionPage, self).update()
1933        if not self.context.next_session_allowed:
1934            self.flash(_("You are not entitled to start session."))
1935            self.redirect(self.url(self.context))
1936            return
1937        self.ac_series = self.request.form.get('ac_series', None)
1938        self.ac_number = self.request.form.get('ac_number', None)
1939
1940        if SUBMIT is None:
1941            return
1942        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1943        code = get_access_code(pin)
1944        if not code:
1945            self.flash(_('Activation code is invalid.'))
1946            return
1947        # Mark pin as used (this also fires a pin related transition)
1948        if code.state == USED:
1949            self.flash(_('Activation code has already been used.'))
1950            return
1951        else:
1952            comment = _(u"invalidated")
1953            # Here we know that the ac is in state initialized so we do not
1954            # expect an error, but the owner might be different
1955            if not invalidate_accesscode(
1956                pin,comment,self.context.student.student_id):
1957                self.flash(_('You are not the owner of this access code.'))
1958                return
1959        if self.context.student.state == CLEARED:
1960            IWorkflowInfo(self.context.student).fireTransition(
1961                'pay_first_school_fee')
1962        elif self.context.student.state == RETURNING:
1963            IWorkflowInfo(self.context.student).fireTransition(
1964                'pay_school_fee')
1965        elif self.context.student.state == PAID:
1966            IWorkflowInfo(self.context.student).fireTransition(
1967                'pay_pg_fee')
1968        self.flash(_('Session started.'))
1969        self.redirect(self.url(self.context))
1970        return
1971
1972class AddStudyLevelFormPage(KofaEditFormPage):
1973    """ Page for students to add current study levels
1974    """
1975    grok.context(IStudentStudyCourse)
1976    grok.name('add')
1977    grok.require('waeup.handleStudent')
1978    grok.template('studyleveladdpage')
1979    form_fields = grok.AutoFields(IStudentStudyCourse)
1980    pnav = 4
1981
1982    @property
1983    def label(self):
1984        studylevelsource = StudyLevelSource().factory
1985        code = self.context.current_level
1986        title = studylevelsource.getTitle(self.context, code)
1987        return _('Add current level ${a}', mapping = {'a':title})
1988
1989    def update(self):
1990        if not self.context.is_current:
1991            emit_lock_message(self)
1992            return
1993        if self.context.student.state != PAID:
1994            emit_lock_message(self)
1995            return
1996        super(AddStudyLevelFormPage, self).update()
1997        return
1998
1999    @action(_('Create course list now'), style='primary')
2000    def addStudyLevel(self, **data):
2001        studylevel = createObject(u'waeup.StudentStudyLevel')
2002        studylevel.level = self.context.current_level
2003        studylevel.level_session = self.context.current_session
2004        try:
2005            self.context.addStudentStudyLevel(
2006                self.context.certificate,studylevel)
2007        except KeyError:
2008            self.flash(_('This level exists.'))
2009        self.redirect(self.url(self.context))
2010        return
2011
2012class StudyLevelEditFormPage(KofaEditFormPage):
2013    """ Page to edit the student study level data by students
2014    """
2015    grok.context(IStudentStudyLevel)
2016    grok.name('edit')
2017    grok.require('waeup.handleStudent')
2018    grok.template('studyleveleditpage')
2019    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2020        'level_session', 'level_verdict')
2021    pnav = 4
2022    max_credits = 50
2023
2024    def update(self):
2025        if not self.context.__parent__.is_current:
2026            emit_lock_message(self)
2027            return
2028        if self.context.student.state != PAID or \
2029            not self.context.is_current_level:
2030            emit_lock_message(self)
2031            return
2032        super(StudyLevelEditFormPage, self).update()
2033        datatable.need()
2034        warning.need()
2035        return
2036
2037    @property
2038    def label(self):
2039        # Here we know that the cookie has been set
2040        lang = self.request.cookies.get('kofa.language')
2041        level_title = translate(self.context.level_title, 'waeup.kofa',
2042            target_language=lang)
2043        return _('Edit course list of ${a}',
2044            mapping = {'a':level_title})
2045
2046    @property
2047    def total_credits(self):
2048        total_credits = 0
2049        for key, val in self.context.items():
2050            total_credits += val.credits
2051        return total_credits
2052
2053    @property
2054    def translated_values(self):
2055        return translated_values(self)
2056
2057    @action(_('Add course ticket'))
2058    def addCourseTicket(self, **data):
2059        self.redirect(self.url(self.context, 'ctadd'))
2060
2061    def _delCourseTicket(self, **data):
2062        form = self.request.form
2063        if form.has_key('val_id'):
2064            child_id = form['val_id']
2065        else:
2066            self.flash(_('No ticket selected.'))
2067            self.redirect(self.url(self.context, '@@edit'))
2068            return
2069        if not isinstance(child_id, list):
2070            child_id = [child_id]
2071        deleted = []
2072        for id in child_id:
2073            # Students are not allowed to remove core tickets
2074            if not self.context[id].mandatory:
2075                del self.context[id]
2076                deleted.append(id)
2077        if len(deleted):
2078            self.flash(_('Successfully removed: ${a}',
2079                mapping = {'a':', '.join(deleted)}))
2080            self.context.writeLogMessage(
2081                self,'removed: %s' % ', '.join(deleted))
2082        self.redirect(self.url(self.context, u'@@edit'))
2083        return
2084
2085    @jsaction(_('Remove selected tickets'))
2086    def delCourseTicket(self, **data):
2087        self._delCourseTicket(**data)
2088        return
2089
2090    def _registerCourses(self, **data):
2091        if self.context.student.is_postgrad:
2092            self.flash(_(
2093                "You are a postgraduate student, "
2094                "your course list can't bee registered."))
2095            self.redirect(self.url(self.context))
2096            return
2097        if self.total_credits > self.max_credits:
2098            self.flash(_('Maximum credits of ${a} exceeded.',
2099                mapping = {'a':self.max_credits}))
2100            return
2101        IWorkflowInfo(self.context.student).fireTransition(
2102            'register_courses')
2103        self.flash(_('Course list has been registered.'))
2104        self.redirect(self.url(self.context))
2105        return
2106
2107    @action(_('Register course list'), style='primary')
2108    def registerCourses(self, **data):
2109        self._registerCourses(**data)
2110        return
2111
2112
2113class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2114    """Add a course ticket by student.
2115    """
2116    grok.name('ctadd')
2117    grok.require('waeup.handleStudent')
2118    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
2119        'score', 'mandatory', 'automatic', 'carry_over', 'credits', 'passmark')
2120
2121    def update(self):
2122        if self.context.student.state != PAID or \
2123            not self.context.is_current_level:
2124            emit_lock_message(self)
2125            return
2126        super(CourseTicketAddFormPage2, self).update()
2127        return
2128
2129    @action(_('Add course ticket'))
2130    def addCourseTicket(self, **data):
2131        # Safety belt
2132        if self.context.student.state != PAID:
2133            return
2134        ticket = createObject(u'waeup.CourseTicket')
2135        course = data['course']
2136        ticket.automatic = False
2137        ticket.carry_over = False
2138        try:
2139            self.context.addCourseTicket(ticket, course)
2140        except KeyError:
2141            self.flash(_('The ticket exists.'))
2142            return
2143        self.flash(_('Successfully added ${a}.',
2144            mapping = {'a':ticket.code}))
2145        self.redirect(self.url(self.context, u'@@edit'))
2146        return
2147
2148
2149class SetPasswordPage(KofaPage):
2150    grok.context(IKofaObject)
2151    grok.name('setpassword')
2152    grok.require('waeup.Anonymous')
2153    grok.template('setpassword')
2154    label = _('Set password for first-time login')
2155    ac_prefix = 'PWD'
2156    pnav = 0
2157    set_button = _('Set')
2158
2159    def update(self, SUBMIT=None):
2160        self.reg_number = self.request.form.get('reg_number', None)
2161        self.ac_series = self.request.form.get('ac_series', None)
2162        self.ac_number = self.request.form.get('ac_number', None)
2163
2164        if SUBMIT is None:
2165            return
2166        hitlist = search(query=self.reg_number,
2167            searchtype='reg_number', view=self)
2168        if not hitlist:
2169            self.flash(_('No student found.'))
2170            return
2171        if len(hitlist) != 1:   # Cannot happen but anyway
2172            self.flash(_('More than one student found.'))
2173            return
2174        student = hitlist[0].context
2175        self.student_id = student.student_id
2176        student_pw = student.password
2177        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2178        code = get_access_code(pin)
2179        if not code:
2180            self.flash(_('Access code is invalid.'))
2181            return
2182        if student_pw and pin == student.adm_code:
2183            self.flash(_(
2184                'Password has already been set. Your Student Id is ${a}',
2185                mapping = {'a':self.student_id}))
2186            return
2187        elif student_pw:
2188            self.flash(
2189                _('Password has already been set. You are using the ' +
2190                'wrong Access Code.'))
2191            return
2192        # Mark pin as used (this also fires a pin related transition)
2193        # and set student password
2194        if code.state == USED:
2195            self.flash(_('Access code has already been used.'))
2196            return
2197        else:
2198            comment = _(u"invalidated")
2199            # Here we know that the ac is in state initialized so we do not
2200            # expect an exception
2201            invalidate_accesscode(pin,comment)
2202            IUserAccount(student).setPassword(self.ac_number)
2203            student.adm_code = pin
2204        self.flash(_('Password has been set. Your Student Id is ${a}',
2205            mapping = {'a':self.student_id}))
2206        return
2207
2208class StudentRequestPasswordPage(KofaAddFormPage):
2209    """Captcha'd registration page for applicants.
2210    """
2211    grok.name('requestpw')
2212    grok.require('waeup.Anonymous')
2213    grok.template('requestpw')
2214    form_fields = grok.AutoFields(IStudentRequestPW).select(
2215        'firstname','number','email')
2216    label = _('Request password for first-time login')
2217
2218    def update(self):
2219        # Handle captcha
2220        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2221        self.captcha_result = self.captcha.verify(self.request)
2222        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2223        return
2224
2225    def _redirect(self, email, password, student_id):
2226        # Forward only email to landing page in base package.
2227        self.redirect(self.url(self.context, 'requestpw_complete',
2228            data = dict(email=email)))
2229        return
2230
2231    def _pw_used(self):
2232        # XXX: False if password has not been used. We need an extra
2233        #      attribute which remembers if student logged in.
2234        return True
2235
2236    @action(_('Send login credentials to email address'), style='primary')
2237    def get_credentials(self, **data):
2238        if not self.captcha_result.is_valid:
2239            # Captcha will display error messages automatically.
2240            # No need to flash something.
2241            return
2242        number = data.get('number','')
2243        firstname = data.get('firstname','')
2244        cat = getUtility(ICatalog, name='students_catalog')
2245        results = list(
2246            cat.searchResults(reg_number=(number, number)))
2247        if not results:
2248            results = list(
2249                cat.searchResults(matric_number=(number, number)))
2250        if results:
2251            student = results[0]
2252            if getattr(student,'firstname',None) is None:
2253                self.flash(_('An error occurred.'))
2254                return
2255            elif student.firstname.lower() != firstname.lower():
2256                # Don't tell the truth here. Anonymous must not
2257                # know that a record was found and only the firstname
2258                # verification failed.
2259                self.flash(_('No student record found.'))
2260                return
2261            elif student.password is not None and self._pw_used:
2262                self.flash(_('Your password has already been set and used. '
2263                             'Please proceed to the login page.'))
2264                return
2265            # Store email address but nothing else.
2266            student.email = data['email']
2267            notify(grok.ObjectModifiedEvent(student))
2268        else:
2269            # No record found, this is the truth.
2270            self.flash(_('No student record found.'))
2271            return
2272
2273        kofa_utils = getUtility(IKofaUtils)
2274        password = kofa_utils.genPassword()
2275        mandate = PasswordMandate()
2276        mandate.params['password'] = password
2277        mandate.params['user'] = student
2278        site = grok.getSite()
2279        site['mandates'].addMandate(mandate)
2280        # Send email with credentials
2281        args = {'mandate_id':mandate.mandate_id}
2282        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2283        url_info = u'Confirmation link: %s' % mandate_url
2284        msg = _('You have successfully requested a password for the')
2285        if kofa_utils.sendCredentials(IUserAccount(student),
2286            password, url_info, msg):
2287            email_sent = student.email
2288        else:
2289            email_sent = None
2290        self._redirect(email=email_sent, password=password,
2291            student_id=student.student_id)
2292        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2293        self.context.logger.info(
2294            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2295        return
2296
2297class StudentRequestPasswordEmailSent(KofaPage):
2298    """Landing page after successful password request.
2299
2300    """
2301    grok.name('requestpw_complete')
2302    grok.require('waeup.Public')
2303    grok.template('requestpwmailsent')
2304    label = _('Your password request was successful.')
2305
2306    def update(self, email=None, student_id=None, password=None):
2307        self.email = email
2308        self.password = password
2309        self.student_id = student_id
2310        return
Note: See TracBrowser for help on using the repository browser.