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

Last change on this file since 8916 was 8903, checked in by Henrik Bettermann, 12 years ago

Add personal manage form page including button viewlet.

  • Property svn:keywords set to Id
File size: 72.2 KB
Line 
1## $Id: browser.py 8903 2012-07-03 21:07:34Z 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 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 import (
36    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
37    ContactAdminForm, KofaForm, NullValidator)
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.browser.interfaces import ICaptchaManager
42from waeup.kofa.interfaces import (
43    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
44    IKofaUtils, IUniversity)
45from waeup.kofa.interfaces import MessageFactory as _
46from waeup.kofa.widgets.datewidget import (
47    FriendlyDateWidget, FriendlyDateDisplayWidget,
48    FriendlyDatetimeDisplayWidget)
49from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
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, IStudentRequestPW
57    )
58from waeup.kofa.students.catalog import search
59from waeup.kofa.students.workflow import (CREATED, 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, to_timezone
66from waeup.kofa.mandates.mandate import PasswordMandate
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 StudentPersonalManageFormPage(KofaEditFormPage):
560    """ Page to manage personal data
561    """
562    grok.context(IStudent)
563    grok.name('manage_personal')
564    grok.require('waeup.manageStudent')
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 StudentPersonalEditFormPage(StudentPersonalManageFormPage):
575    """ Page to edit personal data
576    """
577    grok.name('edit_personal')
578    grok.require('waeup.handleStudent')
579    label = _('Edit personal data')
580    pnav = 4
581
582class StudyCourseDisplayFormPage(KofaDisplayFormPage):
583    """ Page to display the student study course data
584    """
585    grok.context(IStudentStudyCourse)
586    grok.name('index')
587    grok.require('waeup.viewStudent')
588    form_fields = grok.AutoFields(IStudentStudyCourse)
589    grok.template('studycoursepage')
590    pnav = 4
591
592    @property
593    def label(self):
594        return _('${a}: Study Course',
595            mapping = {'a':self.context.__parent__.display_fullname})
596
597    @property
598    def current_mode(self):
599        if self.context.certificate is not None:
600            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
601            return studymodes_dict[self.context.certificate.study_mode]
602        return
603
604    @property
605    def department(self):
606        if self.context.certificate is not None:
607            return self.context.certificate.__parent__.__parent__
608        return
609
610    @property
611    def faculty(self):
612        if self.context.certificate is not None:
613            return self.context.certificate.__parent__.__parent__.__parent__
614        return
615
616class StudyCourseManageFormPage(KofaEditFormPage):
617    """ Page to edit the student study course data
618    """
619    grok.context(IStudentStudyCourse)
620    grok.name('manage')
621    grok.require('waeup.manageStudent')
622    grok.template('studycoursemanagepage')
623    form_fields = grok.AutoFields(IStudentStudyCourse)
624    label = _('Manage study course')
625    pnav = 4
626    taboneactions = [_('Save'),_('Cancel')]
627    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
628    tabthreeactions = [_('Add study level')]
629
630    def update(self):
631        super(StudyCourseManageFormPage, self).update()
632        tabs.need()
633        self.tab1 = self.tab2 = ''
634        qs = self.request.get('QUERY_STRING', '')
635        if not qs:
636            qs = 'tab1'
637        setattr(self, qs, 'active')
638        warning.need()
639        datatable.need()
640        return
641
642    @action(_('Save'), style='primary')
643    def save(self, **data):
644        try:
645            msave(self, **data)
646        except ConstraintNotSatisfied:
647            # The selected level might not exist in certificate
648            self.flash(_('Current level not available for certificate.'))
649            return
650        notify(grok.ObjectModifiedEvent(self.context.__parent__))
651        return
652
653    @property
654    def level_dict(self):
655        studylevelsource = StudyLevelSource().factory
656        for code in studylevelsource.getValues(self.context):
657            title = studylevelsource.getTitle(self.context, code)
658            yield(dict(code=code, title=title))
659
660    @action(_('Add study level'))
661    def addStudyLevel(self, **data):
662        level_code = self.request.form.get('addlevel', None)
663        studylevel = createObject(u'waeup.StudentStudyLevel')
664        studylevel.level = int(level_code)
665        try:
666            self.context.addStudentStudyLevel(
667                self.context.certificate,studylevel)
668            self.flash(_('Study level has been added.'))
669        except KeyError:
670            self.flash(_('This level exists.'))
671        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
672        return
673
674    @jsaction(_('Remove selected levels'))
675    def delStudyLevels(self, **data):
676        form = self.request.form
677        if form.has_key('val_id'):
678            child_id = form['val_id']
679        else:
680            self.flash(_('No study level selected.'))
681            self.redirect(self.url(self.context, '@@manage')+'?tab2')
682            return
683        if not isinstance(child_id, list):
684            child_id = [child_id]
685        deleted = []
686        for id in child_id:
687            del self.context[id]
688            deleted.append(id)
689        if len(deleted):
690            self.flash(_('Successfully removed: ${a}',
691                mapping = {'a':', '.join(deleted)}))
692        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
693        return
694
695class StudyLevelDisplayFormPage(KofaDisplayFormPage):
696    """ Page to display student study levels
697    """
698    grok.context(IStudentStudyLevel)
699    grok.name('index')
700    grok.require('waeup.viewStudent')
701    form_fields = grok.AutoFields(IStudentStudyLevel)
702    grok.template('studylevelpage')
703    pnav = 4
704
705    def update(self):
706        super(StudyLevelDisplayFormPage, self).update()
707        datatable.need()
708        return
709
710    @property
711    def translated_values(self):
712        lang = self.request.cookies.get('kofa.language')
713        for value in self.context.values():
714            value_dict = dict([i for i in value.__dict__.items()])
715            value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
716                target_language=lang)
717            value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
718                target_language=lang)
719            value_dict['automatic'] = translate(str(value.automatic), 'zope',
720                target_language=lang)
721            yield value_dict
722
723    @property
724    def label(self):
725        # Here we know that the cookie has been set
726        lang = self.request.cookies.get('kofa.language')
727        level_title = translate(self.context.level_title, 'waeup.kofa',
728            target_language=lang)
729        return _('${a}: Study Level ${b}', mapping = {
730            'a':self.context.student.display_fullname,
731            'b':level_title})
732
733    @property
734    def total_credits(self):
735        total_credits = 0
736        for key, val in self.context.items():
737            total_credits += val.credits
738        return total_credits
739
740class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
741    """Deliver a PDF slip of the context.
742    """
743    grok.context(IStudentStudyLevel)
744    grok.name('course_registration.pdf')
745    grok.require('waeup.viewStudent')
746    form_fields = grok.AutoFields(IStudentStudyLevel)
747    prefix = 'form'
748
749    @property
750    def title(self):
751        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
752        return translate(_('Level Data'), 'waeup.kofa',
753            target_language=portal_language)
754
755    @property
756    def content_title(self):
757        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
758        return translate(_('Course List'), 'waeup.kofa',
759            target_language=portal_language)
760
761    @property
762    def label(self):
763        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
764        lang = self.request.cookies.get('kofa.language', portal_language)
765        level_title = translate(self.context.level_title, 'waeup.kofa',
766            target_language=lang)
767        return translate(_('Course Registration Slip'),
768            'waeup.kofa', target_language=portal_language) \
769            + ' %s' % level_title
770
771    def render(self):
772        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
773        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
774        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
775        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
776        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
777        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
778        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
779        Mand = translate(_('Mand.'), 'waeup.kofa', target_language=portal_language)
780        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
781        studentview = StudentBaseDisplayFormPage(self.context.student,
782            self.request)
783        students_utils = getUtility(IStudentsUtils)
784        tabledata = sorted(self.context.values(),
785            key=lambda value: str(value.semester) + value.code)
786        return students_utils.renderPDF(
787            self, 'course_registration.pdf',
788            self.context.student, studentview,
789            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
790                         (Title,'title', 5),
791                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
792                         (Cred, 'credits', 1.5),
793                         (Mand, 'mandatory', 1.5),
794                         (Score, 'score', 1.5),
795                         #('Auto', 'automatic', 1.5)
796                         ],
797            tabledata=tabledata)
798
799class StudyLevelManageFormPage(KofaEditFormPage):
800    """ Page to edit the student study level data
801    """
802    grok.context(IStudentStudyLevel)
803    grok.name('manage')
804    grok.require('waeup.manageStudent')
805    grok.template('studylevelmanagepage')
806    form_fields = grok.AutoFields(IStudentStudyLevel)
807    pnav = 4
808    taboneactions = [_('Save'),_('Cancel')]
809    tabtwoactions = [_('Add course ticket'),
810        _('Remove selected tickets'),_('Cancel')]
811
812    def update(self):
813        super(StudyLevelManageFormPage, self).update()
814        tabs.need()
815        self.tab1 = self.tab2 = ''
816        qs = self.request.get('QUERY_STRING', '')
817        if not qs:
818            qs = 'tab1'
819        setattr(self, qs, 'active')
820        warning.need()
821        datatable.need()
822        return
823
824    @property
825    def label(self):
826        # Here we know that the cookie has been set
827        lang = self.request.cookies.get('kofa.language')
828        level_title = translate(self.context.level_title, 'waeup.kofa',
829            target_language=lang)
830        return _('Manage study level ${a}',
831            mapping = {'a':level_title})
832
833    @action(_('Save'), style='primary')
834    def save(self, **data):
835        msave(self, **data)
836        return
837
838    @action(_('Add course ticket'))
839    def addCourseTicket(self, **data):
840        self.redirect(self.url(self.context, '@@add'))
841
842    @jsaction(_('Remove selected tickets'))
843    def delCourseTicket(self, **data):
844        form = self.request.form
845        if form.has_key('val_id'):
846            child_id = form['val_id']
847        else:
848            self.flash(_('No ticket selected.'))
849            self.redirect(self.url(self.context, '@@manage')+'?tab2')
850            return
851        if not isinstance(child_id, list):
852            child_id = [child_id]
853        deleted = []
854        for id in child_id:
855            del self.context[id]
856            deleted.append(id)
857        if len(deleted):
858            self.flash(_('Successfully removed: ${a}',
859                mapping = {'a':', '.join(deleted)}))
860        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
861        return
862
863class ValidateCoursesPage(UtilityView, grok.View):
864    """ Validate course list by course adviser
865    """
866    grok.context(IStudentStudyLevel)
867    grok.name('validate_courses')
868    grok.require('waeup.validateStudent')
869
870    def update(self):
871        if str(self.context.__parent__.current_level) != self.context.__name__:
872            self.flash(_('This level does not correspond current level.'))
873        elif self.context.student.state == REGISTERED:
874            IWorkflowInfo(self.context.student).fireTransition(
875                'validate_courses')
876            self.flash(_('Course list has been validated.'))
877        else:
878            self.flash(_('Student is in the wrong state.'))
879        self.redirect(self.url(self.context))
880        return
881
882    def render(self):
883        return
884
885class RejectCoursesPage(UtilityView, grok.View):
886    """ Reject course list by course adviser
887    """
888    grok.context(IStudentStudyLevel)
889    grok.name('reject_courses')
890    grok.require('waeup.validateStudent')
891
892    def update(self):
893        if str(self.context.__parent__.current_level) != self.context.__name__:
894            self.flash(_('This level does not correspond current level.'))
895            self.redirect(self.url(self.context))
896            return
897        elif self.context.student.state == VALIDATED:
898            IWorkflowInfo(self.context.student).fireTransition('reset8')
899            message = _('Course list request has been annulled.')
900            self.flash(message)
901        elif self.context.student.state == REGISTERED:
902            IWorkflowInfo(self.context.student).fireTransition('reset7')
903            message = _('Course list request has been rejected:')
904            self.flash(message)
905        else:
906            self.flash(_('Student is in the wrong state.'))
907            self.redirect(self.url(self.context))
908            return
909        args = {'subject':message}
910        self.redirect(self.url(self.context.student) +
911            '/contactstudent?%s' % urlencode(args))
912        return
913
914    def render(self):
915        return
916
917class CourseTicketAddFormPage(KofaAddFormPage):
918    """Add a course ticket.
919    """
920    grok.context(IStudentStudyLevel)
921    grok.name('add')
922    grok.require('waeup.manageStudent')
923    label = _('Add course ticket')
924    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
925        'score', 'automatic', 'carry_over')
926    pnav = 4
927
928    @action(_('Add course ticket'))
929    def addCourseTicket(self, **data):
930        ticket = createObject(u'waeup.CourseTicket')
931        course = data['course']
932        ticket.automatic = False
933        ticket.carry_over = False
934        ticket.code = course.code
935        ticket.title = course.title
936        ticket.fcode = course.__parent__.__parent__.__parent__.code
937        ticket.dcode = course.__parent__.__parent__.code
938        ticket.credits = course.credits
939        ticket.passmark = course.passmark
940        ticket.semester = course.semester
941        try:
942            self.context.addCourseTicket(ticket)
943        except KeyError:
944            self.flash(_('The ticket exists.'))
945            return
946        self.flash(_('Successfully added ${a}.',
947            mapping = {'a':ticket.code}))
948        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
949        return
950
951    @action(_('Cancel'), validator=NullValidator)
952    def cancel(self, **data):
953        self.redirect(self.url(self.context))
954
955class CourseTicketDisplayFormPage(KofaDisplayFormPage):
956    """ Page to display course tickets
957    """
958    grok.context(ICourseTicket)
959    grok.name('index')
960    grok.require('waeup.viewStudent')
961    form_fields = grok.AutoFields(ICourseTicket)
962    grok.template('courseticketpage')
963    pnav = 4
964
965    @property
966    def label(self):
967        return _('${a}: Course Ticket ${b}', mapping = {
968            'a':self.context.student.display_fullname,
969            'b':self.context.code})
970
971class CourseTicketManageFormPage(KofaEditFormPage):
972    """ Page to manage course tickets
973    """
974    grok.context(ICourseTicket)
975    grok.name('manage')
976    grok.require('waeup.manageStudent')
977    form_fields = grok.AutoFields(ICourseTicket)
978    grok.template('courseticketmanagepage')
979    pnav = 4
980
981    @property
982    def label(self):
983        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
984
985    @action('Save', style='primary')
986    def save(self, **data):
987        msave(self, **data)
988        return
989
990class PaymentsManageFormPage(KofaEditFormPage):
991    """ Page to manage the student payments
992
993    This manage form page is for both students and students officers.
994    """
995    grok.context(IStudentPaymentsContainer)
996    grok.name('index')
997    grok.require('waeup.payStudent')
998    form_fields = grok.AutoFields(IStudentPaymentsContainer)
999    grok.template('paymentsmanagepage')
1000    pnav = 4
1001
1002    def unremovable(self, ticket):
1003        usertype = getattr(self.request.principal, 'user_type', None)
1004        if not usertype:
1005            return False
1006        return (self.request.principal.user_type == 'student' and ticket.r_code)
1007
1008    @property
1009    def label(self):
1010        return _('${a}: Payments',
1011            mapping = {'a':self.context.__parent__.display_fullname})
1012
1013    def update(self):
1014        super(PaymentsManageFormPage, self).update()
1015        datatable.need()
1016        warning.need()
1017        return
1018
1019    @jsaction(_('Remove selected tickets'))
1020    def delPaymentTicket(self, **data):
1021        form = self.request.form
1022        if form.has_key('val_id'):
1023            child_id = form['val_id']
1024        else:
1025            self.flash(_('No payment selected.'))
1026            self.redirect(self.url(self.context))
1027            return
1028        if not isinstance(child_id, list):
1029            child_id = [child_id]
1030        deleted = []
1031        for id in child_id:
1032            # Students are not allowed to remove used payment tickets
1033            if not self.unremovable(self.context[id]):
1034                del self.context[id]
1035                deleted.append(id)
1036        if len(deleted):
1037            self.flash(_('Successfully removed: ${a}',
1038                mapping = {'a': ', '.join(deleted)}))
1039            self.context.writeLogMessage(
1040                self,'removed: %s' % ', '.join(deleted))
1041        self.redirect(self.url(self.context))
1042        return
1043
1044    @action(_('Add online payment ticket'))
1045    def addPaymentTicket(self, **data):
1046        self.redirect(self.url(self.context, '@@addop'))
1047
1048class OnlinePaymentAddFormPage(KofaAddFormPage):
1049    """ Page to add an online payment ticket
1050    """
1051    grok.context(IStudentPaymentsContainer)
1052    grok.name('addop')
1053    grok.require('waeup.payStudent')
1054    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1055        'p_category')
1056    label = _('Add online payment')
1057    pnav = 4
1058
1059    @action(_('Create ticket'), style='primary')
1060    def createTicket(self, **data):
1061        p_category = data['p_category']
1062        student = self.context.__parent__
1063        if p_category == 'bed_allocation' and student[
1064            'studycourse'].current_session != grok.getSite()[
1065            'hostels'].accommodation_session:
1066                self.flash(
1067                    _('Your current session does not match ' + \
1068                    'accommodation session.'))
1069                self.redirect(self.url(self.context))
1070                return
1071        students_utils = getUtility(IStudentsUtils)
1072        error, payment = students_utils.setPaymentDetails(p_category, student)
1073        if error is not None:
1074            self.flash(error)
1075            self.redirect(self.url(self.context))
1076            return
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.student.display_fullname,
1099            'b':self.context.p_id})
1100
1101class OnlinePaymentApprovePage(UtilityView, grok.View):
1102    """ Callback view
1103    """
1104    grok.context(IStudentOnlinePayment)
1105    grok.name('approve')
1106    grok.require('waeup.managePortal')
1107
1108    def update(self):
1109        success, msg, log = self.context.approveStudentPayment()
1110        if log is not None:
1111            self.context.writeLogMessage(self,log)
1112        self.flash(msg)
1113        return
1114
1115    def render(self):
1116        self.redirect(self.url(self.context, '@@index'))
1117        return
1118
1119class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1120    """ Approval view for students.
1121
1122    This view is used for browser tests only and
1123    must be neutralized in custom pages!
1124    """
1125
1126    grok.name('fake_approve')
1127    grok.require('waeup.payStudent')
1128
1129class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1130    """Deliver a PDF slip of the context.
1131    """
1132    grok.context(IStudentOnlinePayment)
1133    grok.name('payment_slip.pdf')
1134    grok.require('waeup.viewStudent')
1135    form_fields = grok.AutoFields(IStudentOnlinePayment)
1136    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1137    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1138    prefix = 'form'
1139    note = None
1140
1141    @property
1142    def title(self):
1143        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1144        return translate(_('Payment Data'), 'waeup.kofa',
1145            target_language=portal_language)
1146
1147    @property
1148    def label(self):
1149        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1150        return translate(_('Online Payment Slip'),
1151            'waeup.kofa', target_language=portal_language) \
1152            + ' %s' % self.context.p_id
1153
1154    def render(self):
1155        #if self.context.p_state != 'paid':
1156        #    self.flash('Ticket not yet paid.')
1157        #    self.redirect(self.url(self.context))
1158        #    return
1159        studentview = StudentBaseDisplayFormPage(self.context.student,
1160            self.request)
1161        students_utils = getUtility(IStudentsUtils)
1162        return students_utils.renderPDF(self, 'payment_slip.pdf',
1163            self.context.student, studentview, note=self.note)
1164
1165
1166class AccommodationManageFormPage(KofaEditFormPage):
1167    """ Page to manage bed tickets.
1168
1169    This manage form page is for both students and students officers.
1170    """
1171    grok.context(IStudentAccommodation)
1172    grok.name('index')
1173    grok.require('waeup.handleAccommodation')
1174    form_fields = grok.AutoFields(IStudentAccommodation)
1175    grok.template('accommodationmanagepage')
1176    pnav = 4
1177    officers_only_actions = [_('Remove selected')]
1178
1179    @property
1180    def label(self):
1181        return _('${a}: Accommodation',
1182            mapping = {'a':self.context.__parent__.display_fullname})
1183
1184    def update(self):
1185        super(AccommodationManageFormPage, self).update()
1186        datatable.need()
1187        warning.need()
1188        return
1189
1190    @jsaction(_('Remove selected'))
1191    def delBedTickets(self, **data):
1192        if getattr(self.request.principal, 'user_type', None) == 'student':
1193            self.flash(_('You are not allowed to remove bed tickets.'))
1194            self.redirect(self.url(self.context))
1195            return
1196        form = self.request.form
1197        if form.has_key('val_id'):
1198            child_id = form['val_id']
1199        else:
1200            self.flash(_('No bed ticket selected.'))
1201            self.redirect(self.url(self.context))
1202            return
1203        if not isinstance(child_id, list):
1204            child_id = [child_id]
1205        deleted = []
1206        for id in child_id:
1207            del self.context[id]
1208            deleted.append(id)
1209        if len(deleted):
1210            self.flash(_('Successfully removed: ${a}',
1211                mapping = {'a':', '.join(deleted)}))
1212            self.context.writeLogMessage(
1213                self,'removed: % s' % ', '.join(deleted))
1214        self.redirect(self.url(self.context))
1215        return
1216
1217    @property
1218    def selected_actions(self):
1219        if getattr(self.request.principal, 'user_type', None) == 'student':
1220            return [action for action in self.actions
1221                    if not action.label in self.officers_only_actions]
1222        return self.actions
1223
1224class BedTicketAddPage(KofaPage):
1225    """ Page to add an online payment ticket
1226    """
1227    grok.context(IStudentAccommodation)
1228    grok.name('add')
1229    grok.require('waeup.handleAccommodation')
1230    grok.template('enterpin')
1231    ac_prefix = 'HOS'
1232    label = _('Add bed ticket')
1233    pnav = 4
1234    buttonname = _('Create bed ticket')
1235    notice = ''
1236
1237    def update(self, SUBMIT=None):
1238        student = self.context.student
1239        students_utils = getUtility(IStudentsUtils)
1240        acc_details  = students_utils.getAccommodationDetails(student)
1241        if acc_details.get('expired', False):
1242            startdate = acc_details.get('startdate')
1243            enddate = acc_details.get('enddate')
1244            if startdate and enddate:
1245                tz = getUtility(IKofaUtils).tzinfo
1246                startdate = to_timezone(
1247                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1248                enddate = to_timezone(
1249                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1250                self.flash(_("Outside booking period: ${a} - ${b}",
1251                    mapping = {'a': startdate, 'b': enddate}))
1252            else:
1253                self.flash(_("Outside booking period."))
1254            self.redirect(self.url(self.context))
1255            return
1256        if not acc_details:
1257            self.flash(_("Your data are incomplete."))
1258            self.redirect(self.url(self.context))
1259            return
1260        if not student.state in acc_details['allowed_states']:
1261            self.flash(_("You are in the wrong registration state."))
1262            self.redirect(self.url(self.context))
1263            return
1264        if student['studycourse'].current_session != acc_details[
1265            'booking_session']:
1266            self.flash(
1267                _('Your current session does not match accommodation session.'))
1268            self.redirect(self.url(self.context))
1269            return
1270        if str(acc_details['booking_session']) in self.context.keys():
1271            self.flash(
1272                _('You already booked a bed space in current ' \
1273                    + 'accommodation session.'))
1274            self.redirect(self.url(self.context))
1275            return
1276        self.ac_series = self.request.form.get('ac_series', None)
1277        self.ac_number = self.request.form.get('ac_number', None)
1278        if SUBMIT is None:
1279            return
1280        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1281        code = get_access_code(pin)
1282        if not code:
1283            self.flash(_('Activation code is invalid.'))
1284            return
1285        # Search and book bed
1286        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1287        entries = cat.searchResults(
1288            owner=(student.student_id,student.student_id))
1289        if len(entries):
1290            # If bed space has bee manually allocated use this bed
1291            bed = [entry for entry in entries][0]
1292        else:
1293            # else search for other available beds
1294            entries = cat.searchResults(
1295                bed_type=(acc_details['bt'],acc_details['bt']))
1296            available_beds = [
1297                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1298            if available_beds:
1299                students_utils = getUtility(IStudentsUtils)
1300                bed = students_utils.selectBed(available_beds)
1301                bed.bookBed(student.student_id)
1302            else:
1303                self.flash(_('There is no free bed in your category ${a}.',
1304                    mapping = {'a':acc_details['bt']}))
1305                return
1306        # Mark pin as used (this also fires a pin related transition)
1307        if code.state == USED:
1308            self.flash(_('Activation code has already been used.'))
1309            return
1310        else:
1311            comment = _(u'invalidated')
1312            # Here we know that the ac is in state initialized so we do not
1313            # expect an exception, but the owner might be different
1314            if not invalidate_accesscode(
1315                pin,comment,self.context.student.student_id):
1316                self.flash(_('You are not the owner of this access code.'))
1317                return
1318        # Create bed ticket
1319        bedticket = createObject(u'waeup.BedTicket')
1320        bedticket.booking_code = pin
1321        bedticket.booking_session = acc_details['booking_session']
1322        bedticket.bed_type = acc_details['bt']
1323        bedticket.bed = bed
1324        hall_title = bed.__parent__.hostel_name
1325        coordinates = bed.getBedCoordinates()[1:]
1326        block, room_nr, bed_nr = coordinates
1327        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1328            'a':hall_title, 'b':block,
1329            'c':room_nr, 'd':bed_nr,
1330            'e':bed.bed_type})
1331        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1332        bedticket.bed_coordinates = translate(
1333            bc, 'waeup.kofa',target_language=portal_language)
1334        key = str(acc_details['booking_session'])
1335        self.context[key] = bedticket
1336        self.flash(_('Bed ticket created and bed booked: ${a}',
1337            mapping = {'a':bedticket.bed_coordinates}))
1338        self.redirect(self.url(self.context))
1339        return
1340
1341class BedTicketDisplayFormPage(KofaDisplayFormPage):
1342    """ Page to display bed tickets
1343    """
1344    grok.context(IBedTicket)
1345    grok.name('index')
1346    grok.require('waeup.handleAccommodation')
1347    form_fields = grok.AutoFields(IBedTicket)
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 = FriendlyDatetimeDisplayWidget('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.student,
1380            self.request)
1381        students_utils = getUtility(IStudentsUtils)
1382        return students_utils.renderPDF(
1383            self, 'bed_allocation.pdf',
1384            self.context.student, 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.student
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    label = _('Edit base data')
1482    pnav = 4
1483
1484    @action(_('Save'), style='primary')
1485    def save(self, **data):
1486        msave(self, **data)
1487        return
1488
1489class StudentChangePasswordPage(KofaEditFormPage):
1490    """ View to manage student base data
1491    """
1492    grok.context(IStudent)
1493    grok.name('change_password')
1494    grok.require('waeup.handleStudent')
1495    grok.template('change_password')
1496    label = _('Change password')
1497    pnav = 4
1498
1499    @action(_('Save'), style='primary')
1500    def save(self, **data):
1501        form = self.request.form
1502        password = form.get('change_password', None)
1503        password_ctl = form.get('change_password_repeat', None)
1504        if password:
1505            validator = getUtility(IPasswordValidator)
1506            errors = validator.validate_password(password, password_ctl)
1507            if not errors:
1508                IUserAccount(self.context).setPassword(password)
1509                self.context.writeLogMessage(self, 'saved: password')
1510                self.flash(_('Password changed.'))
1511            else:
1512                self.flash( ' '.join(errors))
1513        return
1514
1515class StudentFilesUploadPage(KofaPage):
1516    """ View to upload files by student
1517    """
1518    grok.context(IStudent)
1519    grok.name('change_portrait')
1520    grok.require('waeup.uploadStudentFile')
1521    grok.template('filesuploadpage')
1522    label = _('Upload portrait')
1523    pnav = 4
1524
1525    def update(self):
1526        if self.context.student.state != ADMITTED:
1527            emit_lock_message(self)
1528            return
1529        super(StudentFilesUploadPage, self).update()
1530        return
1531
1532class StartClearancePage(KofaPage):
1533    grok.context(IStudent)
1534    grok.name('start_clearance')
1535    grok.require('waeup.handleStudent')
1536    grok.template('enterpin')
1537    label = _('Start clearance')
1538    ac_prefix = 'CLR'
1539    notice = ''
1540    pnav = 4
1541    buttonname = _('Start clearance now')
1542
1543    @property
1544    def all_required_fields_filled(self):
1545        if self.context.email and self.context.phone:
1546            return True
1547        return False
1548
1549    @property
1550    def portrait_uploaded(self):
1551        store = getUtility(IExtFileStore)
1552        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1553            return True
1554        return False
1555
1556    def update(self, SUBMIT=None):
1557        if not self.context.state == ADMITTED:
1558            self.flash(_("Wrong state"))
1559            self.redirect(self.url(self.context))
1560            return
1561        if not self.portrait_uploaded:
1562            self.flash(_("No portrait uploaded."))
1563            self.redirect(self.url(self.context, 'change_portrait'))
1564            return
1565        if not self.all_required_fields_filled:
1566            self.flash(_("Not all required fields filled."))
1567            self.redirect(self.url(self.context, 'edit_base'))
1568            return
1569        self.ac_series = self.request.form.get('ac_series', None)
1570        self.ac_number = self.request.form.get('ac_number', None)
1571
1572        if SUBMIT is None:
1573            return
1574        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1575        code = get_access_code(pin)
1576        if not code:
1577            self.flash(_('Activation code is invalid.'))
1578            return
1579        # Mark pin as used (this also fires a pin related transition)
1580        # and fire transition start_clearance
1581        if code.state == USED:
1582            self.flash(_('Activation code has already been used.'))
1583            return
1584        else:
1585            comment = _(u"invalidated")
1586            # Here we know that the ac is in state initialized so we do not
1587            # expect an exception, but the owner might be different
1588            if not invalidate_accesscode(pin,comment,self.context.student_id):
1589                self.flash(_('You are not the owner of this access code.'))
1590                return
1591            self.context.clr_code = pin
1592        IWorkflowInfo(self.context).fireTransition('start_clearance')
1593        self.flash(_('Clearance process has been started.'))
1594        self.redirect(self.url(self.context,'cedit'))
1595        return
1596
1597class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1598    """ View to edit student clearance data by student
1599    """
1600    grok.context(IStudent)
1601    grok.name('cedit')
1602    grok.require('waeup.handleStudent')
1603    label = _('Edit clearance data')
1604
1605    @property
1606    def form_fields(self):
1607        if self.context.is_postgrad:
1608            form_fields = grok.AutoFields(IPGStudentClearance).omit('clearance_locked')
1609        else:
1610            form_fields = grok.AutoFields(IUGStudentClearance).omit('clearance_locked')
1611        return form_fields
1612
1613    def update(self):
1614        if self.context.clearance_locked:
1615            emit_lock_message(self)
1616            return
1617        return super(StudentClearanceEditFormPage, self).update()
1618
1619    @action(_('Save'), style='primary')
1620    def save(self, **data):
1621        self.applyData(self.context, **data)
1622        self.flash(_('Clearance form has been saved.'))
1623        return
1624
1625    def dataNotComplete(self):
1626        """To be implemented in the customization package.
1627        """
1628        return False
1629
1630    @action(_('Save and request clearance'), style='primary')
1631    def requestClearance(self, **data):
1632        self.applyData(self.context, **data)
1633        if self.dataNotComplete():
1634            self.flash(self.dataNotComplete())
1635            return
1636        self.flash(_('Clearance form has been saved.'))
1637        self.redirect(self.url(self.context,'request_clearance'))
1638        return
1639
1640class RequestClearancePage(KofaPage):
1641    grok.context(IStudent)
1642    grok.name('request_clearance')
1643    grok.require('waeup.handleStudent')
1644    grok.template('enterpin')
1645    label = _('Request clearance')
1646    notice = _('Enter the CLR access code used for starting clearance.')
1647    ac_prefix = 'CLR'
1648    pnav = 4
1649    buttonname = _('Request clearance now')
1650
1651    def update(self, SUBMIT=None):
1652        self.ac_series = self.request.form.get('ac_series', None)
1653        self.ac_number = self.request.form.get('ac_number', None)
1654        if SUBMIT is None:
1655            return
1656        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1657        if self.context.clr_code != pin:
1658            self.flash(_("This isn't your CLR access code."))
1659            return
1660        state = IWorkflowState(self.context).getState()
1661        # This shouldn't happen, but the application officer
1662        # might have forgotten to lock the form after changing the state
1663        if state != CLEARANCE:
1664            self.flash(_('This form cannot be submitted. Wrong state!'))
1665            return
1666        IWorkflowInfo(self.context).fireTransition('request_clearance')
1667        self.flash(_('Clearance has been requested.'))
1668        self.redirect(self.url(self.context))
1669        return
1670
1671class StartSessionPage(KofaPage):
1672    grok.context(IStudentStudyCourse)
1673    grok.name('start_session')
1674    grok.require('waeup.handleStudent')
1675    grok.template('enterpin')
1676    label = _('Start session')
1677    ac_prefix = 'SFE'
1678    notice = ''
1679    pnav = 4
1680    buttonname = _('Start now')
1681
1682    def update(self, SUBMIT=None):
1683        if not self.context.next_session_allowed:
1684            self.flash(_("You are not entitled to start session."))
1685            self.redirect(self.url(self.context))
1686            return
1687        self.ac_series = self.request.form.get('ac_series', None)
1688        self.ac_number = self.request.form.get('ac_number', None)
1689
1690        if SUBMIT is None:
1691            return
1692        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1693        code = get_access_code(pin)
1694        if not code:
1695            self.flash(_('Activation code is invalid.'))
1696            return
1697        # Mark pin as used (this also fires a pin related transition)
1698        if code.state == USED:
1699            self.flash(_('Activation code has already been used.'))
1700            return
1701        else:
1702            comment = _(u"invalidated")
1703            # Here we know that the ac is in state initialized so we do not
1704            # expect an error, but the owner might be different
1705            if not invalidate_accesscode(
1706                pin,comment,self.context.student.student_id):
1707                self.flash(_('You are not the owner of this access code.'))
1708                return
1709        if self.context.student.state == CLEARED:
1710            IWorkflowInfo(self.context.student).fireTransition(
1711                'pay_first_school_fee')
1712        elif self.context.student.state == RETURNING:
1713            IWorkflowInfo(self.context.student).fireTransition(
1714                'pay_school_fee')
1715        elif self.context.student.state == PAID:
1716            IWorkflowInfo(self.context.student).fireTransition(
1717                'pay_pg_fee')
1718        self.flash(_('Session started.'))
1719        self.redirect(self.url(self.context))
1720        return
1721
1722class AddStudyLevelFormPage(KofaEditFormPage):
1723    """ Page for students to add current study levels
1724    """
1725    grok.context(IStudentStudyCourse)
1726    grok.name('add')
1727    grok.require('waeup.handleStudent')
1728    grok.template('studyleveladdpage')
1729    form_fields = grok.AutoFields(IStudentStudyCourse)
1730    pnav = 4
1731
1732    @property
1733    def label(self):
1734        studylevelsource = StudyLevelSource().factory
1735        code = self.context.current_level
1736        title = studylevelsource.getTitle(self.context, code)
1737        return _('Add current level ${a}', mapping = {'a':title})
1738
1739    def update(self):
1740        if self.context.student.state != PAID:
1741            emit_lock_message(self)
1742            return
1743        super(AddStudyLevelFormPage, self).update()
1744        return
1745
1746    @action(_('Create course list now'), style='primary')
1747    def addStudyLevel(self, **data):
1748        studylevel = createObject(u'waeup.StudentStudyLevel')
1749        studylevel.level = self.context.current_level
1750        studylevel.level_session = self.context.current_session
1751        try:
1752            self.context.addStudentStudyLevel(
1753                self.context.certificate,studylevel)
1754        except KeyError:
1755            self.flash(_('This level exists.'))
1756        self.redirect(self.url(self.context))
1757        return
1758
1759class StudyLevelEditFormPage(KofaEditFormPage):
1760    """ Page to edit the student study level data by students
1761    """
1762    grok.context(IStudentStudyLevel)
1763    grok.name('edit')
1764    grok.require('waeup.handleStudent')
1765    grok.template('studyleveleditpage')
1766    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1767        'level_session', 'level_verdict')
1768    pnav = 4
1769    max_credits = 50
1770
1771    def update(self):
1772        if self.context.student.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        if self.total_credits > self.max_credits:
1826            self.flash(_('Maximum credits of ${a} exceeded.',
1827                mapping = {'a':self.max_credits}))
1828            return
1829        IWorkflowInfo(self.context.student).fireTransition(
1830            'register_courses')
1831        self.flash(_('Course list has been registered.'))
1832        self.redirect(self.url(self.context))
1833        return
1834
1835class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1836    """Add a course ticket by student.
1837    """
1838    grok.name('ctadd')
1839    grok.require('waeup.handleStudent')
1840    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1841        'score', 'mandatory', 'automatic', 'carry_over')
1842
1843    def update(self):
1844        if self.context.student.state != PAID:
1845            emit_lock_message(self)
1846            return
1847        super(CourseTicketAddFormPage2, self).update()
1848        return
1849
1850    @action(_('Add course ticket'))
1851    def addCourseTicket(self, **data):
1852        # Safety belt
1853        if self.context.student.state != PAID:
1854            return
1855        ticket = createObject(u'waeup.CourseTicket')
1856        course = data['course']
1857        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
1858            setattr(ticket, name, getattr(course, name))
1859        ticket.automatic = False
1860        try:
1861            self.context.addCourseTicket(ticket)
1862        except KeyError:
1863            self.flash(_('The ticket exists.'))
1864            return
1865        self.flash(_('Successfully added ${a}.',
1866            mapping = {'a':ticket.code}))
1867        self.redirect(self.url(self.context, u'@@edit'))
1868        return
1869
1870
1871class SetPasswordPage(KofaPage):
1872    grok.context(IKofaObject)
1873    grok.name('setpassword')
1874    grok.require('waeup.Anonymous')
1875    grok.template('setpassword')
1876    label = _('Set password for first-time login')
1877    ac_prefix = 'PWD'
1878    pnav = 0
1879    set_button = _('Set')
1880
1881    def update(self, SUBMIT=None):
1882        self.reg_number = self.request.form.get('reg_number', None)
1883        self.ac_series = self.request.form.get('ac_series', None)
1884        self.ac_number = self.request.form.get('ac_number', None)
1885
1886        if SUBMIT is None:
1887            return
1888        hitlist = search(query=self.reg_number,
1889            searchtype='reg_number', view=self)
1890        if not hitlist:
1891            self.flash(_('No student found.'))
1892            return
1893        if len(hitlist) != 1:   # Cannot happen but anyway
1894            self.flash(_('More than one student found.'))
1895            return
1896        student = hitlist[0].context
1897        self.student_id = student.student_id
1898        student_pw = student.password
1899        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1900        code = get_access_code(pin)
1901        if not code:
1902            self.flash(_('Access code is invalid.'))
1903            return
1904        if student_pw and pin == student.adm_code:
1905            self.flash(_(
1906                'Password has already been set. Your Student Id is ${a}',
1907                mapping = {'a':self.student_id}))
1908            return
1909        elif student_pw:
1910            self.flash(
1911                _('Password has already been set. You are using the ' +
1912                'wrong Access Code.'))
1913            return
1914        # Mark pin as used (this also fires a pin related transition)
1915        # and set student password
1916        if code.state == USED:
1917            self.flash(_('Access code has already been used.'))
1918            return
1919        else:
1920            comment = _(u"invalidated")
1921            # Here we know that the ac is in state initialized so we do not
1922            # expect an exception
1923            invalidate_accesscode(pin,comment)
1924            IUserAccount(student).setPassword(self.ac_number)
1925            student.adm_code = pin
1926        self.flash(_('Password has been set. Your Student Id is ${a}',
1927            mapping = {'a':self.student_id}))
1928        return
1929
1930class StudentRequestPasswordPage(KofaAddFormPage):
1931    """Captcha'd registration page for applicants.
1932    """
1933    grok.name('requestpw')
1934    grok.require('waeup.Anonymous')
1935    grok.template('requestpw')
1936    form_fields = grok.AutoFields(IStudentRequestPW).select(
1937        'firstname','number','email')
1938    label = _('Request password for first-time login')
1939
1940    def update(self):
1941        # Handle captcha
1942        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1943        self.captcha_result = self.captcha.verify(self.request)
1944        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1945        return
1946
1947    def _redirect(self, email, password, student_id):
1948        # Forward only email to landing page in base package.
1949        self.redirect(self.url(self.context, 'requestpw_complete',
1950            data = dict(email=email)))
1951        return
1952
1953    def _pw_used(self):
1954        # XXX: False if password has not been used. We need an extra
1955        #      attribute which remembers if student logged in.
1956        return True
1957
1958    @action(_('Send login credentials to email address'), style='primary')
1959    def get_credentials(self, **data):
1960        if not self.captcha_result.is_valid:
1961            # Captcha will display error messages automatically.
1962            # No need to flash something.
1963            return
1964        number = data.get('number','')
1965        firstname = data.get('firstname','')
1966        cat = getUtility(ICatalog, name='students_catalog')
1967        results = list(
1968            cat.searchResults(reg_number=(number, number)))
1969        if not results:
1970            results = list(
1971                cat.searchResults(matric_number=(number, number)))
1972        if results:
1973            student = results[0]
1974            if getattr(student,'firstname',None) is None:
1975                self.flash(_('An error occurred.'))
1976                return
1977            elif student.firstname.lower() != firstname.lower():
1978                # Don't tell the truth here. Anonymous must not
1979                # know that a record was found and only the firstname
1980                # verification failed.
1981                self.flash(_('No student record found.'))
1982                return
1983            elif student.password is not None and self._pw_used:
1984                self.flash(_('Your password has already been set and used. '
1985                             'Please proceed to the login page.'))
1986                return
1987            # Store email address but nothing else.
1988            student.email = data['email']
1989            notify(grok.ObjectModifiedEvent(student))
1990        else:
1991            # No record found, this is the truth.
1992            self.flash(_('No student record found.'))
1993            return
1994
1995        kofa_utils = getUtility(IKofaUtils)
1996        password = kofa_utils.genPassword()
1997        mandate = PasswordMandate()
1998        mandate.params['password'] = password
1999        mandate.params['user'] = student
2000        site = grok.getSite()
2001        site['mandates'].addMandate(mandate)
2002        # Send email with credentials
2003        args = {'mandate_id':mandate.mandate_id}
2004        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2005        url_info = u'Confirmation link: %s' % mandate_url
2006        msg = _('You have successfully requested a password for the')
2007        if kofa_utils.sendCredentials(IUserAccount(student),
2008            password, url_info, msg):
2009            email_sent = student.email
2010        else:
2011            email_sent = None
2012        self._redirect(email=email_sent, password=password,
2013            student_id=student.student_id)
2014        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2015        self.context.logger.info(
2016            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2017        return
2018
2019class StudentRequestPasswordEmailSent(KofaPage):
2020    """Landing page after successful password request.
2021
2022    """
2023    grok.name('requestpw_complete')
2024    grok.require('waeup.Public')
2025    grok.template('requestpwmailsent')
2026    label = _('Your password request was successful.')
2027
2028    def update(self, email=None, student_id=None, password=None):
2029        self.email = email
2030        self.password = password
2031        self.student_id = student_id
2032        return
Note: See TracBrowser for help on using the repository browser.