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

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

Log bed booking and relocation.

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