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

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

Rename clearance_comment to officer_comment. That's more appropriate since the comment can also be used by other officers.

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