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

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

Add boolean field 'suspended' to IStudent and IApplicant and extend authentication (checkPassword) slightly. Test will follow

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