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

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

Show signature in footer on clearance slip.

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