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

Last change on this file since 7876 was 7875, checked in by Henrik Bettermann, 13 years ago

Surcharge 1 is portal provider fee.

Show data on goto_interswitch page.

  • Property svn:keywords set to Id
File size: 71.8 KB
Line 
1## $Id: browser.py 7875 2012-03-14 07:52:17Z 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 time import time
24from datetime import datetime
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.formlib.textwidgets import BytesDisplayWidget
30from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
31from waeup.kofa.accesscodes import (
32    invalidate_accesscode, get_access_code, create_accesscode)
33from waeup.kofa.accesscodes.workflow import USED
34from waeup.kofa.browser import (
35    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
36    ContactAdminForm, KofaForm, NullValidator)
37from waeup.kofa.browser.interfaces import ICaptchaManager
38from waeup.kofa.browser.breadcrumbs import Breadcrumb
39from waeup.kofa.browser.resources import datepicker, datatable, tabs, warning
40from waeup.kofa.browser.layout import jsaction, action, UtilityView
41from waeup.kofa.interfaces import (
42    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
43    IKofaUtils, IUniversity)
44from waeup.kofa.interfaces import MessageFactory as _
45from waeup.kofa.widgets.datewidget import (
46    FriendlyDateWidget, FriendlyDateDisplayWidget,
47    FriendlyDatetimeDisplayWidget)
48from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
49from waeup.kofa.widgets.phonewidget import PhoneWidget
50from waeup.kofa.students.interfaces import (
51    IStudentsContainer, IStudent, IStudentClearance,
52    IStudentPersonal, IStudentBase, IStudentStudyCourse,
53    IStudentAccommodation, IStudentStudyLevel,
54    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
55    IStudentOnlinePayment, IBedTicket, IStudentsUtils, IStudentChangePassword
56    )
57from waeup.kofa.students.catalog import search
58from waeup.kofa.students.workflow import (ADMITTED, PAID,
59    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED)
60from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
61from waeup.kofa.students.vocabularies import StudyLevelSource
62from waeup.kofa.browser.resources import toggleall
63from waeup.kofa.hostels.hostel import NOT_OCCUPIED
64from waeup.kofa.utils.helpers import get_current_principal
65
66def write_log_message(view, message):
67    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
68    view.context.getStudent().loggerInfo(ob_class, message)
69    return
70
71# Save function used for save methods in pages
72def msave(view, **data):
73    changed_fields = view.applyData(view.context, **data)
74    # Turn list of lists into single list
75    if changed_fields:
76        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
77    # Inform catalog if certificate has changed
78    # (applyData does this only for the context)
79    if 'certificate' in changed_fields:
80        notify(grok.ObjectModifiedEvent(view.context.getStudent()))
81    fields_string = ' + '.join(changed_fields)
82    view.flash(_('Form has been saved.'))
83    if fields_string:
84        write_log_message(view, 'saved: %s' % fields_string)
85    return
86
87def emit_lock_message(view):
88    """Flash a lock message.
89    """
90    view.flash(_('The requested form is locked (read-only).'))
91    view.redirect(view.url(view.context))
92    return
93
94class StudentsBreadcrumb(Breadcrumb):
95    """A breadcrumb for the students container.
96    """
97    grok.context(IStudentsContainer)
98    title = _('Students')
99
100    @property
101    def target(self):
102        user = get_current_principal()
103        if getattr(user, 'user_type', None) == 'student':
104            return None
105        return self.viewname
106
107class StudentBreadcrumb(Breadcrumb):
108    """A breadcrumb for the student container.
109    """
110    grok.context(IStudent)
111
112    def title(self):
113        return self.context.display_fullname
114
115class SudyCourseBreadcrumb(Breadcrumb):
116    """A breadcrumb for the student study course.
117    """
118    grok.context(IStudentStudyCourse)
119    title = _('Study Course')
120
121class PaymentsBreadcrumb(Breadcrumb):
122    """A breadcrumb for the student payments folder.
123    """
124    grok.context(IStudentPaymentsContainer)
125    title = _('Payments')
126
127class OnlinePaymentBreadcrumb(Breadcrumb):
128    """A breadcrumb for payments.
129    """
130    grok.context(IStudentOnlinePayment)
131
132    @property
133    def title(self):
134        return self.context.p_id
135
136class AccommodationBreadcrumb(Breadcrumb):
137    """A breadcrumb for the student accommodation folder.
138    """
139    grok.context(IStudentAccommodation)
140    title = _('Accommodation')
141
142class BedTicketBreadcrumb(Breadcrumb):
143    """A breadcrumb for bed tickets.
144    """
145    grok.context(IBedTicket)
146
147    @property
148    def title(self):
149        return _('Bed Ticket ${a}',
150            mapping = {'a':self.context.getSessionString()})
151
152class StudyLevelBreadcrumb(Breadcrumb):
153    """A breadcrumb for course lists.
154    """
155    grok.context(IStudentStudyLevel)
156
157    @property
158    def title(self):
159        return self.context.level_title
160
161class StudentsContainerPage(KofaPage):
162    """The standard view for student containers.
163    """
164    grok.context(IStudentsContainer)
165    grok.name('index')
166    grok.require('waeup.viewStudentsContainer')
167    grok.template('containerpage')
168    label = _('Student Section')
169    search_button = _('Search')
170    pnav = 4
171
172    def update(self, *args, **kw):
173        datatable.need()
174        form = self.request.form
175        self.hitlist = []
176        if 'searchterm' in form and form['searchterm']:
177            self.searchterm = form['searchterm']
178            self.searchtype = form['searchtype']
179        elif 'old_searchterm' in form:
180            self.searchterm = form['old_searchterm']
181            self.searchtype = form['old_searchtype']
182        else:
183            if 'search' in form:
184                self.flash(_('Empty search string'))
185            return
186        if self.searchtype == 'current_session':
187            self.searchterm = int(self.searchterm)
188        self.hitlist = search(query=self.searchterm,
189            searchtype=self.searchtype, view=self)
190        if not self.hitlist:
191            self.flash('No student found.')
192        return
193
194class StudentsContainerManagePage(KofaPage):
195    """The manage page for student containers.
196    """
197    grok.context(IStudentsContainer)
198    grok.name('manage')
199    grok.require('waeup.manageStudent')
200    grok.template('containermanagepage')
201    pnav = 4
202    label = _('Manage student section')
203    search_button = _('Search')
204    remove_button = _('Remove selected')
205
206    def update(self, *args, **kw):
207        datatable.need()
208        toggleall.need()
209        warning.need()
210        form = self.request.form
211        self.hitlist = []
212        if 'searchterm' in form and form['searchterm']:
213            self.searchterm = form['searchterm']
214            self.searchtype = form['searchtype']
215        elif 'old_searchterm' in form:
216            self.searchterm = form['old_searchterm']
217            self.searchtype = form['old_searchtype']
218        else:
219            if 'search' in form:
220                self.flash(_('Empty search string'))
221            return
222        if not 'entries' in form:
223            self.hitlist = search(query=self.searchterm,
224                searchtype=self.searchtype, view=self)
225            if not self.hitlist:
226                self.flash(_('No student found.'))
227            if 'remove' in form:
228                self.flash(_('No item selected.'))
229            return
230        entries = form['entries']
231        if isinstance(entries, basestring):
232            entries = [entries]
233        deleted = []
234        for entry in entries:
235            if 'remove' in form:
236                del self.context[entry]
237                deleted.append(entry)
238        self.hitlist = search(query=self.searchterm,
239            searchtype=self.searchtype, view=self)
240        if len(deleted):
241            self.flash(_('Successfully removed: ${a}',
242                mapping = {'a':', '.join(deleted)}))
243        return
244
245class StudentAddFormPage(KofaAddFormPage):
246    """Add-form to add a student.
247    """
248    grok.context(IStudentsContainer)
249    grok.require('waeup.manageStudent')
250    grok.name('addstudent')
251    form_fields = grok.AutoFields(IStudent).select(
252        'firstname', 'middlename', 'lastname', 'reg_number')
253    label = _('Add student')
254    pnav = 4
255
256    @action(_('Create student record'), style='primary')
257    def addStudent(self, **data):
258        student = createObject(u'waeup.Student')
259        self.applyData(student, **data)
260        self.context.addStudent(student)
261        self.flash(_('Student record created.'))
262        self.redirect(self.url(self.context[student.student_id], 'index'))
263        return
264
265class StudentBaseDisplayFormPage(KofaDisplayFormPage):
266    """ Page to display student base data
267    """
268    grok.context(IStudent)
269    grok.name('index')
270    grok.require('waeup.viewStudent')
271    grok.template('basepage')
272    form_fields = grok.AutoFields(IStudentBase).omit('password')
273    pnav = 4
274
275    @property
276    def label(self):
277        return _('${a}: Base Data',
278            mapping = {'a':self.context.display_fullname})
279
280    @property
281    def hasPassword(self):
282        if self.context.password:
283            return _('set')
284        return _('unset')
285
286class ContactStudentForm(ContactAdminForm):
287    grok.context(IStudent)
288    grok.name('contactstudent')
289    grok.require('waeup.viewStudent')
290    pnav = 4
291    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
292
293    def update(self, subject=u''):
294        self.form_fields.get('subject').field.default = subject
295        self.subject = subject
296        return
297
298    def label(self):
299        return _(u'Send message to ${a}',
300            mapping = {'a':self.context.display_fullname})
301
302    @action('Send message now', style='primary')
303    def send(self, *args, **data):
304        try:
305            email = self.request.principal.email
306        except AttributeError:
307            email = self.config.email_admin
308        usertype = getattr(self.request.principal,
309                           'user_type', 'system').title()
310        kofa_utils = getUtility(IKofaUtils)
311        success = kofa_utils.sendContactForm(
312                self.request.principal.title,email,
313                self.context.display_fullname,self.context.email,
314                self.request.principal.id,usertype,
315                self.config.name,
316                data['body'],data['subject'])
317        if success:
318            self.flash(_('Your message has been sent.'))
319        else:
320            self.flash(_('An smtp server error occurred.'))
321        return
322
323class StudentBaseManageFormPage(KofaEditFormPage):
324    """ View to manage student base data
325    """
326    grok.context(IStudent)
327    grok.name('manage_base')
328    grok.require('waeup.manageStudent')
329    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
330    form_fields['phone'].custom_widget = PhoneWidget
331    grok.template('basemanagepage')
332    label = _('Manage base data')
333    pnav = 4
334
335    def update(self):
336        datepicker.need() # Enable jQuery datepicker in date fields.
337        tabs.need()
338        self.tab1 = self.tab2 = ''
339        qs = self.request.get('QUERY_STRING', '')
340        if not qs:
341            qs = 'tab1'
342        setattr(self, qs, 'active')
343        super(StudentBaseManageFormPage, self).update()
344        self.wf_info = IWorkflowInfo(self.context)
345        return
346
347    def getTransitions(self):
348        """Return a list of dicts of allowed transition ids and titles.
349
350        Each list entry provides keys ``name`` and ``title`` for
351        internal name and (human readable) title of a single
352        transition.
353        """
354        allowed_transitions = self.wf_info.getManualTransitions()
355        return [dict(name='', title=_('No transition'))] +[
356            dict(name=x, title=y) for x, y in allowed_transitions]
357
358    @action(_('Save'), style='primary')
359    def save(self, **data):
360        form = self.request.form
361        password = form.get('password', None)
362        password_ctl = form.get('control_password', None)
363        if password:
364            validator = getUtility(IPasswordValidator)
365            errors = validator.validate_password(password, password_ctl)
366            if errors:
367                self.flash( ' '.join(errors))
368                return
369        changed_fields = self.applyData(self.context, **data)
370        # Turn list of lists into single list
371        if changed_fields:
372            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
373        else:
374            changed_fields = []
375        if password:
376            # Now we know that the form has no errors and can set password ...
377            IUserAccount(self.context).setPassword(password)
378            changed_fields.append('password')
379        # ... and execute transition
380        if form.has_key('transition') and form['transition']:
381            transition_id = form['transition']
382            self.wf_info.fireTransition(transition_id)
383        fields_string = ' + '.join(changed_fields)
384        self.flash(_('Form has been saved.'))
385        if fields_string:
386            write_log_message(self, 'saved: % s' % fields_string)
387        return
388
389class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
390    """ Page to display student clearance data
391    """
392    grok.context(IStudent)
393    grok.name('view_clearance')
394    grok.require('waeup.viewStudent')
395    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
396    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
397    pnav = 4
398
399    @property
400    def label(self):
401        return _('${a}: Clearance Data',
402            mapping = {'a':self.context.display_fullname})
403
404class ExportPDFClearanceSlipPage(grok.View):
405    """Deliver a PDF slip of the context.
406    """
407    grok.context(IStudent)
408    grok.name('clearance.pdf')
409    grok.require('waeup.viewStudent')
410    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
411    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
412    prefix = 'form'
413
414    @property
415    def title(self):
416        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
417        return translate(_('Clearance Data'), 'waeup.kofa',
418            target_language=portal_language)
419
420    @property
421    def label(self):
422        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
423        return translate(_('Clearance Slip of '),
424            'waeup.kofa', target_language=portal_language) \
425            + ' %s' % self.context.display_fullname
426
427    def render(self):
428        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
429            self.request)
430        students_utils = getUtility(IStudentsUtils)
431        return students_utils.renderPDF(
432            self, 'clearance.pdf',
433            self.context.getStudent(), studentview)
434
435class StudentClearanceManageFormPage(KofaEditFormPage):
436    """ Page to edit student clearance data
437    """
438    grok.context(IStudent)
439    grok.name('edit_clearance')
440    grok.require('waeup.manageStudent')
441    grok.template('clearanceeditpage')
442    form_fields = grok.AutoFields(IStudentClearance)
443    label = _('Manage clearance data')
444    pnav = 4
445    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
446
447    def update(self):
448        datepicker.need() # Enable jQuery datepicker in date fields.
449        tabs.need()
450        self.tab1 = self.tab2 = ''
451        qs = self.request.get('QUERY_STRING', '')
452        if not qs:
453            qs = 'tab1'
454        setattr(self, qs, 'active')
455        return super(StudentClearanceManageFormPage, self).update()
456
457    @action(_('Save'), style='primary')
458    def save(self, **data):
459        msave(self, **data)
460        return
461
462class StudentClearPage(UtilityView, grok.View):
463    """ Clear student by clearance officer
464    """
465    grok.context(IStudent)
466    grok.name('clear')
467    grok.require('waeup.clearStudent')
468
469    def update(self):
470        if self.context.state == REQUESTED:
471            IWorkflowInfo(self.context).fireTransition('clear')
472            self.flash(_('Student has been cleared.'))
473        else:
474            self.flash(_('Student is in wrong state.'))
475        self.redirect(self.url(self.context,'view_clearance'))
476        return
477
478    def render(self):
479        return
480
481class StudentRejectClearancePage(UtilityView, grok.View):
482    """ Reject clearance by clearance officers
483    """
484    grok.context(IStudent)
485    grok.name('reject_clearance')
486    grok.require('waeup.clearStudent')
487
488    def update(self):
489        if self.context.state == CLEARED:
490            IWorkflowInfo(self.context).fireTransition('reset4')
491            message = _('Clearance has been annulled.')
492            self.flash(message)
493        elif self.context.state == REQUESTED:
494            IWorkflowInfo(self.context).fireTransition('reset3')
495            message = _('Clearance request has been rejected.')
496            self.flash(message)
497        else:
498            self.flash(_('Student is in wrong state.'))
499            self.redirect(self.url(self.context,'view_clearance'))
500            return
501        args = {'subject':message}
502        self.redirect(self.url(self.context) +
503            '/contactstudent?%s' % urlencode(args))
504        return
505
506    def render(self):
507        return
508
509class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
510    """ Page to display student personal data
511    """
512    grok.context(IStudent)
513    grok.name('view_personal')
514    grok.require('waeup.viewStudent')
515    form_fields = grok.AutoFields(IStudentPersonal)
516    form_fields['perm_address'].custom_widget = BytesDisplayWidget
517    pnav = 4
518
519    @property
520    def label(self):
521        return _('${a}: Personal Data',
522            mapping = {'a':self.context.display_fullname})
523
524class StudentPersonalManageFormPage(KofaEditFormPage):
525    """ Page to edit student clearance data
526    """
527    grok.context(IStudent)
528    grok.name('edit_personal')
529    grok.require('waeup.viewStudent')
530    form_fields = grok.AutoFields(IStudentPersonal)
531    label = _('Manage personal data')
532    pnav = 4
533
534    @action(_('Save'), style='primary')
535    def save(self, **data):
536        msave(self, **data)
537        return
538
539class StudyCourseDisplayFormPage(KofaDisplayFormPage):
540    """ Page to display the student study course data
541    """
542    grok.context(IStudentStudyCourse)
543    grok.name('index')
544    grok.require('waeup.viewStudent')
545    form_fields = grok.AutoFields(IStudentStudyCourse)
546    grok.template('studycoursepage')
547    pnav = 4
548
549    @property
550    def label(self):
551        return _('${a}: Study Course',
552            mapping = {'a':self.context.__parent__.display_fullname})
553
554    @property
555    def current_mode(self):
556        if self.context.certificate is not None:
557            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
558            return studymodes_dict[self.context.certificate.study_mode]
559        return
560
561    @property
562    def department(self):
563        if self.context.certificate is not None:
564            return self.context.certificate.__parent__.__parent__
565        return
566
567    @property
568    def faculty(self):
569        if self.context.certificate is not None:
570            return self.context.certificate.__parent__.__parent__.__parent__
571        return
572
573class StudyCourseManageFormPage(KofaEditFormPage):
574    """ Page to edit the student study course data
575    """
576    grok.context(IStudentStudyCourse)
577    grok.name('manage')
578    grok.require('waeup.manageStudent')
579    grok.template('studycoursemanagepage')
580    form_fields = grok.AutoFields(IStudentStudyCourse)
581    label = _('Manage study course')
582    pnav = 4
583    taboneactions = [_('Save'),_('Cancel')]
584    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
585    tabthreeactions = [_('Add study level')]
586
587    def update(self):
588        super(StudyCourseManageFormPage, self).update()
589        tabs.need()
590        self.tab1 = self.tab2 = ''
591        qs = self.request.get('QUERY_STRING', '')
592        if not qs:
593            qs = 'tab1'
594        setattr(self, qs, 'active')
595        warning.need()
596        datatable.need()
597        return
598
599    @action(_('Save'), style='primary')
600    def save(self, **data):
601        msave(self, **data)
602        return
603
604    @property
605    def level_dict(self):
606        studylevelsource = StudyLevelSource().factory
607        for code in studylevelsource.getValues(self.context):
608            title = studylevelsource.getTitle(self.context, code)
609            yield(dict(code=code, title=title))
610
611    @action(_('Add study level'))
612    def addStudyLevel(self, **data):
613        level_code = self.request.form.get('addlevel', None)
614        studylevel = StudentStudyLevel()
615        studylevel.level = int(level_code)
616        try:
617            self.context.addStudentStudyLevel(
618                self.context.certificate,studylevel)
619            self.flash(_('Study level has been added.'))
620        except KeyError:
621            self.flash(_('This level exists.'))
622        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
623        return
624
625    @jsaction(_('Remove selected levels'))
626    def delStudyLevels(self, **data):
627        form = self.request.form
628        if form.has_key('val_id'):
629            child_id = form['val_id']
630        else:
631            self.flash(_('No study level selected.'))
632            self.redirect(self.url(self.context, '@@manage')+'?tab2')
633            return
634        if not isinstance(child_id, list):
635            child_id = [child_id]
636        deleted = []
637        for id in child_id:
638            del self.context[id]
639            deleted.append(id)
640        if len(deleted):
641            self.flash(_('Successfully removed: ${a}',
642                mapping = {'a':', '.join(deleted)}))
643        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
644        return
645
646class StudyLevelDisplayFormPage(KofaDisplayFormPage):
647    """ Page to display student study levels
648    """
649    grok.context(IStudentStudyLevel)
650    grok.name('index')
651    grok.require('waeup.viewStudent')
652    form_fields = grok.AutoFields(IStudentStudyLevel)
653    grok.template('studylevelpage')
654    pnav = 4
655
656    def update(self):
657        super(StudyLevelDisplayFormPage, self).update()
658        datatable.need()
659        return
660
661    @property
662    def label(self):
663        # Here we know that the cookie has been set
664        lang = self.request.cookies.get('kofa.language')
665        level_title = translate(self.context.level_title, 'waeup.kofa',
666            target_language=lang)
667        return _('${a}: Study Level ${b}', mapping = {
668            'a':self.context.getStudent().display_fullname,
669            'b':level_title})
670
671    @property
672    def total_credits(self):
673        total_credits = 0
674        for key, val in self.context.items():
675            total_credits += val.credits
676        return total_credits
677
678class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
679    """Deliver a PDF slip of the context.
680    """
681    grok.context(IStudentStudyLevel)
682    grok.name('course_registration.pdf')
683    grok.require('waeup.viewStudent')
684    form_fields = grok.AutoFields(IStudentStudyLevel)
685    prefix = 'form'
686
687    @property
688    def title(self):
689        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
690        return translate(_('Level Data'), 'waeup.kofa',
691            target_language=portal_language)
692
693    @property
694    def content_title(self):
695        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
696        return translate(_('Course List'), 'waeup.kofa',
697            target_language=portal_language)
698
699    @property
700    def label(self):
701        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
702        lang = self.request.cookies.get('kofa.language', portal_language)
703        level_title = translate(self.context.level_title, 'waeup.kofa',
704            target_language=lang)
705        return translate(_('Course Registration Slip  '),
706            'waeup.kofa', target_language=portal_language) \
707            + ' %s' % level_title
708
709    def render(self):
710        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
711        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
712        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
713        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
714        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
715        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
716        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
717        Mand = translate(_('Mand.'), 'waeup.kofa', target_language=portal_language)
718        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
719        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
720            self.request)
721        students_utils = getUtility(IStudentsUtils)
722        tabledata = sorted(self.context.values(),
723            key=lambda value: str(value.semester) + value.code)
724        return students_utils.renderPDF(
725            self, 'course_registration.pdf',
726            self.context.getStudent(), studentview,
727            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
728                         (Title,'title', 5),
729                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
730                         (Cred, 'credits', 1.5),
731                         (Mand, 'mandatory', 1.5),
732                         (Score, 'score', 1.5),('Auto', 'automatic', 1.5)
733                         ],
734            tabledata=tabledata)
735
736class StudyLevelManageFormPage(KofaEditFormPage):
737    """ Page to edit the student study level data
738    """
739    grok.context(IStudentStudyLevel)
740    grok.name('manage')
741    grok.require('waeup.manageStudent')
742    grok.template('studylevelmanagepage')
743    form_fields = grok.AutoFields(IStudentStudyLevel)
744    pnav = 4
745    taboneactions = [_('Save'),_('Cancel')]
746    tabtwoactions = [_('Add course ticket'),
747        _('Remove selected tickets'),_('Cancel')]
748
749    def update(self):
750        super(StudyLevelManageFormPage, self).update()
751        tabs.need()
752        self.tab1 = self.tab2 = ''
753        qs = self.request.get('QUERY_STRING', '')
754        if not qs:
755            qs = 'tab1'
756        setattr(self, qs, 'active')
757        warning.need()
758        datatable.need()
759        return
760
761    @property
762    def label(self):
763        # Here we know that the cookie has been set
764        lang = self.request.cookies.get('kofa.language')
765        level_title = translate(self.context.level_title, 'waeup.kofa',
766            target_language=lang)
767        return _('Manage study level ${a}',
768            mapping = {'a':level_title})
769
770    @action(_('Save'), style='primary')
771    def save(self, **data):
772        msave(self, **data)
773        return
774
775    @action(_('Add course ticket'))
776    def addCourseTicket(self, **data):
777        self.redirect(self.url(self.context, '@@add'))
778
779    @jsaction(_('Remove selected tickets'))
780    def delCourseTicket(self, **data):
781        form = self.request.form
782        if form.has_key('val_id'):
783            child_id = form['val_id']
784        else:
785            self.flash(_('No ticket selected.'))
786            self.redirect(self.url(self.context, '@@manage')+'?tab2')
787            return
788        if not isinstance(child_id, list):
789            child_id = [child_id]
790        deleted = []
791        for id in child_id:
792            del self.context[id]
793            deleted.append(id)
794        if len(deleted):
795            self.flash(_('Successfully removed: ${a}',
796                mapping = {'a':', '.join(deleted)}))
797        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
798        return
799
800class ValidateCoursesPage(UtilityView, grok.View):
801    """ Validate course list by course adviser
802    """
803    grok.context(IStudentStudyLevel)
804    grok.name('validate_courses')
805    grok.require('waeup.validateStudent')
806
807    def update(self):
808        if str(self.context.__parent__.current_level) != self.context.__name__:
809            self.flash(_('This level does not correspond current level.'))
810        elif self.context.getStudent().state == REGISTERED:
811            IWorkflowInfo(self.context.getStudent()).fireTransition(
812                'validate_courses')
813            self.flash(_('Course list has been validated.'))
814        else:
815            self.flash(_('Student is in the wrong state.'))
816        self.redirect(self.url(self.context))
817        return
818
819    def render(self):
820        return
821
822class RejectCoursesPage(UtilityView, grok.View):
823    """ Reject course list by course adviser
824    """
825    grok.context(IStudentStudyLevel)
826    grok.name('reject_courses')
827    grok.require('waeup.validateStudent')
828
829    def update(self):
830        if str(self.context.__parent__.current_level) != self.context.__name__:
831            self.flash(_('This level does not correspond current level.'))
832            self.redirect(self.url(self.context))
833            return
834        elif self.context.getStudent().state == VALIDATED:
835            IWorkflowInfo(self.context.getStudent()).fireTransition('reset8')
836            message = _('Course list request has been annulled.')
837            self.flash(message)
838        elif self.context.getStudent().state == REGISTERED:
839            IWorkflowInfo(self.context.getStudent()).fireTransition('reset7')
840            message = _('Course list request has been rejected:')
841            self.flash(message)
842        else:
843            self.flash(_('Student is in the wrong state.'))
844            self.redirect(self.url(self.context))
845            return
846        args = {'subject':message}
847        self.redirect(self.url(self.context.getStudent()) +
848            '/contactstudent?%s' % urlencode(args))
849        return
850
851    def render(self):
852        return
853
854class CourseTicketAddFormPage(KofaAddFormPage):
855    """Add a course ticket.
856    """
857    grok.context(IStudentStudyLevel)
858    grok.name('add')
859    grok.require('waeup.manageStudent')
860    label = _('Add course ticket')
861    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
862        'grade', 'score', 'automatic', 'carry_over')
863    pnav = 4
864
865    @action(_('Add course ticket'))
866    def addCourseTicket(self, **data):
867        ticket = CourseTicket()
868        course = data['course']
869        ticket.automatic = False
870        ticket.code = course.code
871        ticket.title = course.title
872        ticket.fcode = course.__parent__.__parent__.__parent__.code
873        ticket.dcode = course.__parent__.__parent__.code
874        ticket.credits = course.credits
875        ticket.passmark = course.passmark
876        ticket.semester = course.semester
877        try:
878            self.context.addCourseTicket(ticket)
879        except KeyError:
880            self.flash(_('The ticket exists.'))
881            return
882        self.flash(_('Successfully added ${a}.',
883            mapping = {'a':ticket.code}))
884        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
885        return
886
887    @action(_('Cancel'), validator=NullValidator)
888    def cancel(self, **data):
889        self.redirect(self.url(self.context))
890
891class CourseTicketDisplayFormPage(KofaDisplayFormPage):
892    """ Page to display course tickets
893    """
894    grok.context(ICourseTicket)
895    grok.name('index')
896    grok.require('waeup.viewStudent')
897    form_fields = grok.AutoFields(ICourseTicket)
898    grok.template('courseticketpage')
899    pnav = 4
900
901    @property
902    def label(self):
903        return _('${a}: Course Ticket ${b}', mapping = {
904            'a':self.context.getStudent().display_fullname,
905            'b':self.context.code})
906
907class CourseTicketManageFormPage(KofaEditFormPage):
908    """ Page to manage course tickets
909    """
910    grok.context(ICourseTicket)
911    grok.name('manage')
912    grok.require('waeup.manageStudent')
913    form_fields = grok.AutoFields(ICourseTicket)
914    grok.template('courseticketmanagepage')
915    pnav = 4
916
917    @property
918    def label(self):
919        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
920
921    @action('Save', style='primary')
922    def save(self, **data):
923        msave(self, **data)
924        return
925
926class PaymentsManageFormPage(KofaEditFormPage):
927    """ Page to manage the student payments
928
929    This manage form page is for both students and students officers.
930    """
931    grok.context(IStudentPaymentsContainer)
932    grok.name('index')
933    grok.require('waeup.payStudent')
934    form_fields = grok.AutoFields(IStudentPaymentsContainer)
935    grok.template('paymentsmanagepage')
936    pnav = 4
937
938    def unremovable(self, ticket):
939        usertype = getattr(self.request.principal, 'user_type', None)
940        if not usertype:
941            return False
942        return (self.request.principal.user_type == 'student' and ticket.r_code)
943
944    @property
945    def label(self):
946        return _('${a}: Payments',
947            mapping = {'a':self.context.__parent__.display_fullname})
948
949    def update(self):
950        super(PaymentsManageFormPage, self).update()
951        datatable.need()
952        warning.need()
953        return
954
955    @jsaction(_('Remove selected tickets'))
956    def delPaymentTicket(self, **data):
957        form = self.request.form
958        if form.has_key('val_id'):
959            child_id = form['val_id']
960        else:
961            self.flash(_('No payment selected.'))
962            self.redirect(self.url(self.context))
963            return
964        if not isinstance(child_id, list):
965            child_id = [child_id]
966        deleted = []
967        for id in child_id:
968            # Students are not allowed to remove used payment tickets
969            if not self.unremovable(self.context[id]):
970                del self.context[id]
971                deleted.append(id)
972        if len(deleted):
973            self.flash(_('Successfully removed: ${a}',
974                mapping = {'a': ', '.join(deleted)}))
975            write_log_message(self,'removed: % s' % ', '.join(deleted))
976        self.redirect(self.url(self.context))
977        return
978
979    @action(_('Add online payment ticket'))
980    def addPaymentTicket(self, **data):
981        self.redirect(self.url(self.context, '@@addop'))
982
983class OnlinePaymentAddFormPage(KofaAddFormPage):
984    """ Page to add an online payment ticket
985    """
986    grok.context(IStudentPaymentsContainer)
987    grok.name('addop')
988    grok.require('waeup.payStudent')
989    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
990        'p_category')
991    label = _('Add online payment')
992    pnav = 4
993
994    @action(_('Create ticket'), style='primary')
995    def createTicket(self, **data):
996        p_category = data['p_category']
997        student = self.context.__parent__
998        if p_category == 'bed_allocation' and student[
999            'studycourse'].current_session != grok.getSite()[
1000            'configuration'].accommodation_session:
1001                self.flash(
1002                    _('Your current session does not match ' + \
1003                    'accommodation session.'))
1004                self.redirect(self.url(self.context))
1005                return
1006        students_utils = getUtility(IStudentsUtils)
1007        pay_details  = students_utils.getPaymentDetails(
1008            p_category,student)
1009        if pay_details['error']:
1010            self.flash(pay_details['error'])
1011            self.redirect(self.url(self.context))
1012            return
1013        p_item = pay_details['p_item']
1014        p_session = pay_details['p_session']
1015        for key in self.context.keys():
1016            ticket = self.context[key]
1017            if ticket.p_state == 'paid' and\
1018               ticket.p_category == p_category and \
1019               ticket.p_item == p_item and \
1020               ticket.p_session == p_session:
1021                  self.flash(
1022                      _('This type of payment has already been made.'))
1023                  self.redirect(self.url(self.context))
1024                  return
1025        payment = createObject(u'waeup.StudentOnlinePayment')
1026        self.applyData(payment, **data)
1027        timestamp = "%d" % int(time()*1000)
1028        payment.p_id = "p%s" % timestamp
1029        payment.p_item = p_item
1030        payment.p_session = p_session
1031        payment.amount_auth = pay_details['amount']
1032        payment.surcharge_1 = pay_details['surcharge_1']
1033        payment.surcharge_2 = pay_details['surcharge_2']
1034        payment.surcharge_3 = pay_details['surcharge_3']
1035        self.context[payment.p_id] = payment
1036        self.flash(_('Payment ticket created.'))
1037        self.redirect(self.url(self.context))
1038        return
1039
1040class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1041    """ Page to view an online payment ticket
1042    """
1043    grok.context(IStudentOnlinePayment)
1044    grok.name('index')
1045    grok.require('waeup.viewStudent')
1046    form_fields = grok.AutoFields(IStudentOnlinePayment)
1047    form_fields[
1048        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1049    form_fields[
1050        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1051    pnav = 4
1052
1053    @property
1054    def label(self):
1055        return _('${a}: Online Payment Ticket ${b}', mapping = {
1056            'a':self.context.getStudent().display_fullname,
1057            'b':self.context.p_id})
1058
1059class OnlinePaymentCallbackPage(UtilityView, grok.View):
1060    """ Callback view
1061    """
1062    grok.context(IStudentOnlinePayment)
1063    grok.name('callback')
1064    grok.require('waeup.payStudent')
1065
1066    # This update method simulates a valid callback und must be
1067    # specified in the customization package. The parameters must be taken
1068    # from the incoming request.
1069    def update(self):
1070        if self.context.p_state == 'paid':
1071            self.flash(_('This ticket has already been paid.'))
1072            return
1073        student = self.context.getStudent()
1074        write_log_message(self,'valid callback: %s' % self.context.p_id)
1075        self.context.r_amount_approved = self.context.amount_auth
1076        self.context.r_card_num = u'0000'
1077        self.context.r_code = u'00'
1078        self.context.p_state = 'paid'
1079        self.context.payment_date = datetime.now()
1080        if self.context.p_category == 'clearance':
1081            # Create CLR access code
1082            pin, error = create_accesscode('CLR',0,student.student_id)
1083            if error:
1084                self.flash(_('Valid callback received. ${a}',
1085                    mapping = {'a':error}))
1086                return
1087            self.context.ac = pin
1088        elif self.context.p_category == 'schoolfee':
1089            # Create SFE access code
1090            pin, error = create_accesscode('SFE',0,student.student_id)
1091            if error:
1092                self.flash(_('Valid callback received. ${a}',
1093                    mapping = {'a':error}))
1094                return
1095            self.context.ac = pin
1096        elif self.context.p_category == 'bed_allocation':
1097            # Create HOS access code
1098            pin, error = create_accesscode('HOS',0,student.student_id)
1099            if error:
1100                self.flash(_('Valid callback received. ${a}',
1101                    mapping = {'a':error}))
1102                return
1103            self.context.ac = pin
1104        self.flash(_('Valid callback received.'))
1105        return
1106
1107    def render(self):
1108        self.redirect(self.url(self.context, '@@index'))
1109        return
1110
1111class InterswitchPage(KofaPage):
1112    """ View which sends a POST request to the Interswitch
1113    CollegePAY payment gateway.
1114    """
1115    grok.context(IStudentOnlinePayment)
1116    grok.name('goto_interswitch')
1117    grok.template('goto_interswitch')
1118    grok.require('waeup.payStudent')
1119
1120    label = _('Submit data to CollegePAY (Interswitch Payment Gateway)')
1121    submit_button = _('Submit')
1122
1123    # Uniben product data (to be removed later)
1124    product_id = '57'
1125    currency = '566'
1126    pay_item_id = '5700'
1127
1128    action = 'https://testwebpay.interswitchng.com/test_paydirect/webpay/pay.aspx'
1129    site_redirect_url = 'http://localhost?echo='
1130    site_name = 'xyz.waeup.org'
1131    provider_acct = '2345'
1132    provider_bank_id = '8'
1133    provider_item_name = 'Kofa Provider Fee'
1134    institution_acct = '1234'
1135    institution_bank_id = '9'
1136    institution_name = 'Sample University'
1137
1138    def update(self):
1139        if self.context.p_state != 'unpaid':
1140            self.flash(_("Payment ticket can't be re-send to CollegePAY."))
1141            self.redirect(self.url(self.context, '@@index'))
1142            return
1143        xmldict = {}
1144        self.student = self.context.getStudent()
1145        self.amount = (self.context.amount_auth + self.context.surcharge_1 +
1146            self.context.surcharge_2 + self.context.surcharge_3)
1147        self.local_date_time = str(self.context.creation_date)
1148        certificate = getattr(self.student['studycourse'],'certificate',None)
1149        if certificate is not None:
1150            xmldict['department'] = certificate.__parent__.__parent__.code
1151            xmldict['faculty'] = certificate.__parent__.__parent__.__parent__.code
1152        else:
1153            xmldict['department'] = None
1154            xmldict['faculty'] = None
1155        xmldict['detail_ref'] = self.context.p_id
1156        xmldict['provider_amt'] = self.context.surcharge_1
1157        xmldict['provider_acct'] = self.provider_acct
1158        xmldict['provider_bank_id'] = self.provider_bank_id
1159        xmldict['provider_item_name'] = self.provider_item_name
1160        xmldict['institution_amt'] = self.amount
1161        xmldict['institution_acct'] = self.institution_acct
1162        xmldict['institution_bank_id'] = self.institution_bank_id
1163        xmldict['institution_item_name'] = self.context.p_category
1164        xmldict['institution_name'] = self.institution_name
1165        xmltext = """<payment_item_detail>
1166<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">
1167<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
1168<item_detail item_id="2" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
1169</item_details>
1170</payment_item_detail>""" % xmldict
1171        self.xml_data = """<input type="hidden" name="xml_data" value='%s'  />""" % xmltext
1172        return
1173
1174class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1175    """Deliver a PDF slip of the context.
1176    """
1177    grok.context(IStudentOnlinePayment)
1178    grok.name('payment_receipt.pdf')
1179    grok.require('waeup.viewStudent')
1180    form_fields = grok.AutoFields(IStudentOnlinePayment)
1181    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
1182    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
1183    prefix = 'form'
1184    title = 'Payment Data'
1185
1186    @property
1187    def label(self):
1188        return 'Online Payment Receipt %s' % self.context.p_id
1189
1190    def render(self):
1191        if self.context.p_state != 'paid':
1192            self.flash('Ticket not yet paid.')
1193            self.redirect(self.url(self.context))
1194            return
1195        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1196            self.request)
1197        students_utils = getUtility(IStudentsUtils)
1198        return students_utils.renderPDF(self, 'payment_receipt.pdf',
1199            self.context.getStudent(), studentview)
1200
1201
1202class AccommodationManageFormPage(KofaEditFormPage):
1203    """ Page to manage bed tickets.
1204
1205    This manage form page is for both students and students officers.
1206    """
1207    grok.context(IStudentAccommodation)
1208    grok.name('index')
1209    grok.require('waeup.handleAccommodation')
1210    form_fields = grok.AutoFields(IStudentAccommodation)
1211    grok.template('accommodationmanagepage')
1212    pnav = 4
1213    officers_only_actions = [_('Remove selected')]
1214
1215    @property
1216    def label(self):
1217        return _('${a}: Accommodation',
1218            mapping = {'a':self.context.__parent__.display_fullname})
1219
1220    def update(self):
1221        super(AccommodationManageFormPage, self).update()
1222        datatable.need()
1223        warning.need()
1224        return
1225
1226    @jsaction(_('Remove selected'))
1227    def delBedTickets(self, **data):
1228        if getattr(self.request.principal, 'user_type', None) == 'student':
1229            self.flash(_('You are not allowed to remove bed tickets.'))
1230            self.redirect(self.url(self.context))
1231            return
1232        form = self.request.form
1233        if form.has_key('val_id'):
1234            child_id = form['val_id']
1235        else:
1236            self.flash(_('No bed ticket selected.'))
1237            self.redirect(self.url(self.context))
1238            return
1239        if not isinstance(child_id, list):
1240            child_id = [child_id]
1241        deleted = []
1242        for id in child_id:
1243            del self.context[id]
1244            deleted.append(id)
1245        if len(deleted):
1246            self.flash(_('Successfully removed: ${a}',
1247                mapping = {'a':', '.join(deleted)}))
1248            write_log_message(self,'removed: % s' % ', '.join(deleted))
1249        self.redirect(self.url(self.context))
1250        return
1251
1252    @property
1253    def selected_actions(self):
1254        if getattr(self.request.principal, 'user_type', None) == 'student':
1255            return [action for action in self.actions
1256                    if not action.label in self.officers_only_actions]
1257        return self.actions
1258
1259class BedTicketAddPage(KofaPage):
1260    """ Page to add an online payment ticket
1261    """
1262    grok.context(IStudentAccommodation)
1263    grok.name('add')
1264    grok.require('waeup.handleAccommodation')
1265    grok.template('enterpin')
1266    ac_prefix = 'HOS'
1267    label = _('Add bed ticket')
1268    pnav = 4
1269    buttonname = _('Create bed ticket')
1270    notice = ''
1271
1272    def update(self, SUBMIT=None):
1273        student = self.context.getStudent()
1274        students_utils = getUtility(IStudentsUtils)
1275        acc_details  = students_utils.getAccommodationDetails(student)
1276        if not acc_details:
1277            self.flash(_("Your data are incomplete."))
1278            self.redirect(self.url(self.context))
1279            return
1280        if not student.state in acc_details['allowed_states']:
1281            self.flash(_("You are in the wrong registration state."))
1282            self.redirect(self.url(self.context))
1283            return
1284        if student['studycourse'].current_session != acc_details[
1285            'booking_session']:
1286            self.flash(
1287                _('Your current session does not match accommodation session.'))
1288            self.redirect(self.url(self.context))
1289            return
1290        if str(acc_details['booking_session']) in self.context.keys():
1291            self.flash(
1292                _('You already booked a bed space in current ' \
1293                    + 'accommodation session.'))
1294            self.redirect(self.url(self.context))
1295            return
1296        self.ac_series = self.request.form.get('ac_series', None)
1297        self.ac_number = self.request.form.get('ac_number', None)
1298        if SUBMIT is None:
1299            return
1300        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1301        code = get_access_code(pin)
1302        if not code:
1303            self.flash(_('Activation code is invalid.'))
1304            return
1305        # Search and book bed
1306        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1307        entries = cat.searchResults(
1308            owner=(student.student_id,student.student_id))
1309        if len(entries):
1310            # If bed space has bee manually allocated use this bed
1311            bed = [entry for entry in entries][0]
1312        else:
1313            # else search for other available beds
1314            entries = cat.searchResults(
1315                bed_type=(acc_details['bt'],acc_details['bt']))
1316            available_beds = [
1317                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1318            if available_beds:
1319                students_utils = getUtility(IStudentsUtils)
1320                bed = students_utils.selectBed(available_beds)
1321                bed.bookBed(student.student_id)
1322            else:
1323                self.flash(_('There is no free bed in your category ${a}.',
1324                    mapping = {'a':acc_details['bt']}))
1325                return
1326        # Mark pin as used (this also fires a pin related transition)
1327        if code.state == USED:
1328            self.flash(_('Activation code has already been used.'))
1329            return
1330        else:
1331            comment = _(u'invalidated')
1332            # Here we know that the ac is in state initialized so we do not
1333            # expect an exception, but the owner might be different
1334            if not invalidate_accesscode(
1335                pin,comment,self.context.getStudent().student_id):
1336                self.flash(_('You are not the owner of this access code.'))
1337                return
1338        # Create bed ticket
1339        bedticket = createObject(u'waeup.BedTicket')
1340        bedticket.booking_code = pin
1341        bedticket.booking_session = acc_details['booking_session']
1342        bedticket.bed_type = acc_details['bt']
1343        bedticket.bed = bed
1344        hall_title = bed.__parent__.hostel_name
1345        coordinates = bed.getBedCoordinates()[1:]
1346        block, room_nr, bed_nr = coordinates
1347        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1348            'a':hall_title, 'b':block,
1349            'c':room_nr, 'd':bed_nr,
1350            'e':bed.bed_type})
1351        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1352        bedticket.bed_coordinates = translate(
1353            bc, 'waeup.kofa',target_language=portal_language)
1354        key = str(acc_details['booking_session'])
1355        self.context[key] = bedticket
1356        self.flash(_('Bed ticket created and bed booked: ${a}',
1357            mapping = {'a':bedticket.bed_coordinates}))
1358        self.redirect(self.url(self.context))
1359        return
1360
1361class BedTicketDisplayFormPage(KofaDisplayFormPage):
1362    """ Page to display bed tickets
1363    """
1364    grok.context(IBedTicket)
1365    grok.name('index')
1366    grok.require('waeup.handleAccommodation')
1367    form_fields = grok.AutoFields(IBedTicket)
1368    form_fields[
1369        'booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1370    pnav = 4
1371
1372    @property
1373    def label(self):
1374        return _('Bed Ticket for Session ${a}',
1375            mapping = {'a':self.context.getSessionString()})
1376
1377class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1378    """Deliver a PDF slip of the context.
1379    """
1380    grok.context(IBedTicket)
1381    grok.name('bed_allocation.pdf')
1382    grok.require('waeup.handleAccommodation')
1383    form_fields = grok.AutoFields(IBedTicket)
1384    form_fields['booking_date'].custom_widget = FriendlyDateDisplayWidget('le')
1385    prefix = 'form'
1386
1387    @property
1388    def title(self):
1389        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1390        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1391            target_language=portal_language)
1392
1393    @property
1394    def label(self):
1395        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1396        return translate(_('Bed Allocation: '),
1397            'waeup.kofa', target_language=portal_language) \
1398            + ' %s' % self.context.bed_coordinates
1399
1400    def render(self):
1401        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1402            self.request)
1403        students_utils = getUtility(IStudentsUtils)
1404        return students_utils.renderPDF(
1405            self, 'bed_allocation.pdf',
1406            self.context.getStudent(), studentview)
1407
1408class BedTicketRelocationPage(UtilityView, grok.View):
1409    """ Callback view
1410    """
1411    grok.context(IBedTicket)
1412    grok.name('relocate')
1413    grok.require('waeup.manageHostels')
1414
1415    # Relocate student if student parameters have changed or the bed_type
1416    # of the bed has changed
1417    def update(self):
1418        student = self.context.getStudent()
1419        students_utils = getUtility(IStudentsUtils)
1420        acc_details  = students_utils.getAccommodationDetails(student)
1421        if self.context.bed != None and \
1422              'reserved' in self.context.bed.bed_type:
1423            self.flash(_("Students in reserved beds can't be relocated."))
1424            self.redirect(self.url(self.context))
1425            return
1426        if acc_details['bt'] == self.context.bed_type and \
1427                self.context.bed != None and \
1428                self.context.bed.bed_type == self.context.bed_type:
1429            self.flash(_("Student can't be relocated."))
1430            self.redirect(self.url(self.context))
1431            return
1432        # Search a bed
1433        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1434        entries = cat.searchResults(
1435            owner=(student.student_id,student.student_id))
1436        if len(entries) and self.context.bed == None:
1437            # If booking has been cancelled but other bed space has been
1438            # manually allocated after cancellation use this bed
1439            new_bed = [entry for entry in entries][0]
1440        else:
1441            # Search for other available beds
1442            entries = cat.searchResults(
1443                bed_type=(acc_details['bt'],acc_details['bt']))
1444            available_beds = [
1445                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1446            if available_beds:
1447                students_utils = getUtility(IStudentsUtils)
1448                new_bed = students_utils.selectBed(available_beds)
1449                new_bed.bookBed(student.student_id)
1450            else:
1451                self.flash(_('There is no free bed in your category ${a}.',
1452                    mapping = {'a':acc_details['bt']}))
1453                self.redirect(self.url(self.context))
1454                return
1455        # Release old bed if exists
1456        if self.context.bed != None:
1457            self.context.bed.owner = NOT_OCCUPIED
1458            notify(grok.ObjectModifiedEvent(self.context.bed))
1459        # Alocate new bed
1460        self.context.bed_type = acc_details['bt']
1461        self.context.bed = new_bed
1462        hall_title = new_bed.__parent__.hostel_name
1463        coordinates = new_bed.getBedCoordinates()[1:]
1464        block, room_nr, bed_nr = coordinates
1465        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1466            'a':hall_title, 'b':block,
1467            'c':room_nr, 'd':bed_nr,
1468            'e':new_bed.bed_type})
1469        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1470        self.context.bed_coordinates = translate(
1471            bc, 'waeup.kofa',target_language=portal_language)
1472        self.flash(_('Student relocated: ${a}',
1473            mapping = {'a':self.context.bed_coordinates}))
1474        self.redirect(self.url(self.context))
1475        return
1476
1477    def render(self):
1478        return
1479
1480class StudentHistoryPage(KofaPage):
1481    """ Page to display student clearance data
1482    """
1483    grok.context(IStudent)
1484    grok.name('history')
1485    grok.require('waeup.viewStudent')
1486    grok.template('studenthistory')
1487    pnav = 4
1488
1489    @property
1490    def label(self):
1491        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1492
1493# Pages for students only
1494
1495class StudentBaseEditFormPage(KofaEditFormPage):
1496    """ View to edit student base data
1497    """
1498    grok.context(IStudent)
1499    grok.name('edit_base')
1500    grok.require('waeup.handleStudent')
1501    form_fields = grok.AutoFields(IStudentBase).select(
1502        'email', 'phone')
1503    form_fields['phone'].custom_widget = PhoneWidget
1504    label = _('Edit base data')
1505    pnav = 4
1506
1507    @action(_('Save'), style='primary')
1508    def save(self, **data):
1509        msave(self, **data)
1510        return
1511
1512class StudentChangePasswordPage(KofaEditFormPage):
1513    """ View to manage student base data
1514    """
1515    grok.context(IStudent)
1516    grok.name('change_password')
1517    grok.require('waeup.handleStudent')
1518    grok.template('change_password')
1519    label = _('Change password')
1520    pnav = 4
1521
1522    @action(_('Save'), style='primary')
1523    def save(self, **data):
1524        form = self.request.form
1525        password = form.get('change_password', None)
1526        password_ctl = form.get('change_password_repeat', None)
1527        if password:
1528            validator = getUtility(IPasswordValidator)
1529            errors = validator.validate_password(password, password_ctl)
1530            if not errors:
1531                IUserAccount(self.context).setPassword(password)
1532                write_log_message(self, 'saved: password')
1533                self.flash(_('Password changed.'))
1534            else:
1535                self.flash( ' '.join(errors))
1536        return
1537
1538class StudentFilesUploadPage(KofaPage):
1539    """ View to upload files by student
1540    """
1541    grok.context(IStudent)
1542    grok.name('change_portrait')
1543    grok.require('waeup.uploadStudentFile')
1544    grok.template('filesuploadpage')
1545    label = _('Upload portrait')
1546    pnav = 4
1547
1548    def update(self):
1549        if self.context.getStudent().state != ADMITTED:
1550            emit_lock_message(self)
1551            return
1552        super(StudentFilesUploadPage, self).update()
1553        return
1554
1555class StartClearancePage(KofaPage):
1556    grok.context(IStudent)
1557    grok.name('start_clearance')
1558    grok.require('waeup.handleStudent')
1559    grok.template('enterpin')
1560    label = _('Start clearance')
1561    ac_prefix = 'CLR'
1562    notice = ''
1563    pnav = 4
1564    buttonname = _('Start clearance now')
1565
1566    @property
1567    def all_required_fields_filled(self):
1568        if self.context.email and self.context.phone:
1569            return True
1570        return False
1571
1572    @property
1573    def portrait_uploaded(self):
1574        store = getUtility(IExtFileStore)
1575        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1576            return True
1577        return False
1578
1579    def update(self, SUBMIT=None):
1580        if not self.context.state == ADMITTED:
1581            self.flash(_("Wrong state"))
1582            self.redirect(self.url(self.context))
1583            return
1584        if not self.portrait_uploaded:
1585            self.flash(_("No portrait uploaded."))
1586            self.redirect(self.url(self.context, 'change_portrait'))
1587            return
1588        if not self.all_required_fields_filled:
1589            self.flash(_("Not all required fields filled."))
1590            self.redirect(self.url(self.context, 'edit_base'))
1591            return
1592        self.ac_series = self.request.form.get('ac_series', None)
1593        self.ac_number = self.request.form.get('ac_number', None)
1594
1595        if SUBMIT is None:
1596            return
1597        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1598        code = get_access_code(pin)
1599        if not code:
1600            self.flash(_('Activation code is invalid.'))
1601            return
1602        # Mark pin as used (this also fires a pin related transition)
1603        # and fire transition start_clearance
1604        if code.state == USED:
1605            self.flash(_('Activation code has already been used.'))
1606            return
1607        else:
1608            comment = _(u"invalidated")
1609            # Here we know that the ac is in state initialized so we do not
1610            # expect an exception, but the owner might be different
1611            if not invalidate_accesscode(pin,comment,self.context.student_id):
1612                self.flash(_('You are not the owner of this access code.'))
1613                return
1614            self.context.clr_code = pin
1615        IWorkflowInfo(self.context).fireTransition('start_clearance')
1616        self.flash(_('Clearance process has been started.'))
1617        self.redirect(self.url(self.context,'cedit'))
1618        return
1619
1620class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1621    """ View to edit student clearance data by student
1622    """
1623    grok.context(IStudent)
1624    grok.name('cedit')
1625    grok.require('waeup.handleStudent')
1626    form_fields = grok.AutoFields(
1627        IStudentClearance).omit('clearance_locked')
1628    label = _('Edit clearance data')
1629    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
1630
1631    def update(self):
1632        if self.context.clearance_locked:
1633            emit_lock_message(self)
1634            return
1635        return super(StudentClearanceEditFormPage, self).update()
1636
1637    @action(_('Save'), style='primary')
1638    def save(self, **data):
1639        self.applyData(self.context, **data)
1640        self.flash(_('Clearance form has been saved.'))
1641        return
1642
1643    def dataNotComplete(self):
1644        """To be implemented in the customization package.
1645        """
1646        return False
1647
1648    @action(_('Save and request clearance'), style='primary')
1649    def requestClearance(self, **data):
1650        self.applyData(self.context, **data)
1651        if self.dataNotComplete():
1652            self.flash(self.dataNotComplete())
1653            return
1654        self.flash(_('Clearance form has been saved.'))
1655        self.redirect(self.url(self.context,'request_clearance'))
1656        return
1657
1658class RequestClearancePage(KofaPage):
1659    grok.context(IStudent)
1660    grok.name('request_clearance')
1661    grok.require('waeup.handleStudent')
1662    grok.template('enterpin')
1663    label = _('Request clearance')
1664    notice = _('Enter the CLR access code used for starting clearance.')
1665    ac_prefix = 'CLR'
1666    pnav = 4
1667    buttonname = _('Request clearance now')
1668
1669    def update(self, SUBMIT=None):
1670        self.ac_series = self.request.form.get('ac_series', None)
1671        self.ac_number = self.request.form.get('ac_number', None)
1672        if SUBMIT is None:
1673            return
1674        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1675        if self.context.clr_code != pin:
1676            self.flash(_("This isn't your CLR access code."))
1677            return
1678        state = IWorkflowState(self.context).getState()
1679        # This shouldn't happen, but the application officer
1680        # might have forgotten to lock the form after changing the state
1681        if state != CLEARANCE:
1682            self.flash(_('This form cannot be submitted. Wrong state!'))
1683            return
1684        IWorkflowInfo(self.context).fireTransition('request_clearance')
1685        self.flash(_('Clearance has been requested.'))
1686        self.redirect(self.url(self.context))
1687        return
1688
1689class StartCourseRegistrationPage(KofaPage):
1690    grok.context(IStudentStudyCourse)
1691    grok.name('start_course_registration')
1692    grok.require('waeup.handleStudent')
1693    grok.template('enterpin')
1694    label = _('Start course registration')
1695    ac_prefix = 'SFE'
1696    notice = ''
1697    pnav = 4
1698    buttonname = _('Start course registration now')
1699
1700    def update(self, SUBMIT=None):
1701        if not self.context.getStudent().state in (CLEARED,RETURNING):
1702            self.flash(_("Wrong state"))
1703            self.redirect(self.url(self.context))
1704            return
1705        self.ac_series = self.request.form.get('ac_series', None)
1706        self.ac_number = self.request.form.get('ac_number', None)
1707
1708        if SUBMIT is None:
1709            return
1710        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1711        code = get_access_code(pin)
1712        if not code:
1713            self.flash(_('Activation code is invalid.'))
1714            return
1715        # Mark pin as used (this also fires a pin related transition)
1716        # and fire transition start_clearance
1717        if code.state == USED:
1718            self.flash(_('Activation code has already been used.'))
1719            return
1720        else:
1721            comment = _(u"invalidated")
1722            # Here we know that the ac is in state initialized so we do not
1723            # expect an exception, but the owner might be different
1724            if not invalidate_accesscode(
1725                pin,comment,self.context.getStudent().student_id):
1726                self.flash(_('You are not the owner of this access code.'))
1727                return
1728        if self.context.getStudent().state == CLEARED:
1729            IWorkflowInfo(self.context.getStudent()).fireTransition(
1730                'pay_first_school_fee')
1731        elif self.context.getStudent().state == RETURNING:
1732            IWorkflowInfo(self.context.getStudent()).fireTransition(
1733                'pay_school_fee')
1734        self.flash(_('Course registration has been started.'))
1735        self.redirect(self.url(self.context))
1736        return
1737
1738class AddStudyLevelFormPage(KofaEditFormPage):
1739    """ Page for students to add current study levels
1740    """
1741    grok.context(IStudentStudyCourse)
1742    grok.name('add')
1743    grok.require('waeup.handleStudent')
1744    grok.template('studyleveladdpage')
1745    form_fields = grok.AutoFields(IStudentStudyCourse)
1746    pnav = 4
1747
1748    @property
1749    def label(self):
1750        studylevelsource = StudyLevelSource().factory
1751        code = self.context.current_level
1752        title = studylevelsource.getTitle(self.context, code)
1753        return _('Add current level ${a}', mapping = {'a':title})
1754
1755    def update(self):
1756        if self.context.getStudent().state != PAID:
1757            emit_lock_message(self)
1758            return
1759        super(AddStudyLevelFormPage, self).update()
1760        return
1761
1762    @action(_('Create course list now'), style='primary')
1763    def addStudyLevel(self, **data):
1764        studylevel = StudentStudyLevel()
1765        studylevel.level = self.context.current_level
1766        studylevel.level_session = self.context.current_session
1767        try:
1768            self.context.addStudentStudyLevel(
1769                self.context.certificate,studylevel)
1770        except KeyError:
1771            self.flash(_('This level exists.'))
1772        self.redirect(self.url(self.context))
1773        return
1774
1775class StudyLevelEditFormPage(KofaEditFormPage):
1776    """ Page to edit the student study level data by students
1777    """
1778    grok.context(IStudentStudyLevel)
1779    grok.name('edit')
1780    grok.require('waeup.handleStudent')
1781    grok.template('studyleveleditpage')
1782    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1783        'level_session', 'level_verdict')
1784    pnav = 4
1785
1786    def update(self):
1787        if self.context.getStudent().state != PAID:
1788            emit_lock_message(self)
1789            return
1790        super(StudyLevelEditFormPage, self).update()
1791        datatable.need()
1792        warning.need()
1793        return
1794
1795    @property
1796    def label(self):
1797        # Here we know that the cookie has been set
1798        lang = self.request.cookies.get('kofa.language')
1799        level_title = translate(self.context.level_title, 'waeup.kofa',
1800            target_language=lang)
1801        return _('Add and remove course tickets of study level ${a}',
1802            mapping = {'a':level_title})
1803
1804    @property
1805    def total_credits(self):
1806        total_credits = 0
1807        for key, val in self.context.items():
1808            total_credits += val.credits
1809        return total_credits
1810
1811    @action(_('Add course ticket'))
1812    def addCourseTicket(self, **data):
1813        self.redirect(self.url(self.context, 'ctadd'))
1814
1815    @jsaction(_('Remove selected tickets'))
1816    def delCourseTicket(self, **data):
1817        form = self.request.form
1818        if form.has_key('val_id'):
1819            child_id = form['val_id']
1820        else:
1821            self.flash(_('No ticket selected.'))
1822            self.redirect(self.url(self.context, '@@edit'))
1823            return
1824        if not isinstance(child_id, list):
1825            child_id = [child_id]
1826        deleted = []
1827        for id in child_id:
1828            # Students are not allowed to remove core tickets
1829            if not self.context[id].mandatory:
1830                del self.context[id]
1831                deleted.append(id)
1832        if len(deleted):
1833            self.flash(_('Successfully removed: ${a}',
1834                mapping = {'a':', '.join(deleted)}))
1835        self.redirect(self.url(self.context, u'@@edit'))
1836        return
1837
1838    @action(_('Register course list'), style='primary')
1839    def RegisterCourses(self, **data):
1840        IWorkflowInfo(self.context.getStudent()).fireTransition(
1841            'register_courses')
1842        self.flash(_('Course list has been registered.'))
1843        self.redirect(self.url(self.context))
1844        return
1845
1846class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1847    """Add a course ticket by student.
1848    """
1849    grok.name('ctadd')
1850    grok.require('waeup.handleStudent')
1851    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1852        'grade', 'score', 'mandatory', 'automatic', 'carry_over')
1853
1854    def update(self):
1855        if self.context.getStudent().state != PAID:
1856            emit_lock_message(self)
1857            return
1858        super(CourseTicketAddFormPage2, self).update()
1859        return
1860
1861    @action(_('Add course ticket'))
1862    def addCourseTicket(self, **data):
1863        # Safety belt
1864        if self.context.getStudent().state != PAID:
1865            return
1866        ticket = CourseTicket()
1867        course = data['course']
1868        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
1869            setattr(ticket, name, getattr(course, name))
1870        ticket.automatic = False
1871        try:
1872            self.context.addCourseTicket(ticket)
1873        except KeyError:
1874            self.flash(_('The ticket exists.'))
1875            return
1876        self.flash(_('Successfully added ${a}.',
1877            mapping = {'a':ticket.code}))
1878        self.redirect(self.url(self.context, u'@@edit'))
1879        return
1880
1881class ChangePasswordRequestPage(KofaForm):
1882    """Captcha'd page for students to request a password change.
1883    """
1884    grok.context(IUniversity)
1885    grok.name('changepw')
1886    grok.require('waeup.Anonymous')
1887    grok.template('changepw')
1888    label = _('Change my password')
1889    form_fields = grok.AutoFields(IStudentChangePassword)
1890
1891    def update(self):
1892        # Handle captcha
1893        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1894        self.captcha_result = self.captcha.verify(self.request)
1895        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1896        return
1897
1898    @action(_('Get new login credentials'), style='primary')
1899    def request(self, **data):
1900        if not self.captcha_result.is_valid:
1901            # Captcha will display error messages automatically.
1902            # No need to flash something.
1903            return
1904        # Search student
1905        cat = queryUtility(ICatalog, name='students_catalog')
1906        reg_number = data['reg_number']
1907        email = data['email']
1908        results = cat.searchResults(
1909            reg_number=(reg_number, reg_number),
1910            email=(email,email))
1911        if len(results) == 0:
1912            self.flash(_('No student record found.'))
1913            return
1914        student = list(results)[0]
1915        # Change password
1916        kofa_utils = getUtility(IKofaUtils)
1917        pwd = kofa_utils.genPassword()
1918        IUserAccount(student).setPassword(pwd)
1919        # Send email with new redentials
1920        msg = _('You have successfully changed your password for the')
1921        login_url = self.url(grok.getSite(), 'login')
1922        success = kofa_utils.sendCredentials(
1923            IUserAccount(student),pwd,login_url,msg)
1924        if success:
1925            self.flash(_('An email with your user name and password ' +
1926                'has been sent to ${a}.', mapping = {'a':email}))
1927        else:
1928            self.flash(_('An smtp server error occurred.'))
1929        return
1930
1931class SetPasswordPage(KofaPage):
1932    grok.context(IKofaObject)
1933    grok.name('setpassword')
1934    grok.require('waeup.Anonymous')
1935    grok.template('setpassword')
1936    label = _('Set password for first-time login')
1937    ac_prefix = 'PWD'
1938    pnav = 0
1939    set_button = _('Set')
1940
1941    def update(self, SUBMIT=None):
1942        self.reg_number = self.request.form.get('reg_number', None)
1943        self.ac_series = self.request.form.get('ac_series', None)
1944        self.ac_number = self.request.form.get('ac_number', None)
1945
1946        if SUBMIT is None:
1947            return
1948        hitlist = search(query=self.reg_number,
1949            searchtype='reg_number', view=self)
1950        if not hitlist:
1951            self.flash(_('No student found.'))
1952            return
1953        if len(hitlist) != 1:   # Cannot happen but anyway
1954            self.flash(_('More than one student found.'))
1955            return
1956        student = hitlist[0].context
1957        self.student_id = student.student_id
1958        student_pw = student.password
1959        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1960        code = get_access_code(pin)
1961        if not code:
1962            self.flash(_('Access code is invalid.'))
1963            return
1964        if student_pw and pin == student.adm_code:
1965            self.flash(_(
1966                'Password has already been set. Your Student Id is ${a}',
1967                mapping = {'a':self.student_id}))
1968            return
1969        elif student_pw:
1970            self.flash(
1971                _('Password has already been set. You are using the ' +
1972                'wrong Access Code.'))
1973            return
1974        # Mark pin as used (this also fires a pin related transition)
1975        # and set student password
1976        if code.state == USED:
1977            self.flash(_('Access code has already been used.'))
1978            return
1979        else:
1980            comment = _(u"invalidated")
1981            # Here we know that the ac is in state initialized so we do not
1982            # expect an exception
1983            invalidate_accesscode(pin,comment)
1984            IUserAccount(student).setPassword(self.ac_number)
1985            student.adm_code = pin
1986        self.flash(_('Password has been set. Your Student Id is ${a}',
1987            mapping = {'a':self.student_id}))
1988        return
Note: See TracBrowser for help on using the repository browser.