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

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

Cancel needs no form validation.

Simplify StudyLevelBreadcrumb?.

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