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

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

Wow, we didn't notify the students_catalog when a sudycourse was saved and changed. The browser tests didn't catch this malfunction because there we saved data manually and notified the catalog afterwards.

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