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

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

Activate personal data expiration checker.

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