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

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

clr_code and adm_code (actually all field meant for reimport) must not be readonly.

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