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

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

Tiny code cleanup.

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