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

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

Add tests for changes in previous revision and remove additional bug found with these tests.

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