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

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

Fix docstrings.

In my old ZODB which I used for testing there were orphaned catalog entries. I don't know why. The tests show clearly that the catalog is updated whenever a hostel is removed or cleared. However, to be 100% that nothing goes wrong I added some extra safety belts.

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