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

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

Add hostels and beds exporter (tests will follow).

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