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

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

Explain better what to do.

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