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

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

Show personal data update date.

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