source: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/students/browser.py @ 9166

Last change on this file since 9166 was 9166, checked in by uli, 12 years ago

All remaining changes from last weeks. Sorry for the mess.

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