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

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

_p_activate is a method.

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