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

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

Define separator headlines on clearance forms.

Catch ConstraintNotSatisfied? exception if selected level is wrong.

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