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

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

Unghostify dict.

  • Property svn:keywords set to Id
File size: 81.7 KB
Line 
1## $Id: browser.py 9328 2012-10-12 05:03:45Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for students and related components.
19"""
20import sys
21import grok
22from urllib import urlencode
23from datetime import datetime
24from 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.redirect(self.url(self.context, u'@@manage')+'?tab2')
858        return
859
860class StudentTransferFormPage(KofaAddFormPage):
861    """Page to transfer the student.
862    """
863    grok.context(IStudent)
864    grok.name('transfer')
865    grok.require('waeup.manageStudent')
866    label = _('Transfer student')
867    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
868        'entry_mode', 'entry_session')
869    pnav = 4
870
871    def update(self):
872        super(StudentTransferFormPage, self).update()
873        warning.need()
874        return
875
876    @jsaction(_('Transfer'))
877    def transferStudent(self, **data):
878        error = self.context.transfer(**data)
879        if error == -1:
880            self.flash(_('Current level does not match certificate levels.'))
881        elif error == -2:
882            self.flash(_('Former study course record incomplete.'))
883        elif error == -3:
884            self.flash(_('Maximum number of transfers exceeded.'))
885        else:
886            self.flash(_('Successfully transferred.'))
887        return
888
889class StudyLevelDisplayFormPage(KofaDisplayFormPage):
890    """ Page to display student study levels
891    """
892    grok.context(IStudentStudyLevel)
893    grok.name('index')
894    grok.require('waeup.viewStudent')
895    form_fields = grok.AutoFields(IStudentStudyLevel)
896    form_fields[
897        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
898    grok.template('studylevelpage')
899    pnav = 4
900
901    def update(self):
902        super(StudyLevelDisplayFormPage, self).update()
903        datatable.need()
904        return
905
906    @property
907    def translated_values(self):
908        return translated_values(self)
909
910    @property
911    def label(self):
912        # Here we know that the cookie has been set
913        lang = self.request.cookies.get('kofa.language')
914        level_title = translate(self.context.level_title, 'waeup.kofa',
915            target_language=lang)
916        return _('${a}: Study Level ${b}', mapping = {
917            'a':self.context.student.display_fullname,
918            'b':level_title})
919
920    @property
921    def total_credits(self):
922        total_credits = 0
923        for key, val in self.context.items():
924            total_credits += val.credits
925        return total_credits
926
927class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
928    """Deliver a PDF slip of the context.
929    """
930    grok.context(IStudentStudyLevel)
931    grok.name('course_registration.pdf')
932    grok.require('waeup.viewStudent')
933    form_fields = grok.AutoFields(IStudentStudyLevel)
934    prefix = 'form'
935
936    @property
937    def title(self):
938        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
939        return translate(_('Level Data'), 'waeup.kofa',
940            target_language=portal_language)
941
942    @property
943    def content_title(self):
944        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
945        return translate(_('Course List'), 'waeup.kofa',
946            target_language=portal_language)
947
948    @property
949    def label(self):
950        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
951        lang = self.request.cookies.get('kofa.language', portal_language)
952        level_title = translate(self.context.level_title, 'waeup.kofa',
953            target_language=lang)
954        return translate(_('Course Registration Slip'),
955            'waeup.kofa', target_language=portal_language) \
956            + ' %s' % level_title
957
958    def render(self):
959        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
960        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
961        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
962        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
963        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
964        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
965        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
966        Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
967        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
968        studentview = StudentBasePDFFormPage(self.context.student,
969            self.request)
970        students_utils = getUtility(IStudentsUtils)
971        tabledata = sorted(self.context.values(),
972            key=lambda value: str(value.semester) + value.code)
973        return students_utils.renderPDF(
974            self, 'course_registration.pdf',
975            self.context.student, studentview,
976            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
977                         (Title,'title', 5),
978                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
979                         (Cred, 'credits', 1.5),
980                         (Mand, 'mandatory', 1.5),
981                         (Score, 'score', 1.5),
982                         #('Auto', 'automatic', 1.5)
983                         ],
984            tabledata=tabledata)
985
986class StudyLevelManageFormPage(KofaEditFormPage):
987    """ Page to edit the student study level data
988    """
989    grok.context(IStudentStudyLevel)
990    grok.name('manage')
991    grok.require('waeup.manageStudent')
992    grok.template('studylevelmanagepage')
993    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
994        'validation_date', 'validated_by')
995    pnav = 4
996    taboneactions = [_('Save'),_('Cancel')]
997    tabtwoactions = [_('Add course ticket'),
998        _('Remove selected tickets'),_('Cancel')]
999
1000    def update(self):
1001        if not self.context.__parent__.is_current:
1002            emit_lock_message(self)
1003            return
1004        super(StudyLevelManageFormPage, self).update()
1005        tabs.need()
1006        self.tab1 = self.tab2 = ''
1007        qs = self.request.get('QUERY_STRING', '')
1008        if not qs:
1009            qs = 'tab1'
1010        setattr(self, qs, 'active')
1011        warning.need()
1012        datatable.need()
1013        return
1014
1015    @property
1016    def translated_values(self):
1017        import pdb; pdb.set_trace()
1018        return translated_values(self)
1019
1020    @property
1021    def label(self):
1022        # Here we know that the cookie has been set
1023        lang = self.request.cookies.get('kofa.language')
1024        level_title = translate(self.context.level_title, 'waeup.kofa',
1025            target_language=lang)
1026        return _('Manage study level ${a}',
1027            mapping = {'a':level_title})
1028
1029    @action(_('Save'), style='primary')
1030    def save(self, **data):
1031        msave(self, **data)
1032        return
1033
1034    @action(_('Add course ticket'))
1035    def addCourseTicket(self, **data):
1036        self.redirect(self.url(self.context, '@@add'))
1037
1038    @jsaction(_('Remove selected tickets'))
1039    def delCourseTicket(self, **data):
1040        form = self.request.form
1041        if form.has_key('val_id'):
1042            child_id = form['val_id']
1043        else:
1044            self.flash(_('No ticket selected.'))
1045            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1046            return
1047        if not isinstance(child_id, list):
1048            child_id = [child_id]
1049        deleted = []
1050        for id in child_id:
1051            del self.context[id]
1052            deleted.append(id)
1053        if len(deleted):
1054            self.flash(_('Successfully removed: ${a}',
1055                mapping = {'a':', '.join(deleted)}))
1056        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1057        return
1058
1059class ValidateCoursesPage(UtilityView, grok.View):
1060    """ Validate course list by course adviser
1061    """
1062    grok.context(IStudentStudyLevel)
1063    grok.name('validate_courses')
1064    grok.require('waeup.validateStudent')
1065
1066    def update(self):
1067        if not self.context.__parent__.is_current:
1068            emit_lock_message(self)
1069            return
1070        if str(self.context.__parent__.current_level) != self.context.__name__:
1071            self.flash(_('This level does not correspond current level.'))
1072        elif self.context.student.state == REGISTERED:
1073            IWorkflowInfo(self.context.student).fireTransition(
1074                'validate_courses')
1075            self.flash(_('Course list has been validated.'))
1076        else:
1077            self.flash(_('Student is in the wrong state.'))
1078        self.redirect(self.url(self.context))
1079        return
1080
1081    def render(self):
1082        return
1083
1084class RejectCoursesPage(UtilityView, grok.View):
1085    """ Reject course list by course adviser
1086    """
1087    grok.context(IStudentStudyLevel)
1088    grok.name('reject_courses')
1089    grok.require('waeup.validateStudent')
1090
1091    def update(self):
1092        if not self.context.__parent__.is_current:
1093            emit_lock_message(self)
1094            return
1095        if str(self.context.__parent__.current_level) != self.context.__name__:
1096            self.flash(_('This level does not correspond current level.'))
1097            self.redirect(self.url(self.context))
1098            return
1099        elif self.context.student.state == VALIDATED:
1100            IWorkflowInfo(self.context.student).fireTransition('reset8')
1101            message = _('Course list request has been annulled.')
1102            self.flash(message)
1103        elif self.context.student.state == REGISTERED:
1104            IWorkflowInfo(self.context.student).fireTransition('reset7')
1105            message = _('Course list request has been rejected:')
1106            self.flash(message)
1107        else:
1108            self.flash(_('Student is in the wrong state.'))
1109            self.redirect(self.url(self.context))
1110            return
1111        args = {'subject':message}
1112        self.redirect(self.url(self.context.student) +
1113            '/contactstudent?%s' % urlencode(args))
1114        return
1115
1116    def render(self):
1117        return
1118
1119class CourseTicketAddFormPage(KofaAddFormPage):
1120    """Add a course ticket.
1121    """
1122    grok.context(IStudentStudyLevel)
1123    grok.name('add')
1124    grok.require('waeup.manageStudent')
1125    label = _('Add course ticket')
1126    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1127        'score', 'automatic', 'carry_over', 'credits', 'passmark')
1128    pnav = 4
1129
1130    def update(self):
1131        if not self.context.__parent__.is_current:
1132            emit_lock_message(self)
1133            return
1134        super(CourseTicketAddFormPage, self).update()
1135        return
1136
1137    @action(_('Add course ticket'))
1138    def addCourseTicket(self, **data):
1139        ticket = createObject(u'waeup.CourseTicket')
1140        course = data['course']
1141        ticket.automatic = False
1142        ticket.carry_over = False
1143        try:
1144            self.context.addCourseTicket(ticket, course)
1145        except KeyError:
1146            self.flash(_('The ticket exists.'))
1147            return
1148        self.flash(_('Successfully added ${a}.',
1149            mapping = {'a':ticket.code}))
1150        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1151        return
1152
1153    @action(_('Cancel'), validator=NullValidator)
1154    def cancel(self, **data):
1155        self.redirect(self.url(self.context))
1156
1157class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1158    """ Page to display course tickets
1159    """
1160    grok.context(ICourseTicket)
1161    grok.name('index')
1162    grok.require('waeup.viewStudent')
1163    form_fields = grok.AutoFields(ICourseTicket)
1164    grok.template('courseticketpage')
1165    pnav = 4
1166
1167    @property
1168    def label(self):
1169        return _('${a}: Course Ticket ${b}', mapping = {
1170            'a':self.context.student.display_fullname,
1171            'b':self.context.code})
1172
1173class CourseTicketManageFormPage(KofaEditFormPage):
1174    """ Page to manage course tickets
1175    """
1176    grok.context(ICourseTicket)
1177    grok.name('manage')
1178    grok.require('waeup.manageStudent')
1179    form_fields = grok.AutoFields(ICourseTicket).omit('credits', 'passmark')
1180    grok.template('courseticketmanagepage')
1181    pnav = 4
1182
1183    @property
1184    def label(self):
1185        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1186
1187    @action('Save', style='primary')
1188    def save(self, **data):
1189        msave(self, **data)
1190        return
1191
1192class PaymentsManageFormPage(KofaEditFormPage):
1193    """ Page to manage the student payments
1194
1195    This manage form page is for both students and students officers.
1196    """
1197    grok.context(IStudentPaymentsContainer)
1198    grok.name('index')
1199    grok.require('waeup.payStudent')
1200    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1201    grok.template('paymentsmanagepage')
1202    pnav = 4
1203
1204    def unremovable(self, ticket):
1205        usertype = getattr(self.request.principal, 'user_type', None)
1206        if not usertype:
1207            return False
1208        return (self.request.principal.user_type == 'student' and ticket.r_code)
1209
1210    @property
1211    def label(self):
1212        return _('${a}: Payments',
1213            mapping = {'a':self.context.__parent__.display_fullname})
1214
1215    def update(self):
1216        super(PaymentsManageFormPage, self).update()
1217        datatable.need()
1218        warning.need()
1219        return
1220
1221    @jsaction(_('Remove selected tickets'))
1222    def delPaymentTicket(self, **data):
1223        form = self.request.form
1224        if form.has_key('val_id'):
1225            child_id = form['val_id']
1226        else:
1227            self.flash(_('No payment selected.'))
1228            self.redirect(self.url(self.context))
1229            return
1230        if not isinstance(child_id, list):
1231            child_id = [child_id]
1232        deleted = []
1233        for id in child_id:
1234            # Students are not allowed to remove used payment tickets
1235            if not self.unremovable(self.context[id]):
1236                del self.context[id]
1237                deleted.append(id)
1238        if len(deleted):
1239            self.flash(_('Successfully removed: ${a}',
1240                mapping = {'a': ', '.join(deleted)}))
1241            self.context.writeLogMessage(
1242                self,'removed: %s' % ', '.join(deleted))
1243        self.redirect(self.url(self.context))
1244        return
1245
1246    @action(_('Add online payment ticket'))
1247    def addPaymentTicket(self, **data):
1248        self.redirect(self.url(self.context, '@@addop'))
1249
1250class OnlinePaymentAddFormPage(KofaAddFormPage):
1251    """ Page to add an online payment ticket
1252    """
1253    grok.context(IStudentPaymentsContainer)
1254    grok.name('addop')
1255    grok.require('waeup.payStudent')
1256    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1257        'p_category')
1258    label = _('Add online payment')
1259    pnav = 4
1260
1261    @action(_('Create ticket'), style='primary')
1262    def createTicket(self, **data):
1263        p_category = data['p_category']
1264        previous_session = data.get('p_session', None)
1265        previous_level = data.get('p_level', None)
1266        student = self.context.__parent__
1267        if p_category == 'bed_allocation' and student[
1268            'studycourse'].current_session != grok.getSite()[
1269            'hostels'].accommodation_session:
1270                self.flash(
1271                    _('Your current session does not match ' + \
1272                    'accommodation session.'))
1273                self.redirect(self.url(self.context))
1274                return
1275        students_utils = getUtility(IStudentsUtils)
1276        error, payment = students_utils.setPaymentDetails(
1277            p_category, student, previous_session, previous_level)
1278        if error is not None:
1279            self.flash(error)
1280            if 'previous session' in error:
1281                self.redirect(self.url(self.context) + '/@@addpp')
1282                return
1283            self.redirect(self.url(self.context))
1284            return
1285        self.context[payment.p_id] = payment
1286        self.flash(_('Payment ticket created.'))
1287        self.redirect(self.url(self.context))
1288        return
1289
1290class PreviousPaymentAddFormPage(OnlinePaymentAddFormPage):
1291    """ Page to add an online payment ticket for previous sessions
1292    """
1293    grok.context(IStudentPaymentsContainer)
1294    grok.name('addpp')
1295    grok.require('waeup.payStudent')
1296    form_fields = grok.AutoFields(IStudentPreviousPayment).select(
1297        'p_category', 'p_session', 'p_level')
1298    label = _('Add previous session online payment')
1299    pnav = 4
1300
1301class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1302    """ Page to view an online payment ticket
1303    """
1304    grok.context(IStudentOnlinePayment)
1305    grok.name('index')
1306    grok.require('waeup.viewStudent')
1307    form_fields = grok.AutoFields(IStudentOnlinePayment)
1308    form_fields[
1309        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1310    form_fields[
1311        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1312    pnav = 4
1313
1314    @property
1315    def label(self):
1316        return _('${a}: Online Payment Ticket ${b}', mapping = {
1317            'a':self.context.student.display_fullname,
1318            'b':self.context.p_id})
1319
1320class OnlinePaymentApprovePage(UtilityView, grok.View):
1321    """ Callback view
1322    """
1323    grok.context(IStudentOnlinePayment)
1324    grok.name('approve')
1325    grok.require('waeup.managePortal')
1326
1327    def update(self):
1328        success, msg, log = self.context.approveStudentPayment()
1329        if log is not None:
1330            self.context.writeLogMessage(self,log)
1331        self.flash(msg)
1332        return
1333
1334    def render(self):
1335        self.redirect(self.url(self.context, '@@index'))
1336        return
1337
1338class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1339    """ Approval view for students.
1340
1341    This view is used for browser tests only and
1342    must be neutralized in custom pages!
1343    """
1344
1345    grok.name('fake_approve')
1346    grok.require('waeup.payStudent')
1347
1348class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1349    """Deliver a PDF slip of the context.
1350    """
1351    grok.context(IStudentOnlinePayment)
1352    grok.name('payment_slip.pdf')
1353    grok.require('waeup.viewStudent')
1354    form_fields = grok.AutoFields(IStudentOnlinePayment)
1355    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1356    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1357    prefix = 'form'
1358    note = None
1359
1360    @property
1361    def title(self):
1362        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1363        return translate(_('Payment Data'), 'waeup.kofa',
1364            target_language=portal_language)
1365
1366    @property
1367    def label(self):
1368        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1369        return translate(_('Online Payment Slip'),
1370            'waeup.kofa', target_language=portal_language) \
1371            + ' %s' % self.context.p_id
1372
1373    def render(self):
1374        #if self.context.p_state != 'paid':
1375        #    self.flash('Ticket not yet paid.')
1376        #    self.redirect(self.url(self.context))
1377        #    return
1378        studentview = StudentBasePDFFormPage(self.context.student,
1379            self.request)
1380        students_utils = getUtility(IStudentsUtils)
1381        return students_utils.renderPDF(self, 'payment_slip.pdf',
1382            self.context.student, studentview, note=self.note)
1383
1384
1385class AccommodationManageFormPage(KofaEditFormPage):
1386    """ Page to manage bed tickets.
1387
1388    This manage form page is for both students and students officers.
1389    """
1390    grok.context(IStudentAccommodation)
1391    grok.name('index')
1392    grok.require('waeup.handleAccommodation')
1393    form_fields = grok.AutoFields(IStudentAccommodation)
1394    grok.template('accommodationmanagepage')
1395    pnav = 4
1396    officers_only_actions = [_('Remove selected')]
1397
1398    @property
1399    def label(self):
1400        return _('${a}: Accommodation',
1401            mapping = {'a':self.context.__parent__.display_fullname})
1402
1403    def update(self):
1404        super(AccommodationManageFormPage, self).update()
1405        datatable.need()
1406        warning.need()
1407        return
1408
1409    @jsaction(_('Remove selected'))
1410    def delBedTickets(self, **data):
1411        if getattr(self.request.principal, 'user_type', None) == 'student':
1412            self.flash(_('You are not allowed to remove bed tickets.'))
1413            self.redirect(self.url(self.context))
1414            return
1415        form = self.request.form
1416        if form.has_key('val_id'):
1417            child_id = form['val_id']
1418        else:
1419            self.flash(_('No bed ticket selected.'))
1420            self.redirect(self.url(self.context))
1421            return
1422        if not isinstance(child_id, list):
1423            child_id = [child_id]
1424        deleted = []
1425        for id in child_id:
1426            del self.context[id]
1427            deleted.append(id)
1428        if len(deleted):
1429            self.flash(_('Successfully removed: ${a}',
1430                mapping = {'a':', '.join(deleted)}))
1431            self.context.writeLogMessage(
1432                self,'removed: % s' % ', '.join(deleted))
1433        self.redirect(self.url(self.context))
1434        return
1435
1436    @property
1437    def selected_actions(self):
1438        if getattr(self.request.principal, 'user_type', None) == 'student':
1439            return [action for action in self.actions
1440                    if not action.label in self.officers_only_actions]
1441        return self.actions
1442
1443class BedTicketAddPage(KofaPage):
1444    """ Page to add an online payment ticket
1445    """
1446    grok.context(IStudentAccommodation)
1447    grok.name('add')
1448    grok.require('waeup.handleAccommodation')
1449    grok.template('enterpin')
1450    ac_prefix = 'HOS'
1451    label = _('Add bed ticket')
1452    pnav = 4
1453    buttonname = _('Create bed ticket')
1454    notice = ''
1455    with_ac = True
1456
1457    def update(self, SUBMIT=None):
1458        student = self.context.student
1459        students_utils = getUtility(IStudentsUtils)
1460        acc_details  = students_utils.getAccommodationDetails(student)
1461        if acc_details.get('expired', False):
1462            startdate = acc_details.get('startdate')
1463            enddate = acc_details.get('enddate')
1464            if startdate and enddate:
1465                tz = getUtility(IKofaUtils).tzinfo
1466                startdate = to_timezone(
1467                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1468                enddate = to_timezone(
1469                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1470                self.flash(_("Outside booking period: ${a} - ${b}",
1471                    mapping = {'a': startdate, 'b': enddate}))
1472            else:
1473                self.flash(_("Outside booking period."))
1474            self.redirect(self.url(self.context))
1475            return
1476        if not acc_details:
1477            self.flash(_("Your data are incomplete."))
1478            self.redirect(self.url(self.context))
1479            return
1480        if not student.state in acc_details['allowed_states']:
1481            self.flash(_("You are in the wrong registration state."))
1482            self.redirect(self.url(self.context))
1483            return
1484        if student['studycourse'].current_session != acc_details[
1485            'booking_session']:
1486            self.flash(
1487                _('Your current session does not match accommodation session.'))
1488            self.redirect(self.url(self.context))
1489            return
1490        if str(acc_details['booking_session']) in self.context.keys():
1491            self.flash(
1492                _('You already booked a bed space in current ' \
1493                    + 'accommodation session.'))
1494            self.redirect(self.url(self.context))
1495            return
1496        if self.with_ac:
1497            self.ac_series = self.request.form.get('ac_series', None)
1498            self.ac_number = self.request.form.get('ac_number', None)
1499        if SUBMIT is None:
1500            return
1501        if self.with_ac:
1502            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1503            code = get_access_code(pin)
1504            if not code:
1505                self.flash(_('Activation code is invalid.'))
1506                return
1507        # Search and book bed
1508        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1509        entries = cat.searchResults(
1510            owner=(student.student_id,student.student_id))
1511        if len(entries):
1512            # If bed space has been manually allocated use this bed
1513            bed = [entry for entry in entries][0]
1514        else:
1515            # else search for other available beds
1516            entries = cat.searchResults(
1517                bed_type=(acc_details['bt'],acc_details['bt']))
1518            available_beds = [
1519                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1520            if available_beds:
1521                students_utils = getUtility(IStudentsUtils)
1522                bed = students_utils.selectBed(available_beds)
1523                bed.bookBed(student.student_id)
1524            else:
1525                self.flash(_('There is no free bed in your category ${a}.',
1526                    mapping = {'a':acc_details['bt']}))
1527                return
1528        if self.with_ac:
1529            # Mark pin as used (this also fires a pin related transition)
1530            if code.state == USED:
1531                self.flash(_('Activation code has already been used.'))
1532                return
1533            else:
1534                comment = _(u'invalidated')
1535                # Here we know that the ac is in state initialized so we do not
1536                # expect an exception, but the owner might be different
1537                if not invalidate_accesscode(
1538                    pin,comment,self.context.student.student_id):
1539                    self.flash(_('You are not the owner of this access code.'))
1540                    return
1541        # Create bed ticket
1542        bedticket = createObject(u'waeup.BedTicket')
1543        if self.with_ac:
1544            bedticket.booking_code = pin
1545        bedticket.booking_session = acc_details['booking_session']
1546        bedticket.bed_type = acc_details['bt']
1547        bedticket.bed = bed
1548        hall_title = bed.__parent__.hostel_name
1549        coordinates = bed.coordinates[1:]
1550        block, room_nr, bed_nr = coordinates
1551        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1552            'a':hall_title, 'b':block,
1553            'c':room_nr, 'd':bed_nr,
1554            'e':bed.bed_type})
1555        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1556        bedticket.bed_coordinates = translate(
1557            bc, 'waeup.kofa',target_language=portal_language)
1558        key = str(acc_details['booking_session'])
1559        self.context[key] = bedticket
1560        self.flash(_('Bed ticket created and bed booked: ${a}',
1561            mapping = {'a':bedticket.bed_coordinates}))
1562        self.redirect(self.url(self.context))
1563        return
1564
1565class BedTicketDisplayFormPage(KofaDisplayFormPage):
1566    """ Page to display bed tickets
1567    """
1568    grok.context(IBedTicket)
1569    grok.name('index')
1570    grok.require('waeup.handleAccommodation')
1571    form_fields = grok.AutoFields(IBedTicket)
1572    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1573    pnav = 4
1574
1575    @property
1576    def label(self):
1577        return _('Bed Ticket for Session ${a}',
1578            mapping = {'a':self.context.getSessionString()})
1579
1580class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1581    """Deliver a PDF slip of the context.
1582    """
1583    grok.context(IBedTicket)
1584    grok.name('bed_allocation.pdf')
1585    grok.require('waeup.handleAccommodation')
1586    form_fields = grok.AutoFields(IBedTicket)
1587    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1588    prefix = 'form'
1589
1590    @property
1591    def title(self):
1592        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1593        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1594            target_language=portal_language)
1595
1596    @property
1597    def label(self):
1598        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1599        #return translate(_('Bed Allocation: '),
1600        #    'waeup.kofa', target_language=portal_language) \
1601        #    + ' %s' % self.context.bed_coordinates
1602        return translate(_('Bed Allocation Slip'),
1603            'waeup.kofa', target_language=portal_language) \
1604            + ' %s' % self.context.getSessionString()
1605
1606    def render(self):
1607        studentview = StudentBasePDFFormPage(self.context.student,
1608            self.request)
1609        students_utils = getUtility(IStudentsUtils)
1610        return students_utils.renderPDF(
1611            self, 'bed_allocation.pdf',
1612            self.context.student, studentview)
1613
1614class BedTicketRelocationPage(UtilityView, grok.View):
1615    """ Callback view
1616    """
1617    grok.context(IBedTicket)
1618    grok.name('relocate')
1619    grok.require('waeup.manageHostels')
1620
1621    # Relocate student if student parameters have changed or the bed_type
1622    # of the bed has changed
1623    def update(self):
1624        student = self.context.student
1625        students_utils = getUtility(IStudentsUtils)
1626        acc_details  = students_utils.getAccommodationDetails(student)
1627        if self.context.bed != None and \
1628              'reserved' in self.context.bed.bed_type:
1629            self.flash(_("Students in reserved beds can't be relocated."))
1630            self.redirect(self.url(self.context))
1631            return
1632        if acc_details['bt'] == self.context.bed_type and \
1633                self.context.bed != None and \
1634                self.context.bed.bed_type == self.context.bed_type:
1635            self.flash(_("Student can't be relocated."))
1636            self.redirect(self.url(self.context))
1637            return
1638        # Search a bed
1639        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1640        entries = cat.searchResults(
1641            owner=(student.student_id,student.student_id))
1642        if len(entries) and self.context.bed == None:
1643            # If booking has been cancelled but other bed space has been
1644            # manually allocated after cancellation use this bed
1645            new_bed = [entry for entry in entries][0]
1646        else:
1647            # Search for other available beds
1648            entries = cat.searchResults(
1649                bed_type=(acc_details['bt'],acc_details['bt']))
1650            available_beds = [
1651                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1652            if available_beds:
1653                students_utils = getUtility(IStudentsUtils)
1654                new_bed = students_utils.selectBed(available_beds)
1655                new_bed.bookBed(student.student_id)
1656            else:
1657                self.flash(_('There is no free bed in your category ${a}.',
1658                    mapping = {'a':acc_details['bt']}))
1659                self.redirect(self.url(self.context))
1660                return
1661        # Release old bed if exists
1662        if self.context.bed != None:
1663            self.context.bed.owner = NOT_OCCUPIED
1664            notify(grok.ObjectModifiedEvent(self.context.bed))
1665        # Alocate new bed
1666        self.context.bed_type = acc_details['bt']
1667        self.context.bed = new_bed
1668        hall_title = new_bed.__parent__.hostel_name
1669        coordinates = new_bed.coordinates[1:]
1670        block, room_nr, bed_nr = coordinates
1671        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1672            'a':hall_title, 'b':block,
1673            'c':room_nr, 'd':bed_nr,
1674            'e':new_bed.bed_type})
1675        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1676        self.context.bed_coordinates = translate(
1677            bc, 'waeup.kofa',target_language=portal_language)
1678        self.flash(_('Student relocated: ${a}',
1679            mapping = {'a':self.context.bed_coordinates}))
1680        self.redirect(self.url(self.context))
1681        return
1682
1683    def render(self):
1684        return
1685
1686class StudentHistoryPage(KofaPage):
1687    """ Page to display student clearance data
1688    """
1689    grok.context(IStudent)
1690    grok.name('history')
1691    grok.require('waeup.viewStudent')
1692    grok.template('studenthistory')
1693    pnav = 4
1694
1695    @property
1696    def label(self):
1697        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1698
1699# Pages for students only
1700
1701class StudentBaseEditFormPage(KofaEditFormPage):
1702    """ View to edit student base data
1703    """
1704    grok.context(IStudent)
1705    grok.name('edit_base')
1706    grok.require('waeup.handleStudent')
1707    form_fields = grok.AutoFields(IStudentBase).select(
1708        'email', 'phone')
1709    label = _('Edit base data')
1710    pnav = 4
1711
1712    @action(_('Save'), style='primary')
1713    def save(self, **data):
1714        msave(self, **data)
1715        return
1716
1717class StudentChangePasswordPage(KofaEditFormPage):
1718    """ View to manage student base data
1719    """
1720    grok.context(IStudent)
1721    grok.name('change_password')
1722    grok.require('waeup.handleStudent')
1723    grok.template('change_password')
1724    label = _('Change password')
1725    pnav = 4
1726
1727    @action(_('Save'), style='primary')
1728    def save(self, **data):
1729        form = self.request.form
1730        password = form.get('change_password', None)
1731        password_ctl = form.get('change_password_repeat', None)
1732        if password:
1733            validator = getUtility(IPasswordValidator)
1734            errors = validator.validate_password(password, password_ctl)
1735            if not errors:
1736                IUserAccount(self.context).setPassword(password)
1737                self.context.writeLogMessage(self, 'saved: password')
1738                self.flash(_('Password changed.'))
1739            else:
1740                self.flash( ' '.join(errors))
1741        return
1742
1743class StudentFilesUploadPage(KofaPage):
1744    """ View to upload files by student
1745    """
1746    grok.context(IStudent)
1747    grok.name('change_portrait')
1748    grok.require('waeup.uploadStudentFile')
1749    grok.template('filesuploadpage')
1750    label = _('Upload portrait')
1751    pnav = 4
1752
1753    def update(self):
1754        if self.context.student.state != ADMITTED:
1755            emit_lock_message(self)
1756            return
1757        super(StudentFilesUploadPage, self).update()
1758        return
1759
1760class StartClearancePage(KofaPage):
1761    grok.context(IStudent)
1762    grok.name('start_clearance')
1763    grok.require('waeup.handleStudent')
1764    grok.template('enterpin')
1765    label = _('Start clearance')
1766    ac_prefix = 'CLR'
1767    notice = ''
1768    pnav = 4
1769    buttonname = _('Start clearance now')
1770
1771    @property
1772    def all_required_fields_filled(self):
1773        if self.context.email and self.context.phone:
1774            return True
1775        return False
1776
1777    @property
1778    def portrait_uploaded(self):
1779        store = getUtility(IExtFileStore)
1780        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1781            return True
1782        return False
1783
1784    def update(self, SUBMIT=None):
1785        if not self.context.state == ADMITTED:
1786            self.flash(_("Wrong state"))
1787            self.redirect(self.url(self.context))
1788            return
1789        if not self.portrait_uploaded:
1790            self.flash(_("No portrait uploaded."))
1791            self.redirect(self.url(self.context, 'change_portrait'))
1792            return
1793        if not self.all_required_fields_filled:
1794            self.flash(_("Not all required fields filled."))
1795            self.redirect(self.url(self.context, 'edit_base'))
1796            return
1797        self.ac_series = self.request.form.get('ac_series', None)
1798        self.ac_number = self.request.form.get('ac_number', None)
1799
1800        if SUBMIT is None:
1801            return
1802        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1803        code = get_access_code(pin)
1804        if not code:
1805            self.flash(_('Activation code is invalid.'))
1806            return
1807        if code.state == USED:
1808            self.flash(_('Activation code has already been used.'))
1809            return
1810        # Mark pin as used (this also fires a pin related transition)
1811        # and fire transition start_clearance
1812        comment = _(u"invalidated")
1813        # Here we know that the ac is in state initialized so we do not
1814        # expect an exception, but the owner might be different
1815        if not invalidate_accesscode(pin, comment, self.context.student_id):
1816            self.flash(_('You are not the owner of this access code.'))
1817            return
1818        self.context.clr_code = pin
1819        IWorkflowInfo(self.context).fireTransition('start_clearance')
1820        self.flash(_('Clearance process has been started.'))
1821        self.redirect(self.url(self.context,'cedit'))
1822        return
1823
1824class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1825    """ View to edit student clearance data by student
1826    """
1827    grok.context(IStudent)
1828    grok.name('cedit')
1829    grok.require('waeup.handleStudent')
1830    label = _('Edit clearance data')
1831
1832    @property
1833    def form_fields(self):
1834        if self.context.is_postgrad:
1835            form_fields = grok.AutoFields(IPGStudentClearance).omit(
1836                'clearance_locked', 'clr_code')
1837        else:
1838            form_fields = grok.AutoFields(IUGStudentClearance).omit(
1839                'clearance_locked', 'clr_code')
1840        return form_fields
1841
1842    def update(self):
1843        if self.context.clearance_locked:
1844            emit_lock_message(self)
1845            return
1846        return super(StudentClearanceEditFormPage, self).update()
1847
1848    @action(_('Save'), style='primary')
1849    def save(self, **data):
1850        self.applyData(self.context, **data)
1851        self.flash(_('Clearance form has been saved.'))
1852        return
1853
1854    def dataNotComplete(self):
1855        """To be implemented in the customization package.
1856        """
1857        return False
1858
1859    @action(_('Save and request clearance'), style='primary')
1860    def requestClearance(self, **data):
1861        self.applyData(self.context, **data)
1862        if self.dataNotComplete():
1863            self.flash(self.dataNotComplete())
1864            return
1865        self.flash(_('Clearance form has been saved.'))
1866        if self.context.clr_code:
1867            self.redirect(self.url(self.context, 'request_clearance'))
1868        else:
1869            # We bypass the request_clearance page if student
1870            # has been imported in state 'clearance started' and
1871            # no clr_code was entered before.
1872            state = IWorkflowState(self.context).getState()
1873            if state != CLEARANCE:
1874                # This shouldn't happen, but the application officer
1875                # might have forgotten to lock the form after changing the state
1876                self.flash(_('This form cannot be submitted. Wrong state!'))
1877                return
1878            IWorkflowInfo(self.context).fireTransition('request_clearance')
1879            self.flash(_('Clearance has been requested.'))
1880            self.redirect(self.url(self.context))
1881        return
1882
1883class RequestClearancePage(KofaPage):
1884    grok.context(IStudent)
1885    grok.name('request_clearance')
1886    grok.require('waeup.handleStudent')
1887    grok.template('enterpin')
1888    label = _('Request clearance')
1889    notice = _('Enter the CLR access code used for starting clearance.')
1890    ac_prefix = 'CLR'
1891    pnav = 4
1892    buttonname = _('Request clearance now')
1893
1894    def update(self, SUBMIT=None):
1895        self.ac_series = self.request.form.get('ac_series', None)
1896        self.ac_number = self.request.form.get('ac_number', None)
1897        if SUBMIT is None:
1898            return
1899        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1900        if self.context.clr_code and self.context.clr_code != pin:
1901            self.flash(_("This isn't your CLR access code."))
1902            return
1903        state = IWorkflowState(self.context).getState()
1904        if state != CLEARANCE:
1905            # This shouldn't happen, but the application officer
1906            # might have forgotten to lock the form after changing the state
1907            self.flash(_('This form cannot be submitted. Wrong state!'))
1908            return
1909        IWorkflowInfo(self.context).fireTransition('request_clearance')
1910        self.flash(_('Clearance has been requested.'))
1911        self.redirect(self.url(self.context))
1912        return
1913
1914class StartSessionPage(KofaPage):
1915    grok.context(IStudentStudyCourse)
1916    grok.name('start_session')
1917    grok.require('waeup.handleStudent')
1918    grok.template('enterpin')
1919    label = _('Start session')
1920    ac_prefix = 'SFE'
1921    notice = ''
1922    pnav = 4
1923    buttonname = _('Start now')
1924
1925    def update(self, SUBMIT=None):
1926        if not self.context.is_current:
1927            emit_lock_message(self)
1928            return
1929        super(StartSessionPage, self).update()
1930        if not self.context.next_session_allowed:
1931            self.flash(_("You are not entitled to start session."))
1932            self.redirect(self.url(self.context))
1933            return
1934        self.ac_series = self.request.form.get('ac_series', None)
1935        self.ac_number = self.request.form.get('ac_number', None)
1936
1937        if SUBMIT is None:
1938            return
1939        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1940        code = get_access_code(pin)
1941        if not code:
1942            self.flash(_('Activation code is invalid.'))
1943            return
1944        # Mark pin as used (this also fires a pin related transition)
1945        if code.state == USED:
1946            self.flash(_('Activation code has already been used.'))
1947            return
1948        else:
1949            comment = _(u"invalidated")
1950            # Here we know that the ac is in state initialized so we do not
1951            # expect an error, but the owner might be different
1952            if not invalidate_accesscode(
1953                pin,comment,self.context.student.student_id):
1954                self.flash(_('You are not the owner of this access code.'))
1955                return
1956        if self.context.student.state == CLEARED:
1957            IWorkflowInfo(self.context.student).fireTransition(
1958                'pay_first_school_fee')
1959        elif self.context.student.state == RETURNING:
1960            IWorkflowInfo(self.context.student).fireTransition(
1961                'pay_school_fee')
1962        elif self.context.student.state == PAID:
1963            IWorkflowInfo(self.context.student).fireTransition(
1964                'pay_pg_fee')
1965        self.flash(_('Session started.'))
1966        self.redirect(self.url(self.context))
1967        return
1968
1969class AddStudyLevelFormPage(KofaEditFormPage):
1970    """ Page for students to add current study levels
1971    """
1972    grok.context(IStudentStudyCourse)
1973    grok.name('add')
1974    grok.require('waeup.handleStudent')
1975    grok.template('studyleveladdpage')
1976    form_fields = grok.AutoFields(IStudentStudyCourse)
1977    pnav = 4
1978
1979    @property
1980    def label(self):
1981        studylevelsource = StudyLevelSource().factory
1982        code = self.context.current_level
1983        title = studylevelsource.getTitle(self.context, code)
1984        return _('Add current level ${a}', mapping = {'a':title})
1985
1986    def update(self):
1987        if not self.context.is_current:
1988            emit_lock_message(self)
1989            return
1990        if self.context.student.state != PAID:
1991            emit_lock_message(self)
1992            return
1993        super(AddStudyLevelFormPage, self).update()
1994        return
1995
1996    @action(_('Create course list now'), style='primary')
1997    def addStudyLevel(self, **data):
1998        studylevel = createObject(u'waeup.StudentStudyLevel')
1999        studylevel.level = self.context.current_level
2000        studylevel.level_session = self.context.current_session
2001        try:
2002            self.context.addStudentStudyLevel(
2003                self.context.certificate,studylevel)
2004        except KeyError:
2005            self.flash(_('This level exists.'))
2006        self.redirect(self.url(self.context))
2007        return
2008
2009class StudyLevelEditFormPage(KofaEditFormPage):
2010    """ Page to edit the student study level data by students
2011    """
2012    grok.context(IStudentStudyLevel)
2013    grok.name('edit')
2014    grok.require('waeup.handleStudent')
2015    grok.template('studyleveleditpage')
2016    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2017        'level_session', 'level_verdict')
2018    pnav = 4
2019    max_credits = 50
2020
2021    def update(self):
2022        if not self.context.__parent__.is_current:
2023            emit_lock_message(self)
2024            return
2025        if self.context.student.state != PAID or \
2026            not self.context.is_current_level:
2027            emit_lock_message(self)
2028            return
2029        super(StudyLevelEditFormPage, self).update()
2030        datatable.need()
2031        warning.need()
2032        return
2033
2034    @property
2035    def label(self):
2036        # Here we know that the cookie has been set
2037        lang = self.request.cookies.get('kofa.language')
2038        level_title = translate(self.context.level_title, 'waeup.kofa',
2039            target_language=lang)
2040        return _('Edit course list of ${a}',
2041            mapping = {'a':level_title})
2042
2043    @property
2044    def total_credits(self):
2045        total_credits = 0
2046        for key, val in self.context.items():
2047            total_credits += val.credits
2048        return total_credits
2049
2050    @property
2051    def translated_values(self):
2052        return translated_values(self)
2053
2054    @action(_('Add course ticket'))
2055    def addCourseTicket(self, **data):
2056        self.redirect(self.url(self.context, 'ctadd'))
2057
2058    def _delCourseTicket(self, **data):
2059        form = self.request.form
2060        if form.has_key('val_id'):
2061            child_id = form['val_id']
2062        else:
2063            self.flash(_('No ticket selected.'))
2064            self.redirect(self.url(self.context, '@@edit'))
2065            return
2066        if not isinstance(child_id, list):
2067            child_id = [child_id]
2068        deleted = []
2069        for id in child_id:
2070            # Students are not allowed to remove core tickets
2071            if not self.context[id].mandatory:
2072                del self.context[id]
2073                deleted.append(id)
2074        if len(deleted):
2075            self.flash(_('Successfully removed: ${a}',
2076                mapping = {'a':', '.join(deleted)}))
2077        self.redirect(self.url(self.context, u'@@edit'))
2078        return
2079
2080    @jsaction(_('Remove selected tickets'))
2081    def delCourseTicket(self, **data):
2082        self._delCourseTicket(**data)
2083        return
2084
2085    def _registerCourses(self, **data):
2086        if self.context.student.is_postgrad:
2087            self.flash(_(
2088                "You are a postgraduate student, "
2089                "your course list can't bee registered."))
2090            self.redirect(self.url(self.context))
2091            return
2092        if self.total_credits > self.max_credits:
2093            self.flash(_('Maximum credits of ${a} exceeded.',
2094                mapping = {'a':self.max_credits}))
2095            return
2096        IWorkflowInfo(self.context.student).fireTransition(
2097            'register_courses')
2098        self.flash(_('Course list has been registered.'))
2099        self.redirect(self.url(self.context))
2100        return
2101
2102    @action(_('Register course list'), style='primary')
2103    def registerCourses(self, **data):
2104        self._registerCourses(**data)
2105        return
2106
2107
2108class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2109    """Add a course ticket by student.
2110    """
2111    grok.name('ctadd')
2112    grok.require('waeup.handleStudent')
2113    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
2114        'score', 'mandatory', 'automatic', 'carry_over', 'credits', 'passmark')
2115
2116    def update(self):
2117        if self.context.student.state != PAID or \
2118            not self.context.is_current_level:
2119            emit_lock_message(self)
2120            return
2121        super(CourseTicketAddFormPage2, self).update()
2122        return
2123
2124    @action(_('Add course ticket'))
2125    def addCourseTicket(self, **data):
2126        # Safety belt
2127        if self.context.student.state != PAID:
2128            return
2129        ticket = createObject(u'waeup.CourseTicket')
2130        course = data['course']
2131        ticket.automatic = False
2132        ticket.carry_over = False
2133        try:
2134            self.context.addCourseTicket(ticket, course)
2135        except KeyError:
2136            self.flash(_('The ticket exists.'))
2137            return
2138        self.flash(_('Successfully added ${a}.',
2139            mapping = {'a':ticket.code}))
2140        self.redirect(self.url(self.context, u'@@edit'))
2141        return
2142
2143
2144class SetPasswordPage(KofaPage):
2145    grok.context(IKofaObject)
2146    grok.name('setpassword')
2147    grok.require('waeup.Anonymous')
2148    grok.template('setpassword')
2149    label = _('Set password for first-time login')
2150    ac_prefix = 'PWD'
2151    pnav = 0
2152    set_button = _('Set')
2153
2154    def update(self, SUBMIT=None):
2155        self.reg_number = self.request.form.get('reg_number', None)
2156        self.ac_series = self.request.form.get('ac_series', None)
2157        self.ac_number = self.request.form.get('ac_number', None)
2158
2159        if SUBMIT is None:
2160            return
2161        hitlist = search(query=self.reg_number,
2162            searchtype='reg_number', view=self)
2163        if not hitlist:
2164            self.flash(_('No student found.'))
2165            return
2166        if len(hitlist) != 1:   # Cannot happen but anyway
2167            self.flash(_('More than one student found.'))
2168            return
2169        student = hitlist[0].context
2170        self.student_id = student.student_id
2171        student_pw = student.password
2172        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2173        code = get_access_code(pin)
2174        if not code:
2175            self.flash(_('Access code is invalid.'))
2176            return
2177        if student_pw and pin == student.adm_code:
2178            self.flash(_(
2179                'Password has already been set. Your Student Id is ${a}',
2180                mapping = {'a':self.student_id}))
2181            return
2182        elif student_pw:
2183            self.flash(
2184                _('Password has already been set. You are using the ' +
2185                'wrong Access Code.'))
2186            return
2187        # Mark pin as used (this also fires a pin related transition)
2188        # and set student password
2189        if code.state == USED:
2190            self.flash(_('Access code has already been used.'))
2191            return
2192        else:
2193            comment = _(u"invalidated")
2194            # Here we know that the ac is in state initialized so we do not
2195            # expect an exception
2196            invalidate_accesscode(pin,comment)
2197            IUserAccount(student).setPassword(self.ac_number)
2198            student.adm_code = pin
2199        self.flash(_('Password has been set. Your Student Id is ${a}',
2200            mapping = {'a':self.student_id}))
2201        return
2202
2203class StudentRequestPasswordPage(KofaAddFormPage):
2204    """Captcha'd registration page for applicants.
2205    """
2206    grok.name('requestpw')
2207    grok.require('waeup.Anonymous')
2208    grok.template('requestpw')
2209    form_fields = grok.AutoFields(IStudentRequestPW).select(
2210        'firstname','number','email')
2211    label = _('Request password for first-time login')
2212
2213    def update(self):
2214        # Handle captcha
2215        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2216        self.captcha_result = self.captcha.verify(self.request)
2217        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2218        return
2219
2220    def _redirect(self, email, password, student_id):
2221        # Forward only email to landing page in base package.
2222        self.redirect(self.url(self.context, 'requestpw_complete',
2223            data = dict(email=email)))
2224        return
2225
2226    def _pw_used(self):
2227        # XXX: False if password has not been used. We need an extra
2228        #      attribute which remembers if student logged in.
2229        return True
2230
2231    @action(_('Send login credentials to email address'), style='primary')
2232    def get_credentials(self, **data):
2233        if not self.captcha_result.is_valid:
2234            # Captcha will display error messages automatically.
2235            # No need to flash something.
2236            return
2237        number = data.get('number','')
2238        firstname = data.get('firstname','')
2239        cat = getUtility(ICatalog, name='students_catalog')
2240        results = list(
2241            cat.searchResults(reg_number=(number, number)))
2242        if not results:
2243            results = list(
2244                cat.searchResults(matric_number=(number, number)))
2245        if results:
2246            student = results[0]
2247            if getattr(student,'firstname',None) is None:
2248                self.flash(_('An error occurred.'))
2249                return
2250            elif student.firstname.lower() != firstname.lower():
2251                # Don't tell the truth here. Anonymous must not
2252                # know that a record was found and only the firstname
2253                # verification failed.
2254                self.flash(_('No student record found.'))
2255                return
2256            elif student.password is not None and self._pw_used:
2257                self.flash(_('Your password has already been set and used. '
2258                             'Please proceed to the login page.'))
2259                return
2260            # Store email address but nothing else.
2261            student.email = data['email']
2262            notify(grok.ObjectModifiedEvent(student))
2263        else:
2264            # No record found, this is the truth.
2265            self.flash(_('No student record found.'))
2266            return
2267
2268        kofa_utils = getUtility(IKofaUtils)
2269        password = kofa_utils.genPassword()
2270        mandate = PasswordMandate()
2271        mandate.params['password'] = password
2272        mandate.params['user'] = student
2273        site = grok.getSite()
2274        site['mandates'].addMandate(mandate)
2275        # Send email with credentials
2276        args = {'mandate_id':mandate.mandate_id}
2277        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2278        url_info = u'Confirmation link: %s' % mandate_url
2279        msg = _('You have successfully requested a password for the')
2280        if kofa_utils.sendCredentials(IUserAccount(student),
2281            password, url_info, msg):
2282            email_sent = student.email
2283        else:
2284            email_sent = None
2285        self._redirect(email=email_sent, password=password,
2286            student_id=student.student_id)
2287        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2288        self.context.logger.info(
2289            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2290        return
2291
2292class StudentRequestPasswordEmailSent(KofaPage):
2293    """Landing page after successful password request.
2294
2295    """
2296    grok.name('requestpw_complete')
2297    grok.require('waeup.Public')
2298    grok.template('requestpwmailsent')
2299    label = _('Your password request was successful.')
2300
2301    def update(self, email=None, student_id=None, password=None):
2302        self.email = email
2303        self.password = password
2304        self.student_id = student_id
2305        return
Note: See TracBrowser for help on using the repository browser.