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

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

Change label of ExportPDFBedTicketSlipPage. Fix datetime format.

  • Property svn:keywords set to Id
File size: 80.3 KB
Line 
1## $Id: browser.py 9201 2012-09-19 10:16:38Z 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    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1550    pnav = 4
1551
1552    @property
1553    def label(self):
1554        return _('Bed Ticket for Session ${a}',
1555            mapping = {'a':self.context.getSessionString()})
1556
1557class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1558    """Deliver a PDF slip of the context.
1559    """
1560    grok.context(IBedTicket)
1561    grok.name('bed_allocation.pdf')
1562    grok.require('waeup.handleAccommodation')
1563    form_fields = grok.AutoFields(IBedTicket)
1564    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1565    prefix = 'form'
1566
1567    @property
1568    def title(self):
1569        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1570        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1571            target_language=portal_language)
1572
1573    @property
1574    def label(self):
1575        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1576        #return translate(_('Bed Allocation: '),
1577        #    'waeup.kofa', target_language=portal_language) \
1578        #    + ' %s' % self.context.bed_coordinates
1579        return translate(_('Bed Allocation Slip'),
1580            'waeup.kofa', target_language=portal_language) \
1581            + ' %s' % self.context.getSessionString()
1582
1583    def render(self):
1584        studentview = StudentBasePDFFormPage(self.context.student,
1585            self.request)
1586        students_utils = getUtility(IStudentsUtils)
1587        return students_utils.renderPDF(
1588            self, 'bed_allocation.pdf',
1589            self.context.student, studentview)
1590
1591class BedTicketRelocationPage(UtilityView, grok.View):
1592    """ Callback view
1593    """
1594    grok.context(IBedTicket)
1595    grok.name('relocate')
1596    grok.require('waeup.manageHostels')
1597
1598    # Relocate student if student parameters have changed or the bed_type
1599    # of the bed has changed
1600    def update(self):
1601        student = self.context.student
1602        students_utils = getUtility(IStudentsUtils)
1603        acc_details  = students_utils.getAccommodationDetails(student)
1604        if self.context.bed != None and \
1605              'reserved' in self.context.bed.bed_type:
1606            self.flash(_("Students in reserved beds can't be relocated."))
1607            self.redirect(self.url(self.context))
1608            return
1609        if acc_details['bt'] == self.context.bed_type and \
1610                self.context.bed != None and \
1611                self.context.bed.bed_type == self.context.bed_type:
1612            self.flash(_("Student can't be relocated."))
1613            self.redirect(self.url(self.context))
1614            return
1615        # Search a bed
1616        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1617        entries = cat.searchResults(
1618            owner=(student.student_id,student.student_id))
1619        if len(entries) and self.context.bed == None:
1620            # If booking has been cancelled but other bed space has been
1621            # manually allocated after cancellation use this bed
1622            new_bed = [entry for entry in entries][0]
1623        else:
1624            # Search for other available beds
1625            entries = cat.searchResults(
1626                bed_type=(acc_details['bt'],acc_details['bt']))
1627            available_beds = [
1628                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1629            if available_beds:
1630                students_utils = getUtility(IStudentsUtils)
1631                new_bed = students_utils.selectBed(available_beds)
1632                new_bed.bookBed(student.student_id)
1633            else:
1634                self.flash(_('There is no free bed in your category ${a}.',
1635                    mapping = {'a':acc_details['bt']}))
1636                self.redirect(self.url(self.context))
1637                return
1638        # Release old bed if exists
1639        if self.context.bed != None:
1640            self.context.bed.owner = NOT_OCCUPIED
1641            notify(grok.ObjectModifiedEvent(self.context.bed))
1642        # Alocate new bed
1643        self.context.bed_type = acc_details['bt']
1644        self.context.bed = new_bed
1645        hall_title = new_bed.__parent__.hostel_name
1646        coordinates = new_bed.coordinates[1:]
1647        block, room_nr, bed_nr = coordinates
1648        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1649            'a':hall_title, 'b':block,
1650            'c':room_nr, 'd':bed_nr,
1651            'e':new_bed.bed_type})
1652        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1653        self.context.bed_coordinates = translate(
1654            bc, 'waeup.kofa',target_language=portal_language)
1655        self.flash(_('Student relocated: ${a}',
1656            mapping = {'a':self.context.bed_coordinates}))
1657        self.redirect(self.url(self.context))
1658        return
1659
1660    def render(self):
1661        return
1662
1663class StudentHistoryPage(KofaPage):
1664    """ Page to display student clearance data
1665    """
1666    grok.context(IStudent)
1667    grok.name('history')
1668    grok.require('waeup.viewStudent')
1669    grok.template('studenthistory')
1670    pnav = 4
1671
1672    @property
1673    def label(self):
1674        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1675
1676# Pages for students only
1677
1678class StudentBaseEditFormPage(KofaEditFormPage):
1679    """ View to edit student base data
1680    """
1681    grok.context(IStudent)
1682    grok.name('edit_base')
1683    grok.require('waeup.handleStudent')
1684    form_fields = grok.AutoFields(IStudentBase).select(
1685        'email', 'phone')
1686    label = _('Edit base data')
1687    pnav = 4
1688
1689    @action(_('Save'), style='primary')
1690    def save(self, **data):
1691        msave(self, **data)
1692        return
1693
1694class StudentChangePasswordPage(KofaEditFormPage):
1695    """ View to manage student base data
1696    """
1697    grok.context(IStudent)
1698    grok.name('change_password')
1699    grok.require('waeup.handleStudent')
1700    grok.template('change_password')
1701    label = _('Change password')
1702    pnav = 4
1703
1704    @action(_('Save'), style='primary')
1705    def save(self, **data):
1706        form = self.request.form
1707        password = form.get('change_password', None)
1708        password_ctl = form.get('change_password_repeat', None)
1709        if password:
1710            validator = getUtility(IPasswordValidator)
1711            errors = validator.validate_password(password, password_ctl)
1712            if not errors:
1713                IUserAccount(self.context).setPassword(password)
1714                self.context.writeLogMessage(self, 'saved: password')
1715                self.flash(_('Password changed.'))
1716            else:
1717                self.flash( ' '.join(errors))
1718        return
1719
1720class StudentFilesUploadPage(KofaPage):
1721    """ View to upload files by student
1722    """
1723    grok.context(IStudent)
1724    grok.name('change_portrait')
1725    grok.require('waeup.uploadStudentFile')
1726    grok.template('filesuploadpage')
1727    label = _('Upload portrait')
1728    pnav = 4
1729
1730    def update(self):
1731        if self.context.student.state != ADMITTED:
1732            emit_lock_message(self)
1733            return
1734        super(StudentFilesUploadPage, self).update()
1735        return
1736
1737class StartClearancePage(KofaPage):
1738    grok.context(IStudent)
1739    grok.name('start_clearance')
1740    grok.require('waeup.handleStudent')
1741    grok.template('enterpin')
1742    label = _('Start clearance')
1743    ac_prefix = 'CLR'
1744    notice = ''
1745    pnav = 4
1746    buttonname = _('Start clearance now')
1747
1748    @property
1749    def all_required_fields_filled(self):
1750        if self.context.email and self.context.phone:
1751            return True
1752        return False
1753
1754    @property
1755    def portrait_uploaded(self):
1756        store = getUtility(IExtFileStore)
1757        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1758            return True
1759        return False
1760
1761    def update(self, SUBMIT=None):
1762        if not self.context.state == ADMITTED:
1763            self.flash(_("Wrong state"))
1764            self.redirect(self.url(self.context))
1765            return
1766        if not self.portrait_uploaded:
1767            self.flash(_("No portrait uploaded."))
1768            self.redirect(self.url(self.context, 'change_portrait'))
1769            return
1770        if not self.all_required_fields_filled:
1771            self.flash(_("Not all required fields filled."))
1772            self.redirect(self.url(self.context, 'edit_base'))
1773            return
1774        self.ac_series = self.request.form.get('ac_series', None)
1775        self.ac_number = self.request.form.get('ac_number', None)
1776
1777        if SUBMIT is None:
1778            return
1779        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1780        code = get_access_code(pin)
1781        if not code:
1782            self.flash(_('Activation code is invalid.'))
1783            return
1784        if code.state == USED:
1785            self.flash(_('Activation code has already been used.'))
1786            return
1787        # Mark pin as used (this also fires a pin related transition)
1788        # and fire transition start_clearance
1789        comment = _(u"invalidated")
1790        # Here we know that the ac is in state initialized so we do not
1791        # expect an exception, but the owner might be different
1792        if not invalidate_accesscode(pin, comment, self.context.student_id):
1793            self.flash(_('You are not the owner of this access code.'))
1794            return
1795        self.context.clr_code = pin
1796        IWorkflowInfo(self.context).fireTransition('start_clearance')
1797        self.flash(_('Clearance process has been started.'))
1798        self.redirect(self.url(self.context,'cedit'))
1799        return
1800
1801class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1802    """ View to edit student clearance data by student
1803    """
1804    grok.context(IStudent)
1805    grok.name('cedit')
1806    grok.require('waeup.handleStudent')
1807    label = _('Edit clearance data')
1808
1809    @property
1810    def form_fields(self):
1811        if self.context.is_postgrad:
1812            form_fields = grok.AutoFields(IPGStudentClearance).omit(
1813                'clearance_locked', 'clr_code')
1814        else:
1815            form_fields = grok.AutoFields(IUGStudentClearance).omit(
1816                'clearance_locked', 'clr_code')
1817        return form_fields
1818
1819    def update(self):
1820        if self.context.clearance_locked:
1821            emit_lock_message(self)
1822            return
1823        return super(StudentClearanceEditFormPage, self).update()
1824
1825    @action(_('Save'), style='primary')
1826    def save(self, **data):
1827        self.applyData(self.context, **data)
1828        self.flash(_('Clearance form has been saved.'))
1829        return
1830
1831    def dataNotComplete(self):
1832        """To be implemented in the customization package.
1833        """
1834        return False
1835
1836    @action(_('Save and request clearance'), style='primary')
1837    def requestClearance(self, **data):
1838        self.applyData(self.context, **data)
1839        if self.dataNotComplete():
1840            self.flash(self.dataNotComplete())
1841            return
1842        self.flash(_('Clearance form has been saved.'))
1843        if self.context.clr_code:
1844            self.redirect(self.url(self.context, 'request_clearance'))
1845        else:
1846            # We bypass the request_clearance page if student
1847            # has been imported in state 'clearance started' and
1848            # no clr_code was entered before.
1849            state = IWorkflowState(self.context).getState()
1850            if state != CLEARANCE:
1851                # This shouldn't happen, but the application officer
1852                # might have forgotten to lock the form after changing the state
1853                self.flash(_('This form cannot be submitted. Wrong state!'))
1854                return
1855            IWorkflowInfo(self.context).fireTransition('request_clearance')
1856            self.flash(_('Clearance has been requested.'))
1857            self.redirect(self.url(self.context))
1858        return
1859
1860class RequestClearancePage(KofaPage):
1861    grok.context(IStudent)
1862    grok.name('request_clearance')
1863    grok.require('waeup.handleStudent')
1864    grok.template('enterpin')
1865    label = _('Request clearance')
1866    notice = _('Enter the CLR access code used for starting clearance.')
1867    ac_prefix = 'CLR'
1868    pnav = 4
1869    buttonname = _('Request clearance now')
1870
1871    def update(self, SUBMIT=None):
1872        self.ac_series = self.request.form.get('ac_series', None)
1873        self.ac_number = self.request.form.get('ac_number', None)
1874        if SUBMIT is None:
1875            return
1876        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1877        if self.context.clr_code and self.context.clr_code != pin:
1878            self.flash(_("This isn't your CLR access code."))
1879            return
1880        state = IWorkflowState(self.context).getState()
1881        if state != CLEARANCE:
1882            # This shouldn't happen, but the application officer
1883            # might have forgotten to lock the form after changing the state
1884            self.flash(_('This form cannot be submitted. Wrong state!'))
1885            return
1886        IWorkflowInfo(self.context).fireTransition('request_clearance')
1887        self.flash(_('Clearance has been requested.'))
1888        self.redirect(self.url(self.context))
1889        return
1890
1891class StartSessionPage(KofaPage):
1892    grok.context(IStudentStudyCourse)
1893    grok.name('start_session')
1894    grok.require('waeup.handleStudent')
1895    grok.template('enterpin')
1896    label = _('Start session')
1897    ac_prefix = 'SFE'
1898    notice = ''
1899    pnav = 4
1900    buttonname = _('Start now')
1901
1902    def update(self, SUBMIT=None):
1903        if not self.context.is_current:
1904            emit_lock_message(self)
1905            return
1906        super(StartSessionPage, self).update()
1907        if not self.context.next_session_allowed:
1908            self.flash(_("You are not entitled to start session."))
1909            self.redirect(self.url(self.context))
1910            return
1911        self.ac_series = self.request.form.get('ac_series', None)
1912        self.ac_number = self.request.form.get('ac_number', None)
1913
1914        if SUBMIT is None:
1915            return
1916        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1917        code = get_access_code(pin)
1918        if not code:
1919            self.flash(_('Activation code is invalid.'))
1920            return
1921        # Mark pin as used (this also fires a pin related transition)
1922        if code.state == USED:
1923            self.flash(_('Activation code has already been used.'))
1924            return
1925        else:
1926            comment = _(u"invalidated")
1927            # Here we know that the ac is in state initialized so we do not
1928            # expect an error, but the owner might be different
1929            if not invalidate_accesscode(
1930                pin,comment,self.context.student.student_id):
1931                self.flash(_('You are not the owner of this access code.'))
1932                return
1933        if self.context.student.state == CLEARED:
1934            IWorkflowInfo(self.context.student).fireTransition(
1935                'pay_first_school_fee')
1936        elif self.context.student.state == RETURNING:
1937            IWorkflowInfo(self.context.student).fireTransition(
1938                'pay_school_fee')
1939        elif self.context.student.state == PAID:
1940            IWorkflowInfo(self.context.student).fireTransition(
1941                'pay_pg_fee')
1942        self.flash(_('Session started.'))
1943        self.redirect(self.url(self.context))
1944        return
1945
1946class AddStudyLevelFormPage(KofaEditFormPage):
1947    """ Page for students to add current study levels
1948    """
1949    grok.context(IStudentStudyCourse)
1950    grok.name('add')
1951    grok.require('waeup.handleStudent')
1952    grok.template('studyleveladdpage')
1953    form_fields = grok.AutoFields(IStudentStudyCourse)
1954    pnav = 4
1955
1956    @property
1957    def label(self):
1958        studylevelsource = StudyLevelSource().factory
1959        code = self.context.current_level
1960        title = studylevelsource.getTitle(self.context, code)
1961        return _('Add current level ${a}', mapping = {'a':title})
1962
1963    def update(self):
1964        if not self.context.is_current:
1965            emit_lock_message(self)
1966            return
1967        if self.context.student.state != PAID:
1968            emit_lock_message(self)
1969            return
1970        super(AddStudyLevelFormPage, self).update()
1971        return
1972
1973    @action(_('Create course list now'), style='primary')
1974    def addStudyLevel(self, **data):
1975        studylevel = createObject(u'waeup.StudentStudyLevel')
1976        studylevel.level = self.context.current_level
1977        studylevel.level_session = self.context.current_session
1978        try:
1979            self.context.addStudentStudyLevel(
1980                self.context.certificate,studylevel)
1981        except KeyError:
1982            self.flash(_('This level exists.'))
1983        self.redirect(self.url(self.context))
1984        return
1985
1986class StudyLevelEditFormPage(KofaEditFormPage):
1987    """ Page to edit the student study level data by students
1988    """
1989    grok.context(IStudentStudyLevel)
1990    grok.name('edit')
1991    grok.require('waeup.handleStudent')
1992    grok.template('studyleveleditpage')
1993    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1994        'level_session', 'level_verdict')
1995    pnav = 4
1996    max_credits = 50
1997
1998    def update(self):
1999        if not self.context.__parent__.is_current:
2000            emit_lock_message(self)
2001            return
2002        if self.context.student.state != PAID:
2003            emit_lock_message(self)
2004            return
2005        super(StudyLevelEditFormPage, self).update()
2006        datatable.need()
2007        warning.need()
2008        return
2009
2010    @property
2011    def label(self):
2012        # Here we know that the cookie has been set
2013        lang = self.request.cookies.get('kofa.language')
2014        level_title = translate(self.context.level_title, 'waeup.kofa',
2015            target_language=lang)
2016        return _('Edit course list of ${a}',
2017            mapping = {'a':level_title})
2018
2019    @property
2020    def total_credits(self):
2021        total_credits = 0
2022        for key, val in self.context.items():
2023            total_credits += val.credits
2024        return total_credits
2025
2026    @property
2027    def translated_values(self):
2028        return translated_values(self)
2029
2030    @action(_('Add course ticket'))
2031    def addCourseTicket(self, **data):
2032        self.redirect(self.url(self.context, 'ctadd'))
2033
2034    @jsaction(_('Remove selected tickets'))
2035    def delCourseTicket(self, **data):
2036        form = self.request.form
2037        if form.has_key('val_id'):
2038            child_id = form['val_id']
2039        else:
2040            self.flash(_('No ticket selected.'))
2041            self.redirect(self.url(self.context, '@@edit'))
2042            return
2043        if not isinstance(child_id, list):
2044            child_id = [child_id]
2045        deleted = []
2046        for id in child_id:
2047            # Students are not allowed to remove core tickets
2048            if not self.context[id].mandatory:
2049                del self.context[id]
2050                deleted.append(id)
2051        if len(deleted):
2052            self.flash(_('Successfully removed: ${a}',
2053                mapping = {'a':', '.join(deleted)}))
2054        self.redirect(self.url(self.context, u'@@edit'))
2055        return
2056
2057    @action(_('Register course list'), style='primary')
2058    def registerCourses(self, **data):
2059        if self.total_credits > self.max_credits:
2060            self.flash(_('Maximum credits of ${a} exceeded.',
2061                mapping = {'a':self.max_credits}))
2062            return
2063        IWorkflowInfo(self.context.student).fireTransition(
2064            'register_courses')
2065        self.flash(_('Course list has been registered.'))
2066        self.redirect(self.url(self.context))
2067        return
2068
2069class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2070    """Add a course ticket by student.
2071    """
2072    grok.name('ctadd')
2073    grok.require('waeup.handleStudent')
2074    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
2075        'score', 'mandatory', 'automatic', 'carry_over')
2076
2077    def update(self):
2078        if self.context.student.state != PAID:
2079            emit_lock_message(self)
2080            return
2081        super(CourseTicketAddFormPage2, self).update()
2082        return
2083
2084    @action(_('Add course ticket'))
2085    def addCourseTicket(self, **data):
2086        # Safety belt
2087        if self.context.student.state != PAID:
2088            return
2089        ticket = createObject(u'waeup.CourseTicket')
2090        course = data['course']
2091        ticket.automatic = False
2092        ticket.carry_over = False
2093        try:
2094            self.context.addCourseTicket(ticket, course)
2095        except KeyError:
2096            self.flash(_('The ticket exists.'))
2097            return
2098        self.flash(_('Successfully added ${a}.',
2099            mapping = {'a':ticket.code}))
2100        self.redirect(self.url(self.context, u'@@edit'))
2101        return
2102
2103
2104class SetPasswordPage(KofaPage):
2105    grok.context(IKofaObject)
2106    grok.name('setpassword')
2107    grok.require('waeup.Anonymous')
2108    grok.template('setpassword')
2109    label = _('Set password for first-time login')
2110    ac_prefix = 'PWD'
2111    pnav = 0
2112    set_button = _('Set')
2113
2114    def update(self, SUBMIT=None):
2115        self.reg_number = self.request.form.get('reg_number', None)
2116        self.ac_series = self.request.form.get('ac_series', None)
2117        self.ac_number = self.request.form.get('ac_number', None)
2118
2119        if SUBMIT is None:
2120            return
2121        hitlist = search(query=self.reg_number,
2122            searchtype='reg_number', view=self)
2123        if not hitlist:
2124            self.flash(_('No student found.'))
2125            return
2126        if len(hitlist) != 1:   # Cannot happen but anyway
2127            self.flash(_('More than one student found.'))
2128            return
2129        student = hitlist[0].context
2130        self.student_id = student.student_id
2131        student_pw = student.password
2132        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2133        code = get_access_code(pin)
2134        if not code:
2135            self.flash(_('Access code is invalid.'))
2136            return
2137        if student_pw and pin == student.adm_code:
2138            self.flash(_(
2139                'Password has already been set. Your Student Id is ${a}',
2140                mapping = {'a':self.student_id}))
2141            return
2142        elif student_pw:
2143            self.flash(
2144                _('Password has already been set. You are using the ' +
2145                'wrong Access Code.'))
2146            return
2147        # Mark pin as used (this also fires a pin related transition)
2148        # and set student password
2149        if code.state == USED:
2150            self.flash(_('Access code has already been used.'))
2151            return
2152        else:
2153            comment = _(u"invalidated")
2154            # Here we know that the ac is in state initialized so we do not
2155            # expect an exception
2156            invalidate_accesscode(pin,comment)
2157            IUserAccount(student).setPassword(self.ac_number)
2158            student.adm_code = pin
2159        self.flash(_('Password has been set. Your Student Id is ${a}',
2160            mapping = {'a':self.student_id}))
2161        return
2162
2163class StudentRequestPasswordPage(KofaAddFormPage):
2164    """Captcha'd registration page for applicants.
2165    """
2166    grok.name('requestpw')
2167    grok.require('waeup.Anonymous')
2168    grok.template('requestpw')
2169    form_fields = grok.AutoFields(IStudentRequestPW).select(
2170        'firstname','number','email')
2171    label = _('Request password for first-time login')
2172
2173    def update(self):
2174        # Handle captcha
2175        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2176        self.captcha_result = self.captcha.verify(self.request)
2177        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2178        return
2179
2180    def _redirect(self, email, password, student_id):
2181        # Forward only email to landing page in base package.
2182        self.redirect(self.url(self.context, 'requestpw_complete',
2183            data = dict(email=email)))
2184        return
2185
2186    def _pw_used(self):
2187        # XXX: False if password has not been used. We need an extra
2188        #      attribute which remembers if student logged in.
2189        return True
2190
2191    @action(_('Send login credentials to email address'), style='primary')
2192    def get_credentials(self, **data):
2193        if not self.captcha_result.is_valid:
2194            # Captcha will display error messages automatically.
2195            # No need to flash something.
2196            return
2197        number = data.get('number','')
2198        firstname = data.get('firstname','')
2199        cat = getUtility(ICatalog, name='students_catalog')
2200        results = list(
2201            cat.searchResults(reg_number=(number, number)))
2202        if not results:
2203            results = list(
2204                cat.searchResults(matric_number=(number, number)))
2205        if results:
2206            student = results[0]
2207            if getattr(student,'firstname',None) is None:
2208                self.flash(_('An error occurred.'))
2209                return
2210            elif student.firstname.lower() != firstname.lower():
2211                # Don't tell the truth here. Anonymous must not
2212                # know that a record was found and only the firstname
2213                # verification failed.
2214                self.flash(_('No student record found.'))
2215                return
2216            elif student.password is not None and self._pw_used:
2217                self.flash(_('Your password has already been set and used. '
2218                             'Please proceed to the login page.'))
2219                return
2220            # Store email address but nothing else.
2221            student.email = data['email']
2222            notify(grok.ObjectModifiedEvent(student))
2223        else:
2224            # No record found, this is the truth.
2225            self.flash(_('No student record found.'))
2226            return
2227
2228        kofa_utils = getUtility(IKofaUtils)
2229        password = kofa_utils.genPassword()
2230        mandate = PasswordMandate()
2231        mandate.params['password'] = password
2232        mandate.params['user'] = student
2233        site = grok.getSite()
2234        site['mandates'].addMandate(mandate)
2235        # Send email with credentials
2236        args = {'mandate_id':mandate.mandate_id}
2237        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2238        url_info = u'Confirmation link: %s' % mandate_url
2239        msg = _('You have successfully requested a password for the')
2240        if kofa_utils.sendCredentials(IUserAccount(student),
2241            password, url_info, msg):
2242            email_sent = student.email
2243        else:
2244            email_sent = None
2245        self._redirect(email=email_sent, password=password,
2246            student_id=student.student_id)
2247        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2248        self.context.logger.info(
2249            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2250        return
2251
2252class StudentRequestPasswordEmailSent(KofaPage):
2253    """Landing page after successful password request.
2254
2255    """
2256    grok.name('requestpw_complete')
2257    grok.require('waeup.Public')
2258    grok.template('requestpwmailsent')
2259    label = _('Your password request was successful.')
2260
2261    def update(self, email=None, student_id=None, password=None):
2262        self.email = email
2263        self.password = password
2264        self.student_id = student_id
2265        return
Note: See TracBrowser for help on using the repository browser.