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

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

Use ChangePasswordRequestPage? also for applicants.

Change password requests require a registration number.

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