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

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

Remove Interswitch view components. This is part of a typical Nigerian customization and should be in waeup.custom only.

  • Property svn:keywords set to Id
File size: 68.9 KB
Line 
1## $Id: browser.py 7878 2012-03-14 09:35:47Z 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 ExportPDFPaymentSlipPage(UtilityView, grok.View):
1112    """Deliver a PDF slip of the context.
1113    """
1114    grok.context(IStudentOnlinePayment)
1115    grok.name('payment_receipt.pdf')
1116    grok.require('waeup.viewStudent')
1117    form_fields = grok.AutoFields(IStudentOnlinePayment)
1118    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
1119    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
1120    prefix = 'form'
1121    title = 'Payment Data'
1122
1123    @property
1124    def label(self):
1125        return 'Online Payment Receipt %s' % self.context.p_id
1126
1127    def render(self):
1128        if self.context.p_state != 'paid':
1129            self.flash('Ticket not yet paid.')
1130            self.redirect(self.url(self.context))
1131            return
1132        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1133            self.request)
1134        students_utils = getUtility(IStudentsUtils)
1135        return students_utils.renderPDF(self, 'payment_receipt.pdf',
1136            self.context.getStudent(), studentview)
1137
1138
1139class AccommodationManageFormPage(KofaEditFormPage):
1140    """ Page to manage bed tickets.
1141
1142    This manage form page is for both students and students officers.
1143    """
1144    grok.context(IStudentAccommodation)
1145    grok.name('index')
1146    grok.require('waeup.handleAccommodation')
1147    form_fields = grok.AutoFields(IStudentAccommodation)
1148    grok.template('accommodationmanagepage')
1149    pnav = 4
1150    officers_only_actions = [_('Remove selected')]
1151
1152    @property
1153    def label(self):
1154        return _('${a}: Accommodation',
1155            mapping = {'a':self.context.__parent__.display_fullname})
1156
1157    def update(self):
1158        super(AccommodationManageFormPage, self).update()
1159        datatable.need()
1160        warning.need()
1161        return
1162
1163    @jsaction(_('Remove selected'))
1164    def delBedTickets(self, **data):
1165        if getattr(self.request.principal, 'user_type', None) == 'student':
1166            self.flash(_('You are not allowed to remove bed tickets.'))
1167            self.redirect(self.url(self.context))
1168            return
1169        form = self.request.form
1170        if form.has_key('val_id'):
1171            child_id = form['val_id']
1172        else:
1173            self.flash(_('No bed ticket selected.'))
1174            self.redirect(self.url(self.context))
1175            return
1176        if not isinstance(child_id, list):
1177            child_id = [child_id]
1178        deleted = []
1179        for id in child_id:
1180            del self.context[id]
1181            deleted.append(id)
1182        if len(deleted):
1183            self.flash(_('Successfully removed: ${a}',
1184                mapping = {'a':', '.join(deleted)}))
1185            write_log_message(self,'removed: % s' % ', '.join(deleted))
1186        self.redirect(self.url(self.context))
1187        return
1188
1189    @property
1190    def selected_actions(self):
1191        if getattr(self.request.principal, 'user_type', None) == 'student':
1192            return [action for action in self.actions
1193                    if not action.label in self.officers_only_actions]
1194        return self.actions
1195
1196class BedTicketAddPage(KofaPage):
1197    """ Page to add an online payment ticket
1198    """
1199    grok.context(IStudentAccommodation)
1200    grok.name('add')
1201    grok.require('waeup.handleAccommodation')
1202    grok.template('enterpin')
1203    ac_prefix = 'HOS'
1204    label = _('Add bed ticket')
1205    pnav = 4
1206    buttonname = _('Create bed ticket')
1207    notice = ''
1208
1209    def update(self, SUBMIT=None):
1210        student = self.context.getStudent()
1211        students_utils = getUtility(IStudentsUtils)
1212        acc_details  = students_utils.getAccommodationDetails(student)
1213        if not acc_details:
1214            self.flash(_("Your data are incomplete."))
1215            self.redirect(self.url(self.context))
1216            return
1217        if not student.state in acc_details['allowed_states']:
1218            self.flash(_("You are in the wrong registration state."))
1219            self.redirect(self.url(self.context))
1220            return
1221        if student['studycourse'].current_session != acc_details[
1222            'booking_session']:
1223            self.flash(
1224                _('Your current session does not match accommodation session.'))
1225            self.redirect(self.url(self.context))
1226            return
1227        if str(acc_details['booking_session']) in self.context.keys():
1228            self.flash(
1229                _('You already booked a bed space in current ' \
1230                    + 'accommodation session.'))
1231            self.redirect(self.url(self.context))
1232            return
1233        self.ac_series = self.request.form.get('ac_series', None)
1234        self.ac_number = self.request.form.get('ac_number', None)
1235        if SUBMIT is None:
1236            return
1237        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1238        code = get_access_code(pin)
1239        if not code:
1240            self.flash(_('Activation code is invalid.'))
1241            return
1242        # Search and book bed
1243        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1244        entries = cat.searchResults(
1245            owner=(student.student_id,student.student_id))
1246        if len(entries):
1247            # If bed space has bee manually allocated use this bed
1248            bed = [entry for entry in entries][0]
1249        else:
1250            # else search for other available beds
1251            entries = cat.searchResults(
1252                bed_type=(acc_details['bt'],acc_details['bt']))
1253            available_beds = [
1254                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1255            if available_beds:
1256                students_utils = getUtility(IStudentsUtils)
1257                bed = students_utils.selectBed(available_beds)
1258                bed.bookBed(student.student_id)
1259            else:
1260                self.flash(_('There is no free bed in your category ${a}.',
1261                    mapping = {'a':acc_details['bt']}))
1262                return
1263        # Mark pin as used (this also fires a pin related transition)
1264        if code.state == USED:
1265            self.flash(_('Activation code has already been used.'))
1266            return
1267        else:
1268            comment = _(u'invalidated')
1269            # Here we know that the ac is in state initialized so we do not
1270            # expect an exception, but the owner might be different
1271            if not invalidate_accesscode(
1272                pin,comment,self.context.getStudent().student_id):
1273                self.flash(_('You are not the owner of this access code.'))
1274                return
1275        # Create bed ticket
1276        bedticket = createObject(u'waeup.BedTicket')
1277        bedticket.booking_code = pin
1278        bedticket.booking_session = acc_details['booking_session']
1279        bedticket.bed_type = acc_details['bt']
1280        bedticket.bed = bed
1281        hall_title = bed.__parent__.hostel_name
1282        coordinates = bed.getBedCoordinates()[1:]
1283        block, room_nr, bed_nr = coordinates
1284        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1285            'a':hall_title, 'b':block,
1286            'c':room_nr, 'd':bed_nr,
1287            'e':bed.bed_type})
1288        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1289        bedticket.bed_coordinates = translate(
1290            bc, 'waeup.kofa',target_language=portal_language)
1291        key = str(acc_details['booking_session'])
1292        self.context[key] = bedticket
1293        self.flash(_('Bed ticket created and bed booked: ${a}',
1294            mapping = {'a':bedticket.bed_coordinates}))
1295        self.redirect(self.url(self.context))
1296        return
1297
1298class BedTicketDisplayFormPage(KofaDisplayFormPage):
1299    """ Page to display bed tickets
1300    """
1301    grok.context(IBedTicket)
1302    grok.name('index')
1303    grok.require('waeup.handleAccommodation')
1304    form_fields = grok.AutoFields(IBedTicket)
1305    form_fields[
1306        'booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1307    pnav = 4
1308
1309    @property
1310    def label(self):
1311        return _('Bed Ticket for Session ${a}',
1312            mapping = {'a':self.context.getSessionString()})
1313
1314class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1315    """Deliver a PDF slip of the context.
1316    """
1317    grok.context(IBedTicket)
1318    grok.name('bed_allocation.pdf')
1319    grok.require('waeup.handleAccommodation')
1320    form_fields = grok.AutoFields(IBedTicket)
1321    form_fields['booking_date'].custom_widget = FriendlyDateDisplayWidget('le')
1322    prefix = 'form'
1323
1324    @property
1325    def title(self):
1326        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1327        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1328            target_language=portal_language)
1329
1330    @property
1331    def label(self):
1332        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1333        return translate(_('Bed Allocation: '),
1334            'waeup.kofa', target_language=portal_language) \
1335            + ' %s' % self.context.bed_coordinates
1336
1337    def render(self):
1338        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1339            self.request)
1340        students_utils = getUtility(IStudentsUtils)
1341        return students_utils.renderPDF(
1342            self, 'bed_allocation.pdf',
1343            self.context.getStudent(), studentview)
1344
1345class BedTicketRelocationPage(UtilityView, grok.View):
1346    """ Callback view
1347    """
1348    grok.context(IBedTicket)
1349    grok.name('relocate')
1350    grok.require('waeup.manageHostels')
1351
1352    # Relocate student if student parameters have changed or the bed_type
1353    # of the bed has changed
1354    def update(self):
1355        student = self.context.getStudent()
1356        students_utils = getUtility(IStudentsUtils)
1357        acc_details  = students_utils.getAccommodationDetails(student)
1358        if self.context.bed != None and \
1359              'reserved' in self.context.bed.bed_type:
1360            self.flash(_("Students in reserved beds can't be relocated."))
1361            self.redirect(self.url(self.context))
1362            return
1363        if acc_details['bt'] == self.context.bed_type and \
1364                self.context.bed != None and \
1365                self.context.bed.bed_type == self.context.bed_type:
1366            self.flash(_("Student can't be relocated."))
1367            self.redirect(self.url(self.context))
1368            return
1369        # Search a bed
1370        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1371        entries = cat.searchResults(
1372            owner=(student.student_id,student.student_id))
1373        if len(entries) and self.context.bed == None:
1374            # If booking has been cancelled but other bed space has been
1375            # manually allocated after cancellation use this bed
1376            new_bed = [entry for entry in entries][0]
1377        else:
1378            # Search for other available beds
1379            entries = cat.searchResults(
1380                bed_type=(acc_details['bt'],acc_details['bt']))
1381            available_beds = [
1382                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1383            if available_beds:
1384                students_utils = getUtility(IStudentsUtils)
1385                new_bed = students_utils.selectBed(available_beds)
1386                new_bed.bookBed(student.student_id)
1387            else:
1388                self.flash(_('There is no free bed in your category ${a}.',
1389                    mapping = {'a':acc_details['bt']}))
1390                self.redirect(self.url(self.context))
1391                return
1392        # Release old bed if exists
1393        if self.context.bed != None:
1394            self.context.bed.owner = NOT_OCCUPIED
1395            notify(grok.ObjectModifiedEvent(self.context.bed))
1396        # Alocate new bed
1397        self.context.bed_type = acc_details['bt']
1398        self.context.bed = new_bed
1399        hall_title = new_bed.__parent__.hostel_name
1400        coordinates = new_bed.getBedCoordinates()[1:]
1401        block, room_nr, bed_nr = coordinates
1402        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1403            'a':hall_title, 'b':block,
1404            'c':room_nr, 'd':bed_nr,
1405            'e':new_bed.bed_type})
1406        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1407        self.context.bed_coordinates = translate(
1408            bc, 'waeup.kofa',target_language=portal_language)
1409        self.flash(_('Student relocated: ${a}',
1410            mapping = {'a':self.context.bed_coordinates}))
1411        self.redirect(self.url(self.context))
1412        return
1413
1414    def render(self):
1415        return
1416
1417class StudentHistoryPage(KofaPage):
1418    """ Page to display student clearance data
1419    """
1420    grok.context(IStudent)
1421    grok.name('history')
1422    grok.require('waeup.viewStudent')
1423    grok.template('studenthistory')
1424    pnav = 4
1425
1426    @property
1427    def label(self):
1428        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1429
1430# Pages for students only
1431
1432class StudentBaseEditFormPage(KofaEditFormPage):
1433    """ View to edit student base data
1434    """
1435    grok.context(IStudent)
1436    grok.name('edit_base')
1437    grok.require('waeup.handleStudent')
1438    form_fields = grok.AutoFields(IStudentBase).select(
1439        'email', 'phone')
1440    form_fields['phone'].custom_widget = PhoneWidget
1441    label = _('Edit base data')
1442    pnav = 4
1443
1444    @action(_('Save'), style='primary')
1445    def save(self, **data):
1446        msave(self, **data)
1447        return
1448
1449class StudentChangePasswordPage(KofaEditFormPage):
1450    """ View to manage student base data
1451    """
1452    grok.context(IStudent)
1453    grok.name('change_password')
1454    grok.require('waeup.handleStudent')
1455    grok.template('change_password')
1456    label = _('Change password')
1457    pnav = 4
1458
1459    @action(_('Save'), style='primary')
1460    def save(self, **data):
1461        form = self.request.form
1462        password = form.get('change_password', None)
1463        password_ctl = form.get('change_password_repeat', None)
1464        if password:
1465            validator = getUtility(IPasswordValidator)
1466            errors = validator.validate_password(password, password_ctl)
1467            if not errors:
1468                IUserAccount(self.context).setPassword(password)
1469                write_log_message(self, 'saved: password')
1470                self.flash(_('Password changed.'))
1471            else:
1472                self.flash( ' '.join(errors))
1473        return
1474
1475class StudentFilesUploadPage(KofaPage):
1476    """ View to upload files by student
1477    """
1478    grok.context(IStudent)
1479    grok.name('change_portrait')
1480    grok.require('waeup.uploadStudentFile')
1481    grok.template('filesuploadpage')
1482    label = _('Upload portrait')
1483    pnav = 4
1484
1485    def update(self):
1486        if self.context.getStudent().state != ADMITTED:
1487            emit_lock_message(self)
1488            return
1489        super(StudentFilesUploadPage, self).update()
1490        return
1491
1492class StartClearancePage(KofaPage):
1493    grok.context(IStudent)
1494    grok.name('start_clearance')
1495    grok.require('waeup.handleStudent')
1496    grok.template('enterpin')
1497    label = _('Start clearance')
1498    ac_prefix = 'CLR'
1499    notice = ''
1500    pnav = 4
1501    buttonname = _('Start clearance now')
1502
1503    @property
1504    def all_required_fields_filled(self):
1505        if self.context.email and self.context.phone:
1506            return True
1507        return False
1508
1509    @property
1510    def portrait_uploaded(self):
1511        store = getUtility(IExtFileStore)
1512        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1513            return True
1514        return False
1515
1516    def update(self, SUBMIT=None):
1517        if not self.context.state == ADMITTED:
1518            self.flash(_("Wrong state"))
1519            self.redirect(self.url(self.context))
1520            return
1521        if not self.portrait_uploaded:
1522            self.flash(_("No portrait uploaded."))
1523            self.redirect(self.url(self.context, 'change_portrait'))
1524            return
1525        if not self.all_required_fields_filled:
1526            self.flash(_("Not all required fields filled."))
1527            self.redirect(self.url(self.context, 'edit_base'))
1528            return
1529        self.ac_series = self.request.form.get('ac_series', None)
1530        self.ac_number = self.request.form.get('ac_number', None)
1531
1532        if SUBMIT is None:
1533            return
1534        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1535        code = get_access_code(pin)
1536        if not code:
1537            self.flash(_('Activation code is invalid.'))
1538            return
1539        # Mark pin as used (this also fires a pin related transition)
1540        # and fire transition start_clearance
1541        if code.state == USED:
1542            self.flash(_('Activation code has already been used.'))
1543            return
1544        else:
1545            comment = _(u"invalidated")
1546            # Here we know that the ac is in state initialized so we do not
1547            # expect an exception, but the owner might be different
1548            if not invalidate_accesscode(pin,comment,self.context.student_id):
1549                self.flash(_('You are not the owner of this access code.'))
1550                return
1551            self.context.clr_code = pin
1552        IWorkflowInfo(self.context).fireTransition('start_clearance')
1553        self.flash(_('Clearance process has been started.'))
1554        self.redirect(self.url(self.context,'cedit'))
1555        return
1556
1557class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1558    """ View to edit student clearance data by student
1559    """
1560    grok.context(IStudent)
1561    grok.name('cedit')
1562    grok.require('waeup.handleStudent')
1563    form_fields = grok.AutoFields(
1564        IStudentClearance).omit('clearance_locked')
1565    label = _('Edit clearance data')
1566    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
1567
1568    def update(self):
1569        if self.context.clearance_locked:
1570            emit_lock_message(self)
1571            return
1572        return super(StudentClearanceEditFormPage, self).update()
1573
1574    @action(_('Save'), style='primary')
1575    def save(self, **data):
1576        self.applyData(self.context, **data)
1577        self.flash(_('Clearance form has been saved.'))
1578        return
1579
1580    def dataNotComplete(self):
1581        """To be implemented in the customization package.
1582        """
1583        return False
1584
1585    @action(_('Save and request clearance'), style='primary')
1586    def requestClearance(self, **data):
1587        self.applyData(self.context, **data)
1588        if self.dataNotComplete():
1589            self.flash(self.dataNotComplete())
1590            return
1591        self.flash(_('Clearance form has been saved.'))
1592        self.redirect(self.url(self.context,'request_clearance'))
1593        return
1594
1595class RequestClearancePage(KofaPage):
1596    grok.context(IStudent)
1597    grok.name('request_clearance')
1598    grok.require('waeup.handleStudent')
1599    grok.template('enterpin')
1600    label = _('Request clearance')
1601    notice = _('Enter the CLR access code used for starting clearance.')
1602    ac_prefix = 'CLR'
1603    pnav = 4
1604    buttonname = _('Request clearance now')
1605
1606    def update(self, SUBMIT=None):
1607        self.ac_series = self.request.form.get('ac_series', None)
1608        self.ac_number = self.request.form.get('ac_number', None)
1609        if SUBMIT is None:
1610            return
1611        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1612        if self.context.clr_code != pin:
1613            self.flash(_("This isn't your CLR access code."))
1614            return
1615        state = IWorkflowState(self.context).getState()
1616        # This shouldn't happen, but the application officer
1617        # might have forgotten to lock the form after changing the state
1618        if state != CLEARANCE:
1619            self.flash(_('This form cannot be submitted. Wrong state!'))
1620            return
1621        IWorkflowInfo(self.context).fireTransition('request_clearance')
1622        self.flash(_('Clearance has been requested.'))
1623        self.redirect(self.url(self.context))
1624        return
1625
1626class StartCourseRegistrationPage(KofaPage):
1627    grok.context(IStudentStudyCourse)
1628    grok.name('start_course_registration')
1629    grok.require('waeup.handleStudent')
1630    grok.template('enterpin')
1631    label = _('Start course registration')
1632    ac_prefix = 'SFE'
1633    notice = ''
1634    pnav = 4
1635    buttonname = _('Start course registration now')
1636
1637    def update(self, SUBMIT=None):
1638        if not self.context.getStudent().state in (CLEARED,RETURNING):
1639            self.flash(_("Wrong state"))
1640            self.redirect(self.url(self.context))
1641            return
1642        self.ac_series = self.request.form.get('ac_series', None)
1643        self.ac_number = self.request.form.get('ac_number', None)
1644
1645        if SUBMIT is None:
1646            return
1647        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1648        code = get_access_code(pin)
1649        if not code:
1650            self.flash(_('Activation code is invalid.'))
1651            return
1652        # Mark pin as used (this also fires a pin related transition)
1653        # and fire transition start_clearance
1654        if code.state == USED:
1655            self.flash(_('Activation code has already been used.'))
1656            return
1657        else:
1658            comment = _(u"invalidated")
1659            # Here we know that the ac is in state initialized so we do not
1660            # expect an exception, but the owner might be different
1661            if not invalidate_accesscode(
1662                pin,comment,self.context.getStudent().student_id):
1663                self.flash(_('You are not the owner of this access code.'))
1664                return
1665        if self.context.getStudent().state == CLEARED:
1666            IWorkflowInfo(self.context.getStudent()).fireTransition(
1667                'pay_first_school_fee')
1668        elif self.context.getStudent().state == RETURNING:
1669            IWorkflowInfo(self.context.getStudent()).fireTransition(
1670                'pay_school_fee')
1671        self.flash(_('Course registration has been started.'))
1672        self.redirect(self.url(self.context))
1673        return
1674
1675class AddStudyLevelFormPage(KofaEditFormPage):
1676    """ Page for students to add current study levels
1677    """
1678    grok.context(IStudentStudyCourse)
1679    grok.name('add')
1680    grok.require('waeup.handleStudent')
1681    grok.template('studyleveladdpage')
1682    form_fields = grok.AutoFields(IStudentStudyCourse)
1683    pnav = 4
1684
1685    @property
1686    def label(self):
1687        studylevelsource = StudyLevelSource().factory
1688        code = self.context.current_level
1689        title = studylevelsource.getTitle(self.context, code)
1690        return _('Add current level ${a}', mapping = {'a':title})
1691
1692    def update(self):
1693        if self.context.getStudent().state != PAID:
1694            emit_lock_message(self)
1695            return
1696        super(AddStudyLevelFormPage, self).update()
1697        return
1698
1699    @action(_('Create course list now'), style='primary')
1700    def addStudyLevel(self, **data):
1701        studylevel = StudentStudyLevel()
1702        studylevel.level = self.context.current_level
1703        studylevel.level_session = self.context.current_session
1704        try:
1705            self.context.addStudentStudyLevel(
1706                self.context.certificate,studylevel)
1707        except KeyError:
1708            self.flash(_('This level exists.'))
1709        self.redirect(self.url(self.context))
1710        return
1711
1712class StudyLevelEditFormPage(KofaEditFormPage):
1713    """ Page to edit the student study level data by students
1714    """
1715    grok.context(IStudentStudyLevel)
1716    grok.name('edit')
1717    grok.require('waeup.handleStudent')
1718    grok.template('studyleveleditpage')
1719    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1720        'level_session', 'level_verdict')
1721    pnav = 4
1722
1723    def update(self):
1724        if self.context.getStudent().state != PAID:
1725            emit_lock_message(self)
1726            return
1727        super(StudyLevelEditFormPage, self).update()
1728        datatable.need()
1729        warning.need()
1730        return
1731
1732    @property
1733    def label(self):
1734        # Here we know that the cookie has been set
1735        lang = self.request.cookies.get('kofa.language')
1736        level_title = translate(self.context.level_title, 'waeup.kofa',
1737            target_language=lang)
1738        return _('Add and remove course tickets of study level ${a}',
1739            mapping = {'a':level_title})
1740
1741    @property
1742    def total_credits(self):
1743        total_credits = 0
1744        for key, val in self.context.items():
1745            total_credits += val.credits
1746        return total_credits
1747
1748    @action(_('Add course ticket'))
1749    def addCourseTicket(self, **data):
1750        self.redirect(self.url(self.context, 'ctadd'))
1751
1752    @jsaction(_('Remove selected tickets'))
1753    def delCourseTicket(self, **data):
1754        form = self.request.form
1755        if form.has_key('val_id'):
1756            child_id = form['val_id']
1757        else:
1758            self.flash(_('No ticket selected.'))
1759            self.redirect(self.url(self.context, '@@edit'))
1760            return
1761        if not isinstance(child_id, list):
1762            child_id = [child_id]
1763        deleted = []
1764        for id in child_id:
1765            # Students are not allowed to remove core tickets
1766            if not self.context[id].mandatory:
1767                del self.context[id]
1768                deleted.append(id)
1769        if len(deleted):
1770            self.flash(_('Successfully removed: ${a}',
1771                mapping = {'a':', '.join(deleted)}))
1772        self.redirect(self.url(self.context, u'@@edit'))
1773        return
1774
1775    @action(_('Register course list'), style='primary')
1776    def RegisterCourses(self, **data):
1777        IWorkflowInfo(self.context.getStudent()).fireTransition(
1778            'register_courses')
1779        self.flash(_('Course list has been registered.'))
1780        self.redirect(self.url(self.context))
1781        return
1782
1783class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1784    """Add a course ticket by student.
1785    """
1786    grok.name('ctadd')
1787    grok.require('waeup.handleStudent')
1788    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1789        'grade', 'score', 'mandatory', 'automatic', 'carry_over')
1790
1791    def update(self):
1792        if self.context.getStudent().state != PAID:
1793            emit_lock_message(self)
1794            return
1795        super(CourseTicketAddFormPage2, self).update()
1796        return
1797
1798    @action(_('Add course ticket'))
1799    def addCourseTicket(self, **data):
1800        # Safety belt
1801        if self.context.getStudent().state != PAID:
1802            return
1803        ticket = CourseTicket()
1804        course = data['course']
1805        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
1806            setattr(ticket, name, getattr(course, name))
1807        ticket.automatic = False
1808        try:
1809            self.context.addCourseTicket(ticket)
1810        except KeyError:
1811            self.flash(_('The ticket exists.'))
1812            return
1813        self.flash(_('Successfully added ${a}.',
1814            mapping = {'a':ticket.code}))
1815        self.redirect(self.url(self.context, u'@@edit'))
1816        return
1817
1818class ChangePasswordRequestPage(KofaForm):
1819    """Captcha'd page for students to request a password change.
1820    """
1821    grok.context(IUniversity)
1822    grok.name('changepw')
1823    grok.require('waeup.Anonymous')
1824    grok.template('changepw')
1825    label = _('Change my password')
1826    form_fields = grok.AutoFields(IStudentChangePassword)
1827
1828    def update(self):
1829        # Handle captcha
1830        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1831        self.captcha_result = self.captcha.verify(self.request)
1832        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1833        return
1834
1835    @action(_('Get new login credentials'), style='primary')
1836    def request(self, **data):
1837        if not self.captcha_result.is_valid:
1838            # Captcha will display error messages automatically.
1839            # No need to flash something.
1840            return
1841        # Search student
1842        cat = queryUtility(ICatalog, name='students_catalog')
1843        reg_number = data['reg_number']
1844        email = data['email']
1845        results = cat.searchResults(
1846            reg_number=(reg_number, reg_number),
1847            email=(email,email))
1848        if len(results) == 0:
1849            self.flash(_('No student record found.'))
1850            return
1851        student = list(results)[0]
1852        # Change password
1853        kofa_utils = getUtility(IKofaUtils)
1854        pwd = kofa_utils.genPassword()
1855        IUserAccount(student).setPassword(pwd)
1856        # Send email with new redentials
1857        msg = _('You have successfully changed your password for the')
1858        login_url = self.url(grok.getSite(), 'login')
1859        success = kofa_utils.sendCredentials(
1860            IUserAccount(student),pwd,login_url,msg)
1861        if success:
1862            self.flash(_('An email with your user name and password ' +
1863                'has been sent to ${a}.', mapping = {'a':email}))
1864        else:
1865            self.flash(_('An smtp server error occurred.'))
1866        return
1867
1868class SetPasswordPage(KofaPage):
1869    grok.context(IKofaObject)
1870    grok.name('setpassword')
1871    grok.require('waeup.Anonymous')
1872    grok.template('setpassword')
1873    label = _('Set password for first-time login')
1874    ac_prefix = 'PWD'
1875    pnav = 0
1876    set_button = _('Set')
1877
1878    def update(self, SUBMIT=None):
1879        self.reg_number = self.request.form.get('reg_number', None)
1880        self.ac_series = self.request.form.get('ac_series', None)
1881        self.ac_number = self.request.form.get('ac_number', None)
1882
1883        if SUBMIT is None:
1884            return
1885        hitlist = search(query=self.reg_number,
1886            searchtype='reg_number', view=self)
1887        if not hitlist:
1888            self.flash(_('No student found.'))
1889            return
1890        if len(hitlist) != 1:   # Cannot happen but anyway
1891            self.flash(_('More than one student found.'))
1892            return
1893        student = hitlist[0].context
1894        self.student_id = student.student_id
1895        student_pw = student.password
1896        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1897        code = get_access_code(pin)
1898        if not code:
1899            self.flash(_('Access code is invalid.'))
1900            return
1901        if student_pw and pin == student.adm_code:
1902            self.flash(_(
1903                'Password has already been set. Your Student Id is ${a}',
1904                mapping = {'a':self.student_id}))
1905            return
1906        elif student_pw:
1907            self.flash(
1908                _('Password has already been set. You are using the ' +
1909                'wrong Access Code.'))
1910            return
1911        # Mark pin as used (this also fires a pin related transition)
1912        # and set student password
1913        if code.state == USED:
1914            self.flash(_('Access code has already been used.'))
1915            return
1916        else:
1917            comment = _(u"invalidated")
1918            # Here we know that the ac is in state initialized so we do not
1919            # expect an exception
1920            invalidate_accesscode(pin,comment)
1921            IUserAccount(student).setPassword(self.ac_number)
1922            student.adm_code = pin
1923        self.flash(_('Password has been set. Your Student Id is ${a}',
1924            mapping = {'a':self.student_id}))
1925        return
Note: See TracBrowser for help on using the repository browser.