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

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

Remove breakpoint and prepare formatted_text() for unit tests.

  • Property svn:keywords set to Id
File size: 72.1 KB
Line 
1## $Id: browser.py 8142 2012-04-13 10:11: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 manage student clearance data
469    """
470    grok.context(IStudent)
471    grok.name('manage_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 StudentPersonalEditFormPage(KofaEditFormPage):
569    """ Page to edit personal data
570    """
571    grok.context(IStudent)
572    grok.name('edit_personal')
573    grok.require('waeup.handleStudent')
574    form_fields = grok.AutoFields(IStudentPersonal)
575    label = _('Edit 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 translated_values(self):
713        lang = self.request.cookies.get('kofa.language')
714        for value in self.context.values():
715            value_dict = value.__dict__
716            value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
717                target_language=lang)
718            value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
719                target_language=lang)
720            value_dict['automatic'] = translate(str(value.automatic), 'zope',
721                target_language=lang)
722            yield value_dict
723
724    @property
725    def label(self):
726        # Here we know that the cookie has been set
727        lang = self.request.cookies.get('kofa.language')
728        level_title = translate(self.context.level_title, 'waeup.kofa',
729            target_language=lang)
730        return _('${a}: Study Level ${b}', mapping = {
731            'a':self.context.getStudent().display_fullname,
732            'b':level_title})
733
734    @property
735    def total_credits(self):
736        total_credits = 0
737        for key, val in self.context.items():
738            total_credits += val.credits
739        return total_credits
740
741class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
742    """Deliver a PDF slip of the context.
743    """
744    grok.context(IStudentStudyLevel)
745    grok.name('course_registration.pdf')
746    grok.require('waeup.viewStudent')
747    form_fields = grok.AutoFields(IStudentStudyLevel)
748    prefix = 'form'
749
750    @property
751    def title(self):
752        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
753        return translate(_('Level Data'), 'waeup.kofa',
754            target_language=portal_language)
755
756    @property
757    def content_title(self):
758        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
759        return translate(_('Course List'), 'waeup.kofa',
760            target_language=portal_language)
761
762    @property
763    def label(self):
764        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
765        lang = self.request.cookies.get('kofa.language', portal_language)
766        level_title = translate(self.context.level_title, 'waeup.kofa',
767            target_language=lang)
768        return translate(_('Course Registration Slip'),
769            'waeup.kofa', target_language=portal_language) \
770            + ' %s' % level_title
771
772    def render(self):
773        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
774        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
775        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
776        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
777        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
778        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
779        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
780        Mand = translate(_('Mand.'), 'waeup.kofa', target_language=portal_language)
781        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
782        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
783            self.request)
784        students_utils = getUtility(IStudentsUtils)
785        tabledata = sorted(self.context.values(),
786            key=lambda value: str(value.semester) + value.code)
787        return students_utils.renderPDF(
788            self, 'course_registration.pdf',
789            self.context.getStudent(), studentview,
790            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
791                         (Title,'title', 5),
792                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
793                         (Cred, 'credits', 1.5),
794                         (Mand, 'mandatory', 1.5),
795                         (Score, 'score', 1.5),
796                         #('Auto', 'automatic', 1.5)
797                         ],
798            tabledata=tabledata)
799
800class StudyLevelManageFormPage(KofaEditFormPage):
801    """ Page to edit the student study level data
802    """
803    grok.context(IStudentStudyLevel)
804    grok.name('manage')
805    grok.require('waeup.manageStudent')
806    grok.template('studylevelmanagepage')
807    form_fields = grok.AutoFields(IStudentStudyLevel)
808    pnav = 4
809    taboneactions = [_('Save'),_('Cancel')]
810    tabtwoactions = [_('Add course ticket'),
811        _('Remove selected tickets'),_('Cancel')]
812
813    def update(self):
814        super(StudyLevelManageFormPage, self).update()
815        tabs.need()
816        self.tab1 = self.tab2 = ''
817        qs = self.request.get('QUERY_STRING', '')
818        if not qs:
819            qs = 'tab1'
820        setattr(self, qs, 'active')
821        warning.need()
822        datatable.need()
823        return
824
825    @property
826    def label(self):
827        # Here we know that the cookie has been set
828        lang = self.request.cookies.get('kofa.language')
829        level_title = translate(self.context.level_title, 'waeup.kofa',
830            target_language=lang)
831        return _('Manage study level ${a}',
832            mapping = {'a':level_title})
833
834    @action(_('Save'), style='primary')
835    def save(self, **data):
836        msave(self, **data)
837        return
838
839    @action(_('Add course ticket'))
840    def addCourseTicket(self, **data):
841        self.redirect(self.url(self.context, '@@add'))
842
843    @jsaction(_('Remove selected tickets'))
844    def delCourseTicket(self, **data):
845        form = self.request.form
846        if form.has_key('val_id'):
847            child_id = form['val_id']
848        else:
849            self.flash(_('No ticket selected.'))
850            self.redirect(self.url(self.context, '@@manage')+'?tab2')
851            return
852        if not isinstance(child_id, list):
853            child_id = [child_id]
854        deleted = []
855        for id in child_id:
856            del self.context[id]
857            deleted.append(id)
858        if len(deleted):
859            self.flash(_('Successfully removed: ${a}',
860                mapping = {'a':', '.join(deleted)}))
861        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
862        return
863
864class ValidateCoursesPage(UtilityView, grok.View):
865    """ Validate course list by course adviser
866    """
867    grok.context(IStudentStudyLevel)
868    grok.name('validate_courses')
869    grok.require('waeup.validateStudent')
870
871    def update(self):
872        if str(self.context.__parent__.current_level) != self.context.__name__:
873            self.flash(_('This level does not correspond current level.'))
874        elif self.context.getStudent().state == REGISTERED:
875            IWorkflowInfo(self.context.getStudent()).fireTransition(
876                'validate_courses')
877            self.flash(_('Course list has been validated.'))
878        else:
879            self.flash(_('Student is in the wrong state.'))
880        self.redirect(self.url(self.context))
881        return
882
883    def render(self):
884        return
885
886class RejectCoursesPage(UtilityView, grok.View):
887    """ Reject course list by course adviser
888    """
889    grok.context(IStudentStudyLevel)
890    grok.name('reject_courses')
891    grok.require('waeup.validateStudent')
892
893    def update(self):
894        if str(self.context.__parent__.current_level) != self.context.__name__:
895            self.flash(_('This level does not correspond current level.'))
896            self.redirect(self.url(self.context))
897            return
898        elif self.context.getStudent().state == VALIDATED:
899            IWorkflowInfo(self.context.getStudent()).fireTransition('reset8')
900            message = _('Course list request has been annulled.')
901            self.flash(message)
902        elif self.context.getStudent().state == REGISTERED:
903            IWorkflowInfo(self.context.getStudent()).fireTransition('reset7')
904            message = _('Course list request has been rejected:')
905            self.flash(message)
906        else:
907            self.flash(_('Student is in the wrong state.'))
908            self.redirect(self.url(self.context))
909            return
910        args = {'subject':message}
911        self.redirect(self.url(self.context.getStudent()) +
912            '/contactstudent?%s' % urlencode(args))
913        return
914
915    def render(self):
916        return
917
918class CourseTicketAddFormPage(KofaAddFormPage):
919    """Add a course ticket.
920    """
921    grok.context(IStudentStudyLevel)
922    grok.name('add')
923    grok.require('waeup.manageStudent')
924    label = _('Add course ticket')
925    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
926        'score', 'automatic', 'carry_over')
927    pnav = 4
928
929    @action(_('Add course ticket'))
930    def addCourseTicket(self, **data):
931        ticket = CourseTicket()
932        course = data['course']
933        ticket.automatic = False
934        ticket.carry_over = False
935        ticket.code = course.code
936        ticket.title = course.title
937        ticket.fcode = course.__parent__.__parent__.__parent__.code
938        ticket.dcode = course.__parent__.__parent__.code
939        ticket.credits = course.credits
940        ticket.passmark = course.passmark
941        ticket.semester = course.semester
942        try:
943            self.context.addCourseTicket(ticket)
944        except KeyError:
945            self.flash(_('The ticket exists.'))
946            return
947        self.flash(_('Successfully added ${a}.',
948            mapping = {'a':ticket.code}))
949        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
950        return
951
952    @action(_('Cancel'), validator=NullValidator)
953    def cancel(self, **data):
954        self.redirect(self.url(self.context))
955
956class CourseTicketDisplayFormPage(KofaDisplayFormPage):
957    """ Page to display course tickets
958    """
959    grok.context(ICourseTicket)
960    grok.name('index')
961    grok.require('waeup.viewStudent')
962    form_fields = grok.AutoFields(ICourseTicket)
963    grok.template('courseticketpage')
964    pnav = 4
965
966    @property
967    def label(self):
968        return _('${a}: Course Ticket ${b}', mapping = {
969            'a':self.context.getStudent().display_fullname,
970            'b':self.context.code})
971
972class CourseTicketManageFormPage(KofaEditFormPage):
973    """ Page to manage course tickets
974    """
975    grok.context(ICourseTicket)
976    grok.name('manage')
977    grok.require('waeup.manageStudent')
978    form_fields = grok.AutoFields(ICourseTicket)
979    grok.template('courseticketmanagepage')
980    pnav = 4
981
982    @property
983    def label(self):
984        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
985
986    @action('Save', style='primary')
987    def save(self, **data):
988        msave(self, **data)
989        return
990
991class PaymentsManageFormPage(KofaEditFormPage):
992    """ Page to manage the student payments
993
994    This manage form page is for both students and students officers.
995    """
996    grok.context(IStudentPaymentsContainer)
997    grok.name('index')
998    grok.require('waeup.payStudent')
999    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1000    grok.template('paymentsmanagepage')
1001    pnav = 4
1002
1003    def unremovable(self, ticket):
1004        usertype = getattr(self.request.principal, 'user_type', None)
1005        if not usertype:
1006            return False
1007        return (self.request.principal.user_type == 'student' and ticket.r_code)
1008
1009    @property
1010    def label(self):
1011        return _('${a}: Payments',
1012            mapping = {'a':self.context.__parent__.display_fullname})
1013
1014    def update(self):
1015        super(PaymentsManageFormPage, self).update()
1016        datatable.need()
1017        warning.need()
1018        return
1019
1020    @jsaction(_('Remove selected tickets'))
1021    def delPaymentTicket(self, **data):
1022        form = self.request.form
1023        if form.has_key('val_id'):
1024            child_id = form['val_id']
1025        else:
1026            self.flash(_('No payment selected.'))
1027            self.redirect(self.url(self.context))
1028            return
1029        if not isinstance(child_id, list):
1030            child_id = [child_id]
1031        deleted = []
1032        for id in child_id:
1033            # Students are not allowed to remove used payment tickets
1034            if not self.unremovable(self.context[id]):
1035                del self.context[id]
1036                deleted.append(id)
1037        if len(deleted):
1038            self.flash(_('Successfully removed: ${a}',
1039                mapping = {'a': ', '.join(deleted)}))
1040            write_log_message(self,'removed: % s' % ', '.join(deleted))
1041        self.redirect(self.url(self.context))
1042        return
1043
1044    @action(_('Add online payment ticket'))
1045    def addPaymentTicket(self, **data):
1046        self.redirect(self.url(self.context, '@@addop'))
1047
1048class OnlinePaymentAddFormPage(KofaAddFormPage):
1049    """ Page to add an online payment ticket
1050    """
1051    grok.context(IStudentPaymentsContainer)
1052    grok.name('addop')
1053    grok.require('waeup.payStudent')
1054    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1055        'p_category')
1056    label = _('Add online payment')
1057    pnav = 4
1058
1059    @action(_('Create ticket'), style='primary')
1060    def createTicket(self, **data):
1061        p_category = data['p_category']
1062        student = self.context.__parent__
1063        if p_category == 'bed_allocation' and student[
1064            'studycourse'].current_session != grok.getSite()[
1065            'configuration'].accommodation_session:
1066                self.flash(
1067                    _('Your current session does not match ' + \
1068                    'accommodation session.'))
1069                self.redirect(self.url(self.context))
1070                return
1071        students_utils = getUtility(IStudentsUtils)
1072        try:
1073            pay_details  = students_utils.getPaymentDetails(
1074                p_category,student)
1075        except AttributeError:
1076            self.flash(
1077                _('Study course data are incomplete.'))
1078            self.redirect(self.url(self.context))
1079            return
1080        if pay_details['error']:
1081            self.flash(pay_details['error'])
1082            self.redirect(self.url(self.context))
1083            return
1084        p_item = pay_details['p_item']
1085        p_session = pay_details['p_session']
1086        for key in self.context.keys():
1087            ticket = self.context[key]
1088            if ticket.p_state == 'paid' and\
1089               ticket.p_category == p_category and \
1090               ticket.p_item == p_item and \
1091               ticket.p_session == p_session:
1092                  self.flash(
1093                      _('This type of payment has already been made.'))
1094                  self.redirect(self.url(self.context))
1095                  return
1096        payment = createObject(u'waeup.StudentOnlinePayment')
1097        self.applyData(payment, **data)
1098        timestamp = "%d" % int(time()*1000)
1099        payment.p_id = "p%s" % timestamp
1100        payment.p_item = p_item
1101        payment.p_session = p_session
1102        payment.amount_auth = pay_details['amount']
1103        payment.surcharge_1 = pay_details['surcharge_1']
1104        payment.surcharge_2 = pay_details['surcharge_2']
1105        payment.surcharge_3 = pay_details['surcharge_3']
1106        self.context[payment.p_id] = payment
1107        self.flash(_('Payment ticket created.'))
1108        self.redirect(self.url(self.context))
1109        return
1110
1111class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1112    """ Page to view an online payment ticket
1113    """
1114    grok.context(IStudentOnlinePayment)
1115    grok.name('index')
1116    grok.require('waeup.viewStudent')
1117    form_fields = grok.AutoFields(IStudentOnlinePayment)
1118    form_fields[
1119        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1120    form_fields[
1121        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1122    pnav = 4
1123
1124    @property
1125    def label(self):
1126        return _('${a}: Online Payment Ticket ${b}', mapping = {
1127            'a':self.context.getStudent().display_fullname,
1128            'b':self.context.p_id})
1129
1130class OnlinePaymentCallbackPage(UtilityView, grok.View):
1131    """ Callback view
1132    """
1133    grok.context(IStudentOnlinePayment)
1134    grok.name('simulate_callback')
1135    grok.require('waeup.payStudent')
1136
1137    # This update method simulates a valid callback und must be
1138    # neutralized in the customization package.
1139    def update(self):
1140        if self.context.p_state == 'paid':
1141            self.flash(_('This ticket has already been paid.'))
1142            return
1143        student = self.context.getStudent()
1144        write_log_message(self,'valid callback: %s' % self.context.p_id)
1145        self.context.r_amount_approved = self.context.amount_auth
1146        self.context.r_card_num = u'0000'
1147        self.context.r_code = u'00'
1148        self.context.p_state = 'paid'
1149        self.context.payment_date = datetime.now()
1150        if self.context.p_category == 'clearance':
1151            # Create CLR access code
1152            pin, error = create_accesscode('CLR',0,student.student_id)
1153            if error:
1154                self.flash(_('Valid callback received. ${a}',
1155                    mapping = {'a':error}))
1156                return
1157            self.context.ac = pin
1158        elif self.context.p_category == 'schoolfee':
1159            # Create SFE access code
1160            pin, error = create_accesscode('SFE',0,student.student_id)
1161            if error:
1162                self.flash(_('Valid callback received. ${a}',
1163                    mapping = {'a':error}))
1164                return
1165            self.context.ac = pin
1166        elif self.context.p_category == 'bed_allocation':
1167            # Create HOS access code
1168            pin, error = create_accesscode('HOS',0,student.student_id)
1169            if error:
1170                self.flash(_('Valid callback received. ${a}',
1171                    mapping = {'a':error}))
1172                return
1173            self.context.ac = pin
1174        self.flash(_('Valid callback received.'))
1175        return
1176
1177    def render(self):
1178        self.redirect(self.url(self.context, '@@index'))
1179        return
1180
1181class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1182    """Deliver a PDF slip of the context.
1183    """
1184    grok.context(IStudentOnlinePayment)
1185    grok.name('payment_receipt.pdf')
1186    grok.require('waeup.viewStudent')
1187    form_fields = grok.AutoFields(IStudentOnlinePayment)
1188    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
1189    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
1190    prefix = 'form'
1191    title = 'Payment Data'
1192
1193    @property
1194    def label(self):
1195        return 'Online Payment Receipt %s' % self.context.p_id
1196
1197    def render(self):
1198        if self.context.p_state != 'paid':
1199            self.flash('Ticket not yet paid.')
1200            self.redirect(self.url(self.context))
1201            return
1202        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1203            self.request)
1204        students_utils = getUtility(IStudentsUtils)
1205        return students_utils.renderPDF(self, 'payment_receipt.pdf',
1206            self.context.getStudent(), studentview)
1207
1208
1209class AccommodationManageFormPage(KofaEditFormPage):
1210    """ Page to manage bed tickets.
1211
1212    This manage form page is for both students and students officers.
1213    """
1214    grok.context(IStudentAccommodation)
1215    grok.name('index')
1216    grok.require('waeup.handleAccommodation')
1217    form_fields = grok.AutoFields(IStudentAccommodation)
1218    grok.template('accommodationmanagepage')
1219    pnav = 4
1220    officers_only_actions = [_('Remove selected')]
1221
1222    @property
1223    def label(self):
1224        return _('${a}: Accommodation',
1225            mapping = {'a':self.context.__parent__.display_fullname})
1226
1227    def update(self):
1228        super(AccommodationManageFormPage, self).update()
1229        datatable.need()
1230        warning.need()
1231        return
1232
1233    @jsaction(_('Remove selected'))
1234    def delBedTickets(self, **data):
1235        if getattr(self.request.principal, 'user_type', None) == 'student':
1236            self.flash(_('You are not allowed to remove bed tickets.'))
1237            self.redirect(self.url(self.context))
1238            return
1239        form = self.request.form
1240        if form.has_key('val_id'):
1241            child_id = form['val_id']
1242        else:
1243            self.flash(_('No bed ticket selected.'))
1244            self.redirect(self.url(self.context))
1245            return
1246        if not isinstance(child_id, list):
1247            child_id = [child_id]
1248        deleted = []
1249        for id in child_id:
1250            del self.context[id]
1251            deleted.append(id)
1252        if len(deleted):
1253            self.flash(_('Successfully removed: ${a}',
1254                mapping = {'a':', '.join(deleted)}))
1255            write_log_message(self,'removed: % s' % ', '.join(deleted))
1256        self.redirect(self.url(self.context))
1257        return
1258
1259    @property
1260    def selected_actions(self):
1261        if getattr(self.request.principal, 'user_type', None) == 'student':
1262            return [action for action in self.actions
1263                    if not action.label in self.officers_only_actions]
1264        return self.actions
1265
1266class BedTicketAddPage(KofaPage):
1267    """ Page to add an online payment ticket
1268    """
1269    grok.context(IStudentAccommodation)
1270    grok.name('add')
1271    grok.require('waeup.handleAccommodation')
1272    grok.template('enterpin')
1273    ac_prefix = 'HOS'
1274    label = _('Add bed ticket')
1275    pnav = 4
1276    buttonname = _('Create bed ticket')
1277    notice = ''
1278
1279    def update(self, SUBMIT=None):
1280        student = self.context.getStudent()
1281        students_utils = getUtility(IStudentsUtils)
1282        acc_details  = students_utils.getAccommodationDetails(student)
1283        if not acc_details:
1284            self.flash(_("Your data are incomplete."))
1285            self.redirect(self.url(self.context))
1286            return
1287        if not student.state in acc_details['allowed_states']:
1288            self.flash(_("You are in the wrong registration state."))
1289            self.redirect(self.url(self.context))
1290            return
1291        if student['studycourse'].current_session != acc_details[
1292            'booking_session']:
1293            self.flash(
1294                _('Your current session does not match accommodation session.'))
1295            self.redirect(self.url(self.context))
1296            return
1297        if str(acc_details['booking_session']) in self.context.keys():
1298            self.flash(
1299                _('You already booked a bed space in current ' \
1300                    + 'accommodation session.'))
1301            self.redirect(self.url(self.context))
1302            return
1303        self.ac_series = self.request.form.get('ac_series', None)
1304        self.ac_number = self.request.form.get('ac_number', None)
1305        if SUBMIT is None:
1306            return
1307        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1308        code = get_access_code(pin)
1309        if not code:
1310            self.flash(_('Activation code is invalid.'))
1311            return
1312        # Search and book bed
1313        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1314        entries = cat.searchResults(
1315            owner=(student.student_id,student.student_id))
1316        if len(entries):
1317            # If bed space has bee manually allocated use this bed
1318            bed = [entry for entry in entries][0]
1319        else:
1320            # else search for other available beds
1321            entries = cat.searchResults(
1322                bed_type=(acc_details['bt'],acc_details['bt']))
1323            available_beds = [
1324                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1325            if available_beds:
1326                students_utils = getUtility(IStudentsUtils)
1327                bed = students_utils.selectBed(available_beds)
1328                bed.bookBed(student.student_id)
1329            else:
1330                self.flash(_('There is no free bed in your category ${a}.',
1331                    mapping = {'a':acc_details['bt']}))
1332                return
1333        # Mark pin as used (this also fires a pin related transition)
1334        if code.state == USED:
1335            self.flash(_('Activation code has already been used.'))
1336            return
1337        else:
1338            comment = _(u'invalidated')
1339            # Here we know that the ac is in state initialized so we do not
1340            # expect an exception, but the owner might be different
1341            if not invalidate_accesscode(
1342                pin,comment,self.context.getStudent().student_id):
1343                self.flash(_('You are not the owner of this access code.'))
1344                return
1345        # Create bed ticket
1346        bedticket = createObject(u'waeup.BedTicket')
1347        bedticket.booking_code = pin
1348        bedticket.booking_session = acc_details['booking_session']
1349        bedticket.bed_type = acc_details['bt']
1350        bedticket.bed = bed
1351        hall_title = bed.__parent__.hostel_name
1352        coordinates = bed.getBedCoordinates()[1:]
1353        block, room_nr, bed_nr = coordinates
1354        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1355            'a':hall_title, 'b':block,
1356            'c':room_nr, 'd':bed_nr,
1357            'e':bed.bed_type})
1358        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1359        bedticket.bed_coordinates = translate(
1360            bc, 'waeup.kofa',target_language=portal_language)
1361        key = str(acc_details['booking_session'])
1362        self.context[key] = bedticket
1363        self.flash(_('Bed ticket created and bed booked: ${a}',
1364            mapping = {'a':bedticket.bed_coordinates}))
1365        self.redirect(self.url(self.context))
1366        return
1367
1368class BedTicketDisplayFormPage(KofaDisplayFormPage):
1369    """ Page to display bed tickets
1370    """
1371    grok.context(IBedTicket)
1372    grok.name('index')
1373    grok.require('waeup.handleAccommodation')
1374    form_fields = grok.AutoFields(IBedTicket)
1375    form_fields[
1376        'booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1377    pnav = 4
1378
1379    @property
1380    def label(self):
1381        return _('Bed Ticket for Session ${a}',
1382            mapping = {'a':self.context.getSessionString()})
1383
1384class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1385    """Deliver a PDF slip of the context.
1386    """
1387    grok.context(IBedTicket)
1388    grok.name('bed_allocation.pdf')
1389    grok.require('waeup.handleAccommodation')
1390    form_fields = grok.AutoFields(IBedTicket)
1391    form_fields['booking_date'].custom_widget = FriendlyDateDisplayWidget('le')
1392    prefix = 'form'
1393
1394    @property
1395    def title(self):
1396        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1397        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1398            target_language=portal_language)
1399
1400    @property
1401    def label(self):
1402        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1403        return translate(_('Bed Allocation: '),
1404            'waeup.kofa', target_language=portal_language) \
1405            + ' %s' % self.context.bed_coordinates
1406
1407    def render(self):
1408        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1409            self.request)
1410        students_utils = getUtility(IStudentsUtils)
1411        return students_utils.renderPDF(
1412            self, 'bed_allocation.pdf',
1413            self.context.getStudent(), studentview)
1414
1415class BedTicketRelocationPage(UtilityView, grok.View):
1416    """ Callback view
1417    """
1418    grok.context(IBedTicket)
1419    grok.name('relocate')
1420    grok.require('waeup.manageHostels')
1421
1422    # Relocate student if student parameters have changed or the bed_type
1423    # of the bed has changed
1424    def update(self):
1425        student = self.context.getStudent()
1426        students_utils = getUtility(IStudentsUtils)
1427        acc_details  = students_utils.getAccommodationDetails(student)
1428        if self.context.bed != None and \
1429              'reserved' in self.context.bed.bed_type:
1430            self.flash(_("Students in reserved beds can't be relocated."))
1431            self.redirect(self.url(self.context))
1432            return
1433        if acc_details['bt'] == self.context.bed_type and \
1434                self.context.bed != None and \
1435                self.context.bed.bed_type == self.context.bed_type:
1436            self.flash(_("Student can't be relocated."))
1437            self.redirect(self.url(self.context))
1438            return
1439        # Search a bed
1440        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1441        entries = cat.searchResults(
1442            owner=(student.student_id,student.student_id))
1443        if len(entries) and self.context.bed == None:
1444            # If booking has been cancelled but other bed space has been
1445            # manually allocated after cancellation use this bed
1446            new_bed = [entry for entry in entries][0]
1447        else:
1448            # Search for other available beds
1449            entries = cat.searchResults(
1450                bed_type=(acc_details['bt'],acc_details['bt']))
1451            available_beds = [
1452                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1453            if available_beds:
1454                students_utils = getUtility(IStudentsUtils)
1455                new_bed = students_utils.selectBed(available_beds)
1456                new_bed.bookBed(student.student_id)
1457            else:
1458                self.flash(_('There is no free bed in your category ${a}.',
1459                    mapping = {'a':acc_details['bt']}))
1460                self.redirect(self.url(self.context))
1461                return
1462        # Release old bed if exists
1463        if self.context.bed != None:
1464            self.context.bed.owner = NOT_OCCUPIED
1465            notify(grok.ObjectModifiedEvent(self.context.bed))
1466        # Alocate new bed
1467        self.context.bed_type = acc_details['bt']
1468        self.context.bed = new_bed
1469        hall_title = new_bed.__parent__.hostel_name
1470        coordinates = new_bed.getBedCoordinates()[1:]
1471        block, room_nr, bed_nr = coordinates
1472        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1473            'a':hall_title, 'b':block,
1474            'c':room_nr, 'd':bed_nr,
1475            'e':new_bed.bed_type})
1476        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1477        self.context.bed_coordinates = translate(
1478            bc, 'waeup.kofa',target_language=portal_language)
1479        self.flash(_('Student relocated: ${a}',
1480            mapping = {'a':self.context.bed_coordinates}))
1481        self.redirect(self.url(self.context))
1482        return
1483
1484    def render(self):
1485        return
1486
1487class StudentHistoryPage(KofaPage):
1488    """ Page to display student clearance data
1489    """
1490    grok.context(IStudent)
1491    grok.name('history')
1492    grok.require('waeup.viewStudent')
1493    grok.template('studenthistory')
1494    pnav = 4
1495
1496    @property
1497    def label(self):
1498        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1499
1500# Pages for students only
1501
1502class StudentBaseEditFormPage(KofaEditFormPage):
1503    """ View to edit student base data
1504    """
1505    grok.context(IStudent)
1506    grok.name('edit_base')
1507    grok.require('waeup.handleStudent')
1508    form_fields = grok.AutoFields(IStudentBase).select(
1509        'email', 'phone')
1510    form_fields['phone'].custom_widget = PhoneWidget
1511    label = _('Edit base data')
1512    pnav = 4
1513
1514    @action(_('Save'), style='primary')
1515    def save(self, **data):
1516        msave(self, **data)
1517        return
1518
1519class StudentChangePasswordPage(KofaEditFormPage):
1520    """ View to manage student base data
1521    """
1522    grok.context(IStudent)
1523    grok.name('change_password')
1524    grok.require('waeup.handleStudent')
1525    grok.template('change_password')
1526    label = _('Change password')
1527    pnav = 4
1528
1529    @action(_('Save'), style='primary')
1530    def save(self, **data):
1531        form = self.request.form
1532        password = form.get('change_password', None)
1533        password_ctl = form.get('change_password_repeat', None)
1534        if password:
1535            validator = getUtility(IPasswordValidator)
1536            errors = validator.validate_password(password, password_ctl)
1537            if not errors:
1538                IUserAccount(self.context).setPassword(password)
1539                write_log_message(self, 'saved: password')
1540                self.flash(_('Password changed.'))
1541            else:
1542                self.flash( ' '.join(errors))
1543        return
1544
1545class StudentFilesUploadPage(KofaPage):
1546    """ View to upload files by student
1547    """
1548    grok.context(IStudent)
1549    grok.name('change_portrait')
1550    grok.require('waeup.uploadStudentFile')
1551    grok.template('filesuploadpage')
1552    label = _('Upload portrait')
1553    pnav = 4
1554
1555    def update(self):
1556        if self.context.getStudent().state != ADMITTED:
1557            emit_lock_message(self)
1558            return
1559        super(StudentFilesUploadPage, self).update()
1560        return
1561
1562class StartClearancePage(KofaPage):
1563    grok.context(IStudent)
1564    grok.name('start_clearance')
1565    grok.require('waeup.handleStudent')
1566    grok.template('enterpin')
1567    label = _('Start clearance')
1568    ac_prefix = 'CLR'
1569    notice = ''
1570    pnav = 4
1571    buttonname = _('Start clearance now')
1572
1573    @property
1574    def all_required_fields_filled(self):
1575        if self.context.email and self.context.phone:
1576            return True
1577        return False
1578
1579    @property
1580    def portrait_uploaded(self):
1581        store = getUtility(IExtFileStore)
1582        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1583            return True
1584        return False
1585
1586    def update(self, SUBMIT=None):
1587        if not self.context.state == ADMITTED:
1588            self.flash(_("Wrong state"))
1589            self.redirect(self.url(self.context))
1590            return
1591        if not self.portrait_uploaded:
1592            self.flash(_("No portrait uploaded."))
1593            self.redirect(self.url(self.context, 'change_portrait'))
1594            return
1595        if not self.all_required_fields_filled:
1596            self.flash(_("Not all required fields filled."))
1597            self.redirect(self.url(self.context, 'edit_base'))
1598            return
1599        self.ac_series = self.request.form.get('ac_series', None)
1600        self.ac_number = self.request.form.get('ac_number', None)
1601
1602        if SUBMIT is None:
1603            return
1604        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1605        code = get_access_code(pin)
1606        if not code:
1607            self.flash(_('Activation code is invalid.'))
1608            return
1609        # Mark pin as used (this also fires a pin related transition)
1610        # and fire transition start_clearance
1611        if code.state == USED:
1612            self.flash(_('Activation code has already been used.'))
1613            return
1614        else:
1615            comment = _(u"invalidated")
1616            # Here we know that the ac is in state initialized so we do not
1617            # expect an exception, but the owner might be different
1618            if not invalidate_accesscode(pin,comment,self.context.student_id):
1619                self.flash(_('You are not the owner of this access code.'))
1620                return
1621            self.context.clr_code = pin
1622        IWorkflowInfo(self.context).fireTransition('start_clearance')
1623        self.flash(_('Clearance process has been started.'))
1624        self.redirect(self.url(self.context,'cedit'))
1625        return
1626
1627class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1628    """ View to edit student clearance data by student
1629    """
1630    grok.context(IStudent)
1631    grok.name('cedit')
1632    grok.require('waeup.handleStudent')
1633    label = _('Edit clearance data')
1634
1635    @property
1636    def form_fields(self):
1637        cm = getattr(self.context,'current_mode', None)
1638        if cm is not None and cm.startswith('pg'):
1639            form_fields = grok.AutoFields(IPGStudentClearance).omit('clearance_locked')
1640        else:
1641            form_fields = grok.AutoFields(IUGStudentClearance).omit('clearance_locked')
1642        form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
1643        return form_fields
1644
1645    def update(self):
1646        if self.context.clearance_locked:
1647            emit_lock_message(self)
1648            return
1649        return super(StudentClearanceEditFormPage, self).update()
1650
1651    @action(_('Save'), style='primary')
1652    def save(self, **data):
1653        self.applyData(self.context, **data)
1654        self.flash(_('Clearance form has been saved.'))
1655        return
1656
1657    def dataNotComplete(self):
1658        """To be implemented in the customization package.
1659        """
1660        return False
1661
1662    @action(_('Save and request clearance'), style='primary')
1663    def requestClearance(self, **data):
1664        self.applyData(self.context, **data)
1665        if self.dataNotComplete():
1666            self.flash(self.dataNotComplete())
1667            return
1668        self.flash(_('Clearance form has been saved.'))
1669        self.redirect(self.url(self.context,'request_clearance'))
1670        return
1671
1672class RequestClearancePage(KofaPage):
1673    grok.context(IStudent)
1674    grok.name('request_clearance')
1675    grok.require('waeup.handleStudent')
1676    grok.template('enterpin')
1677    label = _('Request clearance')
1678    notice = _('Enter the CLR access code used for starting clearance.')
1679    ac_prefix = 'CLR'
1680    pnav = 4
1681    buttonname = _('Request clearance now')
1682
1683    def update(self, SUBMIT=None):
1684        self.ac_series = self.request.form.get('ac_series', None)
1685        self.ac_number = self.request.form.get('ac_number', None)
1686        if SUBMIT is None:
1687            return
1688        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1689        if self.context.clr_code != pin:
1690            self.flash(_("This isn't your CLR access code."))
1691            return
1692        state = IWorkflowState(self.context).getState()
1693        # This shouldn't happen, but the application officer
1694        # might have forgotten to lock the form after changing the state
1695        if state != CLEARANCE:
1696            self.flash(_('This form cannot be submitted. Wrong state!'))
1697            return
1698        IWorkflowInfo(self.context).fireTransition('request_clearance')
1699        self.flash(_('Clearance has been requested.'))
1700        self.redirect(self.url(self.context))
1701        return
1702
1703class StartCourseRegistrationPage(KofaPage):
1704    grok.context(IStudentStudyCourse)
1705    grok.name('start_course_registration')
1706    grok.require('waeup.handleStudent')
1707    grok.template('enterpin')
1708    label = _('Start course registration')
1709    ac_prefix = 'SFE'
1710    notice = ''
1711    pnav = 4
1712    buttonname = _('Start course registration now')
1713
1714    def update(self, SUBMIT=None):
1715        if not self.context.getStudent().state in (CLEARED,RETURNING):
1716            self.flash(_("Wrong state"))
1717            self.redirect(self.url(self.context))
1718            return
1719        self.ac_series = self.request.form.get('ac_series', None)
1720        self.ac_number = self.request.form.get('ac_number', None)
1721
1722        if SUBMIT is None:
1723            return
1724        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1725        code = get_access_code(pin)
1726        if not code:
1727            self.flash(_('Activation code is invalid.'))
1728            return
1729        # Mark pin as used (this also fires a pin related transition)
1730        # and fire transition start_clearance
1731        if code.state == USED:
1732            self.flash(_('Activation code has already been used.'))
1733            return
1734        else:
1735            comment = _(u"invalidated")
1736            # Here we know that the ac is in state initialized so we do not
1737            # expect an exception, but the owner might be different
1738            if not invalidate_accesscode(
1739                pin,comment,self.context.getStudent().student_id):
1740                self.flash(_('You are not the owner of this access code.'))
1741                return
1742        if self.context.getStudent().state == CLEARED:
1743            IWorkflowInfo(self.context.getStudent()).fireTransition(
1744                'pay_first_school_fee')
1745        elif self.context.getStudent().state == RETURNING:
1746            IWorkflowInfo(self.context.getStudent()).fireTransition(
1747                'pay_school_fee')
1748        self.flash(_('Course registration has been started.'))
1749        self.redirect(self.url(self.context))
1750        return
1751
1752class AddStudyLevelFormPage(KofaEditFormPage):
1753    """ Page for students to add current study levels
1754    """
1755    grok.context(IStudentStudyCourse)
1756    grok.name('add')
1757    grok.require('waeup.handleStudent')
1758    grok.template('studyleveladdpage')
1759    form_fields = grok.AutoFields(IStudentStudyCourse)
1760    pnav = 4
1761
1762    @property
1763    def label(self):
1764        studylevelsource = StudyLevelSource().factory
1765        code = self.context.current_level
1766        title = studylevelsource.getTitle(self.context, code)
1767        return _('Add current level ${a}', mapping = {'a':title})
1768
1769    def update(self):
1770        if self.context.getStudent().state != PAID:
1771            emit_lock_message(self)
1772            return
1773        super(AddStudyLevelFormPage, self).update()
1774        return
1775
1776    @action(_('Create course list now'), style='primary')
1777    def addStudyLevel(self, **data):
1778        studylevel = StudentStudyLevel()
1779        studylevel.level = self.context.current_level
1780        studylevel.level_session = self.context.current_session
1781        try:
1782            self.context.addStudentStudyLevel(
1783                self.context.certificate,studylevel)
1784        except KeyError:
1785            self.flash(_('This level exists.'))
1786        self.redirect(self.url(self.context))
1787        return
1788
1789class StudyLevelEditFormPage(KofaEditFormPage):
1790    """ Page to edit the student study level data by students
1791    """
1792    grok.context(IStudentStudyLevel)
1793    grok.name('edit')
1794    grok.require('waeup.handleStudent')
1795    grok.template('studyleveleditpage')
1796    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1797        'level_session', 'level_verdict')
1798    pnav = 4
1799
1800    def update(self):
1801        if self.context.getStudent().state != PAID:
1802            emit_lock_message(self)
1803            return
1804        super(StudyLevelEditFormPage, self).update()
1805        datatable.need()
1806        warning.need()
1807        return
1808
1809    @property
1810    def label(self):
1811        # Here we know that the cookie has been set
1812        lang = self.request.cookies.get('kofa.language')
1813        level_title = translate(self.context.level_title, 'waeup.kofa',
1814            target_language=lang)
1815        return _('Add and remove course tickets of study level ${a}',
1816            mapping = {'a':level_title})
1817
1818    @property
1819    def total_credits(self):
1820        total_credits = 0
1821        for key, val in self.context.items():
1822            total_credits += val.credits
1823        return total_credits
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        IWorkflowInfo(self.context.getStudent()).fireTransition(
1855            'register_courses')
1856        self.flash(_('Course list has been registered.'))
1857        self.redirect(self.url(self.context))
1858        return
1859
1860class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1861    """Add a course ticket by student.
1862    """
1863    grok.name('ctadd')
1864    grok.require('waeup.handleStudent')
1865    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1866        'score', 'mandatory', 'automatic', 'carry_over')
1867
1868    def update(self):
1869        if self.context.getStudent().state != PAID:
1870            emit_lock_message(self)
1871            return
1872        super(CourseTicketAddFormPage2, self).update()
1873        return
1874
1875    @action(_('Add course ticket'))
1876    def addCourseTicket(self, **data):
1877        # Safety belt
1878        if self.context.getStudent().state != PAID:
1879            return
1880        ticket = CourseTicket()
1881        course = data['course']
1882        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
1883            setattr(ticket, name, getattr(course, name))
1884        ticket.automatic = False
1885        try:
1886            self.context.addCourseTicket(ticket)
1887        except KeyError:
1888            self.flash(_('The ticket exists.'))
1889            return
1890        self.flash(_('Successfully added ${a}.',
1891            mapping = {'a':ticket.code}))
1892        self.redirect(self.url(self.context, u'@@edit'))
1893        return
1894
1895class ChangePasswordRequestPage(KofaForm):
1896    """Captcha'd page for students to request a password change.
1897    """
1898    grok.context(IUniversity)
1899    grok.name('sendpw')
1900    grok.require('waeup.Anonymous')
1901    grok.template('sendpassword')
1902    label = _('Send me a new password')
1903    form_fields = grok.AutoFields(IStudentChangePassword)
1904
1905    def update(self):
1906        # Handle captcha
1907        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1908        self.captcha_result = self.captcha.verify(self.request)
1909        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1910        return
1911
1912    @action(_('Get new login credentials'), style='primary')
1913    def request(self, **data):
1914        if not self.captcha_result.is_valid:
1915            # Captcha will display error messages automatically.
1916            # No need to flash something.
1917            return
1918        # Search student or applicant
1919        reg_number = data['reg_number']
1920        email = data['email']
1921        cat = queryUtility(ICatalog, name='students_catalog')
1922        results = cat.searchResults(
1923            reg_number=(reg_number, reg_number),
1924            email=(email,email))
1925        if len(results) == 0:
1926            # Try also the applicants_catalog if no student record was found.
1927            cat = queryUtility(ICatalog, name='applicants_catalog')
1928            if cat is None:
1929                self.flash(_('Application package not installed.'))
1930                return
1931            results = cat.searchResults(
1932                reg_number=(reg_number, reg_number),
1933                email=(email,email))
1934        if len(results) == 0:
1935            self.flash(_('No record found.'))
1936            return
1937        student = list(results)[0]
1938        # Change password
1939        kofa_utils = getUtility(IKofaUtils)
1940        pwd = kofa_utils.genPassword()
1941        IUserAccount(student).setPassword(pwd)
1942        # Send email with new redentials
1943        msg = _('You have successfully changed your password for the')
1944        login_url = self.url(grok.getSite(), 'login')
1945        success = kofa_utils.sendCredentials(
1946            IUserAccount(student),pwd,login_url,msg)
1947        if success:
1948            self.flash(_('An email with your user name and password ' +
1949                'has been sent to ${a}.', mapping = {'a':email}))
1950        else:
1951            self.flash(_('An smtp server error occurred.'))
1952        return
1953
1954class SetPasswordPage(KofaPage):
1955    grok.context(IKofaObject)
1956    grok.name('setpassword')
1957    grok.require('waeup.Anonymous')
1958    grok.template('setpassword')
1959    label = _('Set password for first-time login')
1960    ac_prefix = 'PWD'
1961    pnav = 0
1962    set_button = _('Set')
1963
1964    def update(self, SUBMIT=None):
1965        self.reg_number = self.request.form.get('reg_number', None)
1966        self.ac_series = self.request.form.get('ac_series', None)
1967        self.ac_number = self.request.form.get('ac_number', None)
1968
1969        if SUBMIT is None:
1970            return
1971        hitlist = search(query=self.reg_number,
1972            searchtype='reg_number', view=self)
1973        if not hitlist:
1974            self.flash(_('No student found.'))
1975            return
1976        if len(hitlist) != 1:   # Cannot happen but anyway
1977            self.flash(_('More than one student found.'))
1978            return
1979        student = hitlist[0].context
1980        self.student_id = student.student_id
1981        student_pw = student.password
1982        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1983        code = get_access_code(pin)
1984        if not code:
1985            self.flash(_('Access code is invalid.'))
1986            return
1987        if student_pw and pin == student.adm_code:
1988            self.flash(_(
1989                'Password has already been set. Your Student Id is ${a}',
1990                mapping = {'a':self.student_id}))
1991            return
1992        elif student_pw:
1993            self.flash(
1994                _('Password has already been set. You are using the ' +
1995                'wrong Access Code.'))
1996            return
1997        # Mark pin as used (this also fires a pin related transition)
1998        # and set student password
1999        if code.state == USED:
2000            self.flash(_('Access code has already been used.'))
2001            return
2002        else:
2003            comment = _(u"invalidated")
2004            # Here we know that the ac is in state initialized so we do not
2005            # expect an exception
2006            invalidate_accesscode(pin,comment)
2007            IUserAccount(student).setPassword(self.ac_number)
2008            student.adm_code = pin
2009        self.flash(_('Password has been set. Your Student Id is ${a}',
2010            mapping = {'a':self.student_id}))
2011        return
Note: See TracBrowser for help on using the repository browser.