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

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

Print signature space only in state cleared. Comment by med:

"and that should even only be after clearing a student so we eliminate the problem of clearance officers
print and sign eligibility forms for students without clicking on cleared"

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