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

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

Translate boolean values also in edit and manage pages.

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