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

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

Start customization of base data fields on pdf slips.

  • Property svn:keywords set to Id
File size: 83.9 KB
Line 
1## $Id: browser.py 9374 2012-10-21 20:28:28Z 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
603    @property
604    def form_fields(self):
605        if self.context.is_postgrad:
606            form_fields = grok.AutoFields(
607                IPGStudentClearance).omit('clearance_locked')
608        else:
609            form_fields = grok.AutoFields(
610                IUGStudentClearance).omit('clearance_locked')
611        return form_fields
612
613    @property
614    def title(self):
615        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
616        return translate(_('Clearance Data'), 'waeup.kofa',
617            target_language=portal_language)
618
619    @property
620    def label(self):
621        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
622        return translate(_('Clearance Slip of'),
623            'waeup.kofa', target_language=portal_language) \
624            + ' %s' % self.context.display_fullname
625
626    def _signatures(self):
627        if self.context.state == CLEARED:
628            return (_('Student Signature'), _('Clearance Officer Signature'))
629        return
630
631    def render(self):
632        studentview = StudentBasePDFFormPage(self.context.student,
633            self.request, ('password', 'suspended', 'phone', 'adm_code', 'sex'))
634        students_utils = getUtility(IStudentsUtils)
635        return students_utils.renderPDF(
636            self, 'clearance.pdf',
637            self.context.student, studentview, signatures=self._signatures())
638
639class StudentClearanceManageFormPage(KofaEditFormPage):
640    """ Page to manage student clearance data
641    """
642    grok.context(IStudent)
643    grok.name('manage_clearance')
644    grok.require('waeup.manageStudent')
645    grok.template('clearanceeditpage')
646    label = _('Manage clearance data')
647    pnav = 4
648
649    @property
650    def separators(self):
651        return getUtility(IStudentsUtils).SEPARATORS_DICT
652
653    @property
654    def form_fields(self):
655        if self.context.is_postgrad:
656            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
657        else:
658            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
659        return form_fields
660
661    def update(self):
662        datepicker.need() # Enable jQuery datepicker in date fields.
663        tabs.need()
664        self.tab1 = self.tab2 = ''
665        qs = self.request.get('QUERY_STRING', '')
666        if not qs:
667            qs = 'tab1'
668        setattr(self, qs, 'active')
669        return super(StudentClearanceManageFormPage, self).update()
670
671    @action(_('Save'), style='primary')
672    def save(self, **data):
673        msave(self, **data)
674        return
675
676class StudentClearPage(UtilityView, grok.View):
677    """ Clear student by clearance officer
678    """
679    grok.context(IStudent)
680    grok.name('clear')
681    grok.require('waeup.clearStudent')
682
683    def update(self):
684        if self.context.state == REQUESTED:
685            IWorkflowInfo(self.context).fireTransition('clear')
686            self.flash(_('Student has been cleared.'))
687        else:
688            self.flash(_('Student is in wrong state.'))
689        self.redirect(self.url(self.context,'view_clearance'))
690        return
691
692    def render(self):
693        return
694
695class StudentRejectClearancePage(UtilityView, grok.View):
696    """ Reject clearance by clearance officers
697    """
698    grok.context(IStudent)
699    grok.name('reject_clearance')
700    grok.require('waeup.clearStudent')
701
702    def update(self):
703        if self.context.state == CLEARED:
704            IWorkflowInfo(self.context).fireTransition('reset4')
705            message = _('Clearance has been annulled.')
706            self.flash(message)
707        elif self.context.state == REQUESTED:
708            IWorkflowInfo(self.context).fireTransition('reset3')
709            message = _('Clearance request has been rejected.')
710            self.flash(message)
711        else:
712            self.flash(_('Student is in wrong state.'))
713            self.redirect(self.url(self.context,'view_clearance'))
714            return
715        args = {'subject':message}
716        self.redirect(self.url(self.context) +
717            '/contactstudent?%s' % urlencode(args))
718        return
719
720    def render(self):
721        return
722
723class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
724    """ Page to display student personal data
725    """
726    grok.context(IStudent)
727    grok.name('view_personal')
728    grok.require('waeup.viewStudent')
729    form_fields = grok.AutoFields(IStudentPersonal)
730    form_fields['perm_address'].custom_widget = BytesDisplayWidget
731    pnav = 4
732
733    @property
734    def label(self):
735        return _('${a}: Personal Data',
736            mapping = {'a':self.context.display_fullname})
737
738class StudentPersonalManageFormPage(KofaEditFormPage):
739    """ Page to manage personal data
740    """
741    grok.context(IStudent)
742    grok.name('manage_personal')
743    grok.require('waeup.manageStudent')
744    form_fields = grok.AutoFields(IStudentPersonal)
745    label = _('Manage personal data')
746    pnav = 4
747
748    @action(_('Save'), style='primary')
749    def save(self, **data):
750        msave(self, **data)
751        return
752
753class StudentPersonalEditFormPage(StudentPersonalManageFormPage):
754    """ Page to edit personal data
755    """
756    grok.name('edit_personal')
757    grok.require('waeup.handleStudent')
758    label = _('Edit personal data')
759    pnav = 4
760
761class StudyCourseDisplayFormPage(KofaDisplayFormPage):
762    """ Page to display the student study course data
763    """
764    grok.context(IStudentStudyCourse)
765    grok.name('index')
766    grok.require('waeup.viewStudent')
767    grok.template('studycoursepage')
768    pnav = 4
769
770    @property
771    def form_fields(self):
772        if self.context.is_postgrad:
773            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
774                'current_verdict', 'previous_verdict')
775        else:
776            form_fields = grok.AutoFields(IStudentStudyCourse)
777        return form_fields
778
779    @property
780    def label(self):
781        if self.context.is_current:
782            return _('${a}: Study Course',
783                mapping = {'a':self.context.__parent__.display_fullname})
784        else:
785            return _('${a}: Previous Study Course',
786                mapping = {'a':self.context.__parent__.display_fullname})
787
788    @property
789    def current_mode(self):
790        if self.context.certificate is not None:
791            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
792            return studymodes_dict[self.context.certificate.study_mode]
793        return
794
795    @property
796    def department(self):
797        if self.context.certificate is not None:
798            return self.context.certificate.__parent__.__parent__
799        return
800
801    @property
802    def faculty(self):
803        if self.context.certificate is not None:
804            return self.context.certificate.__parent__.__parent__.__parent__
805        return
806
807    @property
808    def prev_studycourses(self):
809        if self.context.is_current:
810            if self.context.__parent__.get('studycourse_2', None) is not None:
811                return (
812                        {'href':self.url(self.context.student) + '/studycourse_1',
813                        'title':_('First Study Course, ')},
814                        {'href':self.url(self.context.student) + '/studycourse_2',
815                        'title':_('Second Study Course')}
816                        )
817            if self.context.__parent__.get('studycourse_1', None) is not None:
818                return (
819                        {'href':self.url(self.context.student) + '/studycourse_1',
820                        'title':_('First Study Course')},
821                        )
822        return
823
824class StudyCourseManageFormPage(KofaEditFormPage):
825    """ Page to edit the student study course data
826    """
827    grok.context(IStudentStudyCourse)
828    grok.name('manage')
829    grok.require('waeup.manageStudent')
830    grok.template('studycoursemanagepage')
831    label = _('Manage study course')
832    pnav = 4
833    taboneactions = [_('Save'),_('Cancel')]
834    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
835    tabthreeactions = [_('Add study level')]
836
837    @property
838    def form_fields(self):
839        if self.context.is_postgrad:
840            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
841                'current_verdict', 'previous_verdict')
842        else:
843            form_fields = grok.AutoFields(IStudentStudyCourse)
844        return form_fields
845
846    def update(self):
847        if not self.context.is_current:
848            emit_lock_message(self)
849            return
850        super(StudyCourseManageFormPage, self).update()
851        tabs.need()
852        self.tab1 = self.tab2 = ''
853        qs = self.request.get('QUERY_STRING', '')
854        if not qs:
855            qs = 'tab1'
856        setattr(self, qs, 'active')
857        warning.need()
858        datatable.need()
859        return
860
861    @action(_('Save'), style='primary')
862    def save(self, **data):
863        try:
864            msave(self, **data)
865        except ConstraintNotSatisfied:
866            # The selected level might not exist in certificate
867            self.flash(_('Current level not available for certificate.'))
868            return
869        notify(grok.ObjectModifiedEvent(self.context.__parent__))
870        return
871
872    @property
873    def level_dict(self):
874        studylevelsource = StudyLevelSource().factory
875        for code in studylevelsource.getValues(self.context):
876            title = studylevelsource.getTitle(self.context, code)
877            yield(dict(code=code, title=title))
878
879    @action(_('Add study level'))
880    def addStudyLevel(self, **data):
881        level_code = self.request.form.get('addlevel', None)
882        studylevel = createObject(u'waeup.StudentStudyLevel')
883        studylevel.level = int(level_code)
884        studylevel.level_session = self.context.current_session
885        try:
886            self.context.addStudentStudyLevel(
887                self.context.certificate,studylevel)
888            self.flash(_('Study level has been added.'))
889        except KeyError:
890            self.flash(_('This level exists.'))
891        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
892        return
893
894    @jsaction(_('Remove selected levels'))
895    def delStudyLevels(self, **data):
896        form = self.request.form
897        if form.has_key('val_id'):
898            child_id = form['val_id']
899        else:
900            self.flash(_('No study level selected.'))
901            self.redirect(self.url(self.context, '@@manage')+'?tab2')
902            return
903        if not isinstance(child_id, list):
904            child_id = [child_id]
905        deleted = []
906        for id in child_id:
907            del self.context[id]
908            deleted.append(id)
909        if len(deleted):
910            self.flash(_('Successfully removed: ${a}',
911                mapping = {'a':', '.join(deleted)}))
912            self.context.writeLogMessage(
913                self,'removed: %s' % ', '.join(deleted))
914        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
915        return
916
917class StudentTransferFormPage(KofaAddFormPage):
918    """Page to transfer the student.
919    """
920    grok.context(IStudent)
921    grok.name('transfer')
922    grok.require('waeup.manageStudent')
923    label = _('Transfer student')
924    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
925        'entry_mode', 'entry_session')
926    pnav = 4
927
928    def update(self):
929        super(StudentTransferFormPage, self).update()
930        warning.need()
931        return
932
933    @jsaction(_('Transfer'))
934    def transferStudent(self, **data):
935        error = self.context.transfer(**data)
936        if error == -1:
937            self.flash(_('Current level does not match certificate levels.'))
938        elif error == -2:
939            self.flash(_('Former study course record incomplete.'))
940        elif error == -3:
941            self.flash(_('Maximum number of transfers exceeded.'))
942        else:
943            self.flash(_('Successfully transferred.'))
944        return
945
946class StudyLevelDisplayFormPage(KofaDisplayFormPage):
947    """ Page to display student study levels
948    """
949    grok.context(IStudentStudyLevel)
950    grok.name('index')
951    grok.require('waeup.viewStudent')
952    form_fields = grok.AutoFields(IStudentStudyLevel)
953    form_fields[
954        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
955    grok.template('studylevelpage')
956    pnav = 4
957
958    def update(self):
959        super(StudyLevelDisplayFormPage, self).update()
960        datatable.need()
961        return
962
963    @property
964    def translated_values(self):
965        return translated_values(self)
966
967    @property
968    def label(self):
969        # Here we know that the cookie has been set
970        lang = self.request.cookies.get('kofa.language')
971        level_title = translate(self.context.level_title, 'waeup.kofa',
972            target_language=lang)
973        return _('${a}: Study Level ${b}', mapping = {
974            'a':self.context.student.display_fullname,
975            'b':level_title})
976
977    @property
978    def total_credits(self):
979        total_credits = 0
980        for key, val in self.context.items():
981            total_credits += val.credits
982        return total_credits
983
984class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
985    """Deliver a PDF slip of the context.
986    """
987    grok.context(IStudentStudyLevel)
988    grok.name('course_registration.pdf')
989    grok.require('waeup.viewStudent')
990    form_fields = grok.AutoFields(IStudentStudyLevel)
991    prefix = 'form'
992
993    @property
994    def title(self):
995        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
996        return translate(_('Level Data'), 'waeup.kofa',
997            target_language=portal_language)
998
999    @property
1000    def content_title(self):
1001        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1002        return translate(_('Course List'), 'waeup.kofa',
1003            target_language=portal_language)
1004
1005    @property
1006    def label(self):
1007        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1008        lang = self.request.cookies.get('kofa.language', portal_language)
1009        level_title = translate(self.context.level_title, 'waeup.kofa',
1010            target_language=lang)
1011        return translate(_('Course Registration Slip'),
1012            'waeup.kofa', target_language=portal_language) \
1013            + ' %s' % level_title
1014
1015    def render(self):
1016        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1017        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1018        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1019        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1020        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1021        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1022        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1023        Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1024        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1025        studentview = StudentBasePDFFormPage(self.context.student,
1026            self.request, ('password', 'suspended', 'phone', 'adm_code', 'sex'))
1027        students_utils = getUtility(IStudentsUtils)
1028        tabledata = sorted(self.context.values(),
1029            key=lambda value: str(value.semester) + value.code)
1030        return students_utils.renderPDF(
1031            self, 'course_registration.pdf',
1032            self.context.student, studentview,
1033            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
1034                         (Title,'title', 5),
1035                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1036                         (Cred, 'credits', 1.5),
1037                         (Mand, 'mandatory', 1.5),
1038                         (Score, 'score', 1.5),
1039                         #('Auto', 'automatic', 1.5)
1040                         ],
1041            tabledata=tabledata)
1042
1043class StudyLevelManageFormPage(KofaEditFormPage):
1044    """ Page to edit the student study level data
1045    """
1046    grok.context(IStudentStudyLevel)
1047    grok.name('manage')
1048    grok.require('waeup.manageStudent')
1049    grok.template('studylevelmanagepage')
1050    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1051        'validation_date', 'validated_by')
1052    pnav = 4
1053    taboneactions = [_('Save'),_('Cancel')]
1054    tabtwoactions = [_('Add course ticket'),
1055        _('Remove selected tickets'),_('Cancel')]
1056
1057    def update(self):
1058        if not self.context.__parent__.is_current:
1059            emit_lock_message(self)
1060            return
1061        super(StudyLevelManageFormPage, self).update()
1062        tabs.need()
1063        self.tab1 = self.tab2 = ''
1064        qs = self.request.get('QUERY_STRING', '')
1065        if not qs:
1066            qs = 'tab1'
1067        setattr(self, qs, 'active')
1068        warning.need()
1069        datatable.need()
1070        return
1071
1072    @property
1073    def translated_values(self):
1074        return translated_values(self)
1075
1076    @property
1077    def label(self):
1078        # Here we know that the cookie has been set
1079        lang = self.request.cookies.get('kofa.language')
1080        level_title = translate(self.context.level_title, 'waeup.kofa',
1081            target_language=lang)
1082        return _('Manage study level ${a}',
1083            mapping = {'a':level_title})
1084
1085    @action(_('Save'), style='primary')
1086    def save(self, **data):
1087        msave(self, **data)
1088        return
1089
1090    @action(_('Add course ticket'))
1091    def addCourseTicket(self, **data):
1092        self.redirect(self.url(self.context, '@@add'))
1093
1094    @jsaction(_('Remove selected tickets'))
1095    def delCourseTicket(self, **data):
1096        form = self.request.form
1097        if form.has_key('val_id'):
1098            child_id = form['val_id']
1099        else:
1100            self.flash(_('No ticket selected.'))
1101            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1102            return
1103        if not isinstance(child_id, list):
1104            child_id = [child_id]
1105        deleted = []
1106        for id in child_id:
1107            del self.context[id]
1108            deleted.append(id)
1109        if len(deleted):
1110            self.flash(_('Successfully removed: ${a}',
1111                mapping = {'a':', '.join(deleted)}))
1112            self.context.writeLogMessage(
1113                self,'removed: %s' % ', '.join(deleted))
1114        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1115        return
1116
1117class ValidateCoursesPage(UtilityView, grok.View):
1118    """ Validate course list by course adviser
1119    """
1120    grok.context(IStudentStudyLevel)
1121    grok.name('validate_courses')
1122    grok.require('waeup.validateStudent')
1123
1124    def update(self):
1125        if not self.context.__parent__.is_current:
1126            emit_lock_message(self)
1127            return
1128        if str(self.context.__parent__.current_level) != self.context.__name__:
1129            self.flash(_('This level does not correspond current level.'))
1130        elif self.context.student.state == REGISTERED:
1131            IWorkflowInfo(self.context.student).fireTransition(
1132                'validate_courses')
1133            self.flash(_('Course list has been validated.'))
1134        else:
1135            self.flash(_('Student is in the wrong state.'))
1136        self.redirect(self.url(self.context))
1137        return
1138
1139    def render(self):
1140        return
1141
1142class RejectCoursesPage(UtilityView, grok.View):
1143    """ Reject course list by course adviser
1144    """
1145    grok.context(IStudentStudyLevel)
1146    grok.name('reject_courses')
1147    grok.require('waeup.validateStudent')
1148
1149    def update(self):
1150        if not self.context.__parent__.is_current:
1151            emit_lock_message(self)
1152            return
1153        if str(self.context.__parent__.current_level) != self.context.__name__:
1154            self.flash(_('This level does not correspond current level.'))
1155            self.redirect(self.url(self.context))
1156            return
1157        elif self.context.student.state == VALIDATED:
1158            IWorkflowInfo(self.context.student).fireTransition('reset8')
1159            message = _('Course list request has been annulled.')
1160            self.flash(message)
1161        elif self.context.student.state == REGISTERED:
1162            IWorkflowInfo(self.context.student).fireTransition('reset7')
1163            message = _('Course list request has been rejected:')
1164            self.flash(message)
1165        else:
1166            self.flash(_('Student is in the wrong state.'))
1167            self.redirect(self.url(self.context))
1168            return
1169        args = {'subject':message}
1170        self.redirect(self.url(self.context.student) +
1171            '/contactstudent?%s' % urlencode(args))
1172        return
1173
1174    def render(self):
1175        return
1176
1177class CourseTicketAddFormPage(KofaAddFormPage):
1178    """Add a course ticket.
1179    """
1180    grok.context(IStudentStudyLevel)
1181    grok.name('add')
1182    grok.require('waeup.manageStudent')
1183    label = _('Add course ticket')
1184    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1185        'score', 'automatic', 'carry_over', 'credits', 'passmark')
1186    pnav = 4
1187
1188    def update(self):
1189        if not self.context.__parent__.is_current:
1190            emit_lock_message(self)
1191            return
1192        super(CourseTicketAddFormPage, self).update()
1193        return
1194
1195    @action(_('Add course ticket'))
1196    def addCourseTicket(self, **data):
1197        ticket = createObject(u'waeup.CourseTicket')
1198        course = data['course']
1199        ticket.automatic = False
1200        ticket.carry_over = False
1201        try:
1202            self.context.addCourseTicket(ticket, course)
1203        except KeyError:
1204            self.flash(_('The ticket exists.'))
1205            return
1206        self.flash(_('Successfully added ${a}.',
1207            mapping = {'a':ticket.code}))
1208        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1209        return
1210
1211    @action(_('Cancel'), validator=NullValidator)
1212    def cancel(self, **data):
1213        self.redirect(self.url(self.context))
1214
1215class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1216    """ Page to display course tickets
1217    """
1218    grok.context(ICourseTicket)
1219    grok.name('index')
1220    grok.require('waeup.viewStudent')
1221    form_fields = grok.AutoFields(ICourseTicket)
1222    grok.template('courseticketpage')
1223    pnav = 4
1224
1225    @property
1226    def label(self):
1227        return _('${a}: Course Ticket ${b}', mapping = {
1228            'a':self.context.student.display_fullname,
1229            'b':self.context.code})
1230
1231class CourseTicketManageFormPage(KofaEditFormPage):
1232    """ Page to manage course tickets
1233    """
1234    grok.context(ICourseTicket)
1235    grok.name('manage')
1236    grok.require('waeup.manageStudent')
1237    form_fields = grok.AutoFields(ICourseTicket).omit('credits', 'passmark')
1238    grok.template('courseticketmanagepage')
1239    pnav = 4
1240
1241    @property
1242    def label(self):
1243        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1244
1245    @action('Save', style='primary')
1246    def save(self, **data):
1247        msave(self, **data)
1248        return
1249
1250class PaymentsManageFormPage(KofaEditFormPage):
1251    """ Page to manage the student payments
1252
1253    This manage form page is for both students and students officers.
1254    """
1255    grok.context(IStudentPaymentsContainer)
1256    grok.name('index')
1257    grok.require('waeup.payStudent')
1258    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1259    grok.template('paymentsmanagepage')
1260    pnav = 4
1261
1262    def unremovable(self, ticket):
1263        usertype = getattr(self.request.principal, 'user_type', None)
1264        if not usertype:
1265            return False
1266        return (self.request.principal.user_type == 'student' and ticket.r_code)
1267
1268    @property
1269    def label(self):
1270        return _('${a}: Payments',
1271            mapping = {'a':self.context.__parent__.display_fullname})
1272
1273    def update(self):
1274        super(PaymentsManageFormPage, self).update()
1275        datatable.need()
1276        warning.need()
1277        return
1278
1279    @jsaction(_('Remove selected tickets'))
1280    def delPaymentTicket(self, **data):
1281        form = self.request.form
1282        if form.has_key('val_id'):
1283            child_id = form['val_id']
1284        else:
1285            self.flash(_('No payment selected.'))
1286            self.redirect(self.url(self.context))
1287            return
1288        if not isinstance(child_id, list):
1289            child_id = [child_id]
1290        deleted = []
1291        for id in child_id:
1292            # Students are not allowed to remove used payment tickets
1293            if not self.unremovable(self.context[id]):
1294                del self.context[id]
1295                deleted.append(id)
1296        if len(deleted):
1297            self.flash(_('Successfully removed: ${a}',
1298                mapping = {'a': ', '.join(deleted)}))
1299            self.context.writeLogMessage(
1300                self,'removed: %s' % ', '.join(deleted))
1301        self.redirect(self.url(self.context))
1302        return
1303
1304    @action(_('Add online payment ticket'))
1305    def addPaymentTicket(self, **data):
1306        self.redirect(self.url(self.context, '@@addop'))
1307
1308class OnlinePaymentAddFormPage(KofaAddFormPage):
1309    """ Page to add an online payment ticket
1310    """
1311    grok.context(IStudentPaymentsContainer)
1312    grok.name('addop')
1313    grok.require('waeup.payStudent')
1314    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1315        'p_category')
1316    label = _('Add online payment')
1317    pnav = 4
1318
1319    @action(_('Create ticket'), style='primary')
1320    def createTicket(self, **data):
1321        p_category = data['p_category']
1322        previous_session = data.get('p_session', None)
1323        previous_level = data.get('p_level', None)
1324        student = self.context.__parent__
1325        if p_category == 'bed_allocation' and student[
1326            'studycourse'].current_session != grok.getSite()[
1327            'hostels'].accommodation_session:
1328                self.flash(
1329                    _('Your current session does not match ' + \
1330                    'accommodation session.'))
1331                self.redirect(self.url(self.context))
1332                return
1333        students_utils = getUtility(IStudentsUtils)
1334        error, payment = students_utils.setPaymentDetails(
1335            p_category, student, previous_session, previous_level)
1336        if error is not None:
1337            self.flash(error)
1338            if 'previous session' in error:
1339                self.redirect(self.url(self.context) + '/@@addpp')
1340                return
1341            self.redirect(self.url(self.context))
1342            return
1343        self.context[payment.p_id] = payment
1344        self.flash(_('Payment ticket created.'))
1345        self.redirect(self.url(self.context))
1346        return
1347
1348class PreviousPaymentAddFormPage(OnlinePaymentAddFormPage):
1349    """ Page to add an online payment ticket for previous sessions
1350    """
1351    grok.context(IStudentPaymentsContainer)
1352    grok.name('addpp')
1353    grok.require('waeup.payStudent')
1354    form_fields = grok.AutoFields(IStudentPreviousPayment).select(
1355        'p_category', 'p_session', 'p_level')
1356    label = _('Add previous session online payment')
1357    pnav = 4
1358
1359class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1360    """ Page to view an online payment ticket
1361    """
1362    grok.context(IStudentOnlinePayment)
1363    grok.name('index')
1364    grok.require('waeup.viewStudent')
1365    form_fields = grok.AutoFields(IStudentOnlinePayment)
1366    form_fields[
1367        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1368    form_fields[
1369        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1370    pnav = 4
1371
1372    @property
1373    def label(self):
1374        return _('${a}: Online Payment Ticket ${b}', mapping = {
1375            'a':self.context.student.display_fullname,
1376            'b':self.context.p_id})
1377
1378class OnlinePaymentApprovePage(UtilityView, grok.View):
1379    """ Callback view
1380    """
1381    grok.context(IStudentOnlinePayment)
1382    grok.name('approve')
1383    grok.require('waeup.managePortal')
1384
1385    def update(self):
1386        success, msg, log = self.context.approveStudentPayment()
1387        if log is not None:
1388            self.context.writeLogMessage(self,log)
1389        self.flash(msg)
1390        return
1391
1392    def render(self):
1393        self.redirect(self.url(self.context, '@@index'))
1394        return
1395
1396class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1397    """ Approval view for students.
1398
1399    This view is used for browser tests only and
1400    must be neutralized in custom pages!
1401    """
1402
1403    grok.name('fake_approve')
1404    grok.require('waeup.payStudent')
1405
1406class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1407    """Deliver a PDF slip of the context.
1408    """
1409    grok.context(IStudentOnlinePayment)
1410    grok.name('payment_slip.pdf')
1411    grok.require('waeup.viewStudent')
1412    form_fields = grok.AutoFields(IStudentOnlinePayment)
1413    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1414    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1415    prefix = 'form'
1416    note = None
1417
1418    @property
1419    def title(self):
1420        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1421        return translate(_('Payment Data'), 'waeup.kofa',
1422            target_language=portal_language)
1423
1424    @property
1425    def label(self):
1426        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1427        return translate(_('Online Payment Slip'),
1428            'waeup.kofa', target_language=portal_language) \
1429            + ' %s' % self.context.p_id
1430
1431    def render(self):
1432        #if self.context.p_state != 'paid':
1433        #    self.flash('Ticket not yet paid.')
1434        #    self.redirect(self.url(self.context))
1435        #    return
1436        studentview = StudentBasePDFFormPage(self.context.student,
1437            self.request, ('password', 'suspended', 'phone', 'adm_code', 'sex'))
1438        students_utils = getUtility(IStudentsUtils)
1439        return students_utils.renderPDF(self, 'payment_slip.pdf',
1440            self.context.student, studentview, note=self.note)
1441
1442
1443class AccommodationManageFormPage(KofaEditFormPage):
1444    """ Page to manage bed tickets.
1445
1446    This manage form page is for both students and students officers.
1447    """
1448    grok.context(IStudentAccommodation)
1449    grok.name('index')
1450    grok.require('waeup.handleAccommodation')
1451    form_fields = grok.AutoFields(IStudentAccommodation)
1452    grok.template('accommodationmanagepage')
1453    pnav = 4
1454    officers_only_actions = [_('Remove selected')]
1455
1456    @property
1457    def label(self):
1458        return _('${a}: Accommodation',
1459            mapping = {'a':self.context.__parent__.display_fullname})
1460
1461    def update(self):
1462        super(AccommodationManageFormPage, self).update()
1463        datatable.need()
1464        warning.need()
1465        return
1466
1467    @jsaction(_('Remove selected'))
1468    def delBedTickets(self, **data):
1469        if getattr(self.request.principal, 'user_type', None) == 'student':
1470            self.flash(_('You are not allowed to remove bed tickets.'))
1471            self.redirect(self.url(self.context))
1472            return
1473        form = self.request.form
1474        if form.has_key('val_id'):
1475            child_id = form['val_id']
1476        else:
1477            self.flash(_('No bed ticket selected.'))
1478            self.redirect(self.url(self.context))
1479            return
1480        if not isinstance(child_id, list):
1481            child_id = [child_id]
1482        deleted = []
1483        for id in child_id:
1484            del self.context[id]
1485            deleted.append(id)
1486        if len(deleted):
1487            self.flash(_('Successfully removed: ${a}',
1488                mapping = {'a':', '.join(deleted)}))
1489            self.context.writeLogMessage(
1490                self,'removed: % s' % ', '.join(deleted))
1491        self.redirect(self.url(self.context))
1492        return
1493
1494    @property
1495    def selected_actions(self):
1496        if getattr(self.request.principal, 'user_type', None) == 'student':
1497            return [action for action in self.actions
1498                    if not action.label in self.officers_only_actions]
1499        return self.actions
1500
1501class BedTicketAddPage(KofaPage):
1502    """ Page to add an online payment ticket
1503    """
1504    grok.context(IStudentAccommodation)
1505    grok.name('add')
1506    grok.require('waeup.handleAccommodation')
1507    grok.template('enterpin')
1508    ac_prefix = 'HOS'
1509    label = _('Add bed ticket')
1510    pnav = 4
1511    buttonname = _('Create bed ticket')
1512    notice = ''
1513    with_ac = True
1514
1515    def update(self, SUBMIT=None):
1516        student = self.context.student
1517        students_utils = getUtility(IStudentsUtils)
1518        acc_details  = students_utils.getAccommodationDetails(student)
1519        if acc_details.get('expired', False):
1520            startdate = acc_details.get('startdate')
1521            enddate = acc_details.get('enddate')
1522            if startdate and enddate:
1523                tz = getUtility(IKofaUtils).tzinfo
1524                startdate = to_timezone(
1525                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1526                enddate = to_timezone(
1527                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1528                self.flash(_("Outside booking period: ${a} - ${b}",
1529                    mapping = {'a': startdate, 'b': enddate}))
1530            else:
1531                self.flash(_("Outside booking period."))
1532            self.redirect(self.url(self.context))
1533            return
1534        if not acc_details:
1535            self.flash(_("Your data are incomplete."))
1536            self.redirect(self.url(self.context))
1537            return
1538        if not student.state in acc_details['allowed_states']:
1539            self.flash(_("You are in the wrong registration state."))
1540            self.redirect(self.url(self.context))
1541            return
1542        if student['studycourse'].current_session != acc_details[
1543            'booking_session']:
1544            self.flash(
1545                _('Your current session does not match accommodation session.'))
1546            self.redirect(self.url(self.context))
1547            return
1548        if str(acc_details['booking_session']) in self.context.keys():
1549            self.flash(
1550                _('You already booked a bed space in current ' \
1551                    + 'accommodation session.'))
1552            self.redirect(self.url(self.context))
1553            return
1554        if self.with_ac:
1555            self.ac_series = self.request.form.get('ac_series', None)
1556            self.ac_number = self.request.form.get('ac_number', None)
1557        if SUBMIT is None:
1558            return
1559        if self.with_ac:
1560            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1561            code = get_access_code(pin)
1562            if not code:
1563                self.flash(_('Activation code is invalid.'))
1564                return
1565        # Search and book bed
1566        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1567        entries = cat.searchResults(
1568            owner=(student.student_id,student.student_id))
1569        if len(entries):
1570            # If bed space has been manually allocated use this bed
1571            bed = [entry for entry in entries][0]
1572        else:
1573            # else search for other available beds
1574            entries = cat.searchResults(
1575                bed_type=(acc_details['bt'],acc_details['bt']))
1576            available_beds = [
1577                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1578            if available_beds:
1579                students_utils = getUtility(IStudentsUtils)
1580                bed = students_utils.selectBed(available_beds)
1581                bed.bookBed(student.student_id)
1582            else:
1583                self.flash(_('There is no free bed in your category ${a}.',
1584                    mapping = {'a':acc_details['bt']}))
1585                return
1586        if self.with_ac:
1587            # Mark pin as used (this also fires a pin related transition)
1588            if code.state == USED:
1589                self.flash(_('Activation code has already been used.'))
1590                return
1591            else:
1592                comment = _(u'invalidated')
1593                # Here we know that the ac is in state initialized so we do not
1594                # expect an exception, but the owner might be different
1595                if not invalidate_accesscode(
1596                    pin,comment,self.context.student.student_id):
1597                    self.flash(_('You are not the owner of this access code.'))
1598                    return
1599        # Create bed ticket
1600        bedticket = createObject(u'waeup.BedTicket')
1601        if self.with_ac:
1602            bedticket.booking_code = pin
1603        bedticket.booking_session = acc_details['booking_session']
1604        bedticket.bed_type = acc_details['bt']
1605        bedticket.bed = bed
1606        hall_title = bed.__parent__.hostel_name
1607        coordinates = bed.coordinates[1:]
1608        block, room_nr, bed_nr = coordinates
1609        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1610            'a':hall_title, 'b':block,
1611            'c':room_nr, 'd':bed_nr,
1612            'e':bed.bed_type})
1613        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1614        bedticket.bed_coordinates = translate(
1615            bc, 'waeup.kofa',target_language=portal_language)
1616        key = str(acc_details['booking_session'])
1617        self.context[key] = bedticket
1618        self.flash(_('Bed ticket created and bed booked: ${a}',
1619            mapping = {'a':bedticket.bed_coordinates}))
1620        self.redirect(self.url(self.context))
1621        return
1622
1623class BedTicketDisplayFormPage(KofaDisplayFormPage):
1624    """ Page to display bed tickets
1625    """
1626    grok.context(IBedTicket)
1627    grok.name('index')
1628    grok.require('waeup.handleAccommodation')
1629    form_fields = grok.AutoFields(IBedTicket)
1630    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1631    pnav = 4
1632
1633    @property
1634    def label(self):
1635        return _('Bed Ticket for Session ${a}',
1636            mapping = {'a':self.context.getSessionString()})
1637
1638class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1639    """Deliver a PDF slip of the context.
1640    """
1641    grok.context(IBedTicket)
1642    grok.name('bed_allocation.pdf')
1643    grok.require('waeup.handleAccommodation')
1644    form_fields = grok.AutoFields(IBedTicket)
1645    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1646    prefix = 'form'
1647
1648    @property
1649    def title(self):
1650        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1651        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1652            target_language=portal_language)
1653
1654    @property
1655    def label(self):
1656        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1657        #return translate(_('Bed Allocation: '),
1658        #    'waeup.kofa', target_language=portal_language) \
1659        #    + ' %s' % self.context.bed_coordinates
1660        return translate(_('Bed Allocation Slip'),
1661            'waeup.kofa', target_language=portal_language) \
1662            + ' %s' % self.context.getSessionString()
1663
1664    def render(self):
1665        studentview = StudentBasePDFFormPage(self.context.student,
1666            self.request, ('password', 'suspended', 'phone', 'adm_code', 'sex'))
1667        students_utils = getUtility(IStudentsUtils)
1668        return students_utils.renderPDF(
1669            self, 'bed_allocation.pdf',
1670            self.context.student, studentview)
1671
1672class BedTicketRelocationPage(UtilityView, grok.View):
1673    """ Callback view
1674    """
1675    grok.context(IBedTicket)
1676    grok.name('relocate')
1677    grok.require('waeup.manageHostels')
1678
1679    # Relocate student if student parameters have changed or the bed_type
1680    # of the bed has changed
1681    def update(self):
1682        student = self.context.student
1683        students_utils = getUtility(IStudentsUtils)
1684        acc_details  = students_utils.getAccommodationDetails(student)
1685        if self.context.bed != None and \
1686              'reserved' in self.context.bed.bed_type:
1687            self.flash(_("Students in reserved beds can't be relocated."))
1688            self.redirect(self.url(self.context))
1689            return
1690        if acc_details['bt'] == self.context.bed_type and \
1691                self.context.bed != None and \
1692                self.context.bed.bed_type == self.context.bed_type:
1693            self.flash(_("Student can't be relocated."))
1694            self.redirect(self.url(self.context))
1695            return
1696        # Search a bed
1697        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1698        entries = cat.searchResults(
1699            owner=(student.student_id,student.student_id))
1700        if len(entries) and self.context.bed == None:
1701            # If booking has been cancelled but other bed space has been
1702            # manually allocated after cancellation use this bed
1703            new_bed = [entry for entry in entries][0]
1704        else:
1705            # Search for other available beds
1706            entries = cat.searchResults(
1707                bed_type=(acc_details['bt'],acc_details['bt']))
1708            available_beds = [
1709                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1710            if available_beds:
1711                students_utils = getUtility(IStudentsUtils)
1712                new_bed = students_utils.selectBed(available_beds)
1713                new_bed.bookBed(student.student_id)
1714            else:
1715                self.flash(_('There is no free bed in your category ${a}.',
1716                    mapping = {'a':acc_details['bt']}))
1717                self.redirect(self.url(self.context))
1718                return
1719        # Release old bed if exists
1720        if self.context.bed != None:
1721            self.context.bed.owner = NOT_OCCUPIED
1722            notify(grok.ObjectModifiedEvent(self.context.bed))
1723        # Alocate new bed
1724        self.context.bed_type = acc_details['bt']
1725        self.context.bed = new_bed
1726        hall_title = new_bed.__parent__.hostel_name
1727        coordinates = new_bed.coordinates[1:]
1728        block, room_nr, bed_nr = coordinates
1729        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1730            'a':hall_title, 'b':block,
1731            'c':room_nr, 'd':bed_nr,
1732            'e':new_bed.bed_type})
1733        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1734        self.context.bed_coordinates = translate(
1735            bc, 'waeup.kofa',target_language=portal_language)
1736        self.flash(_('Student relocated: ${a}',
1737            mapping = {'a':self.context.bed_coordinates}))
1738        self.redirect(self.url(self.context))
1739        return
1740
1741    def render(self):
1742        return
1743
1744class StudentHistoryPage(KofaPage):
1745    """ Page to display student clearance data
1746    """
1747    grok.context(IStudent)
1748    grok.name('history')
1749    grok.require('waeup.viewStudent')
1750    grok.template('studenthistory')
1751    pnav = 4
1752
1753    @property
1754    def label(self):
1755        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1756
1757# Pages for students only
1758
1759class StudentBaseEditFormPage(KofaEditFormPage):
1760    """ View to edit student base data
1761    """
1762    grok.context(IStudent)
1763    grok.name('edit_base')
1764    grok.require('waeup.handleStudent')
1765    form_fields = grok.AutoFields(IStudentBase).select(
1766        'email', 'phone')
1767    label = _('Edit base data')
1768    pnav = 4
1769
1770    @action(_('Save'), style='primary')
1771    def save(self, **data):
1772        msave(self, **data)
1773        return
1774
1775class StudentChangePasswordPage(KofaEditFormPage):
1776    """ View to manage student base data
1777    """
1778    grok.context(IStudent)
1779    grok.name('change_password')
1780    grok.require('waeup.handleStudent')
1781    grok.template('change_password')
1782    label = _('Change password')
1783    pnav = 4
1784
1785    @action(_('Save'), style='primary')
1786    def save(self, **data):
1787        form = self.request.form
1788        password = form.get('change_password', None)
1789        password_ctl = form.get('change_password_repeat', None)
1790        if password:
1791            validator = getUtility(IPasswordValidator)
1792            errors = validator.validate_password(password, password_ctl)
1793            if not errors:
1794                IUserAccount(self.context).setPassword(password)
1795                self.context.writeLogMessage(self, 'saved: password')
1796                self.flash(_('Password changed.'))
1797            else:
1798                self.flash( ' '.join(errors))
1799        return
1800
1801class StudentFilesUploadPage(KofaPage):
1802    """ View to upload files by student
1803    """
1804    grok.context(IStudent)
1805    grok.name('change_portrait')
1806    grok.require('waeup.uploadStudentFile')
1807    grok.template('filesuploadpage')
1808    label = _('Upload portrait')
1809    pnav = 4
1810
1811    def update(self):
1812        if self.context.student.state != ADMITTED:
1813            emit_lock_message(self)
1814            return
1815        super(StudentFilesUploadPage, self).update()
1816        return
1817
1818class StartClearancePage(KofaPage):
1819    grok.context(IStudent)
1820    grok.name('start_clearance')
1821    grok.require('waeup.handleStudent')
1822    grok.template('enterpin')
1823    label = _('Start clearance')
1824    ac_prefix = 'CLR'
1825    notice = ''
1826    pnav = 4
1827    buttonname = _('Start clearance now')
1828
1829    @property
1830    def all_required_fields_filled(self):
1831        if self.context.email and self.context.phone:
1832            return True
1833        return False
1834
1835    @property
1836    def portrait_uploaded(self):
1837        store = getUtility(IExtFileStore)
1838        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1839            return True
1840        return False
1841
1842    def update(self, SUBMIT=None):
1843        if not self.context.state == ADMITTED:
1844            self.flash(_("Wrong state"))
1845            self.redirect(self.url(self.context))
1846            return
1847        if not self.portrait_uploaded:
1848            self.flash(_("No portrait uploaded."))
1849            self.redirect(self.url(self.context, 'change_portrait'))
1850            return
1851        if not self.all_required_fields_filled:
1852            self.flash(_("Not all required fields filled."))
1853            self.redirect(self.url(self.context, 'edit_base'))
1854            return
1855        self.ac_series = self.request.form.get('ac_series', None)
1856        self.ac_number = self.request.form.get('ac_number', None)
1857
1858        if SUBMIT is None:
1859            return
1860        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1861        code = get_access_code(pin)
1862        if not code:
1863            self.flash(_('Activation code is invalid.'))
1864            return
1865        if code.state == USED:
1866            self.flash(_('Activation code has already been used.'))
1867            return
1868        # Mark pin as used (this also fires a pin related transition)
1869        # and fire transition start_clearance
1870        comment = _(u"invalidated")
1871        # Here we know that the ac is in state initialized so we do not
1872        # expect an exception, but the owner might be different
1873        if not invalidate_accesscode(pin, comment, self.context.student_id):
1874            self.flash(_('You are not the owner of this access code.'))
1875            return
1876        self.context.clr_code = pin
1877        IWorkflowInfo(self.context).fireTransition('start_clearance')
1878        self.flash(_('Clearance process has been started.'))
1879        self.redirect(self.url(self.context,'cedit'))
1880        return
1881
1882class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1883    """ View to edit student clearance data by student
1884    """
1885    grok.context(IStudent)
1886    grok.name('cedit')
1887    grok.require('waeup.handleStudent')
1888    label = _('Edit clearance data')
1889
1890    @property
1891    def form_fields(self):
1892        if self.context.is_postgrad:
1893            form_fields = grok.AutoFields(IPGStudentClearance).omit(
1894                'clearance_locked', 'clr_code')
1895        else:
1896            form_fields = grok.AutoFields(IUGStudentClearance).omit(
1897                'clearance_locked', 'clr_code')
1898        return form_fields
1899
1900    def update(self):
1901        if self.context.clearance_locked:
1902            emit_lock_message(self)
1903            return
1904        return super(StudentClearanceEditFormPage, self).update()
1905
1906    @action(_('Save'), style='primary')
1907    def save(self, **data):
1908        self.applyData(self.context, **data)
1909        self.flash(_('Clearance form has been saved.'))
1910        return
1911
1912    def dataNotComplete(self):
1913        """To be implemented in the customization package.
1914        """
1915        return False
1916
1917    @action(_('Save and request clearance'), style='primary')
1918    def requestClearance(self, **data):
1919        self.applyData(self.context, **data)
1920        if self.dataNotComplete():
1921            self.flash(self.dataNotComplete())
1922            return
1923        self.flash(_('Clearance form has been saved.'))
1924        if self.context.clr_code:
1925            self.redirect(self.url(self.context, 'request_clearance'))
1926        else:
1927            # We bypass the request_clearance page if student
1928            # has been imported in state 'clearance started' and
1929            # no clr_code was entered before.
1930            state = IWorkflowState(self.context).getState()
1931            if state != CLEARANCE:
1932                # This shouldn't happen, but the application officer
1933                # might have forgotten to lock the form after changing the state
1934                self.flash(_('This form cannot be submitted. Wrong state!'))
1935                return
1936            IWorkflowInfo(self.context).fireTransition('request_clearance')
1937            self.flash(_('Clearance has been requested.'))
1938            self.redirect(self.url(self.context))
1939        return
1940
1941class RequestClearancePage(KofaPage):
1942    grok.context(IStudent)
1943    grok.name('request_clearance')
1944    grok.require('waeup.handleStudent')
1945    grok.template('enterpin')
1946    label = _('Request clearance')
1947    notice = _('Enter the CLR access code used for starting clearance.')
1948    ac_prefix = 'CLR'
1949    pnav = 4
1950    buttonname = _('Request clearance now')
1951
1952    def update(self, SUBMIT=None):
1953        self.ac_series = self.request.form.get('ac_series', None)
1954        self.ac_number = self.request.form.get('ac_number', None)
1955        if SUBMIT is None:
1956            return
1957        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1958        if self.context.clr_code and self.context.clr_code != pin:
1959            self.flash(_("This isn't your CLR access code."))
1960            return
1961        state = IWorkflowState(self.context).getState()
1962        if state != CLEARANCE:
1963            # This shouldn't happen, but the application officer
1964            # might have forgotten to lock the form after changing the state
1965            self.flash(_('This form cannot be submitted. Wrong state!'))
1966            return
1967        IWorkflowInfo(self.context).fireTransition('request_clearance')
1968        self.flash(_('Clearance has been requested.'))
1969        self.redirect(self.url(self.context))
1970        return
1971
1972class StartSessionPage(KofaPage):
1973    grok.context(IStudentStudyCourse)
1974    grok.name('start_session')
1975    grok.require('waeup.handleStudent')
1976    grok.template('enterpin')
1977    label = _('Start session')
1978    ac_prefix = 'SFE'
1979    notice = ''
1980    pnav = 4
1981    buttonname = _('Start now')
1982
1983    def update(self, SUBMIT=None):
1984        if not self.context.is_current:
1985            emit_lock_message(self)
1986            return
1987        super(StartSessionPage, self).update()
1988        if not self.context.next_session_allowed:
1989            self.flash(_("You are not entitled to start session."))
1990            self.redirect(self.url(self.context))
1991            return
1992        self.ac_series = self.request.form.get('ac_series', None)
1993        self.ac_number = self.request.form.get('ac_number', None)
1994
1995        if SUBMIT is None:
1996            return
1997        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1998        code = get_access_code(pin)
1999        if not code:
2000            self.flash(_('Activation code is invalid.'))
2001            return
2002        # Mark pin as used (this also fires a pin related transition)
2003        if code.state == USED:
2004            self.flash(_('Activation code has already been used.'))
2005            return
2006        else:
2007            comment = _(u"invalidated")
2008            # Here we know that the ac is in state initialized so we do not
2009            # expect an error, but the owner might be different
2010            if not invalidate_accesscode(
2011                pin,comment,self.context.student.student_id):
2012                self.flash(_('You are not the owner of this access code.'))
2013                return
2014        if self.context.student.state == CLEARED:
2015            IWorkflowInfo(self.context.student).fireTransition(
2016                'pay_first_school_fee')
2017        elif self.context.student.state == RETURNING:
2018            IWorkflowInfo(self.context.student).fireTransition(
2019                'pay_school_fee')
2020        elif self.context.student.state == PAID:
2021            IWorkflowInfo(self.context.student).fireTransition(
2022                'pay_pg_fee')
2023        self.flash(_('Session started.'))
2024        self.redirect(self.url(self.context))
2025        return
2026
2027class AddStudyLevelFormPage(KofaEditFormPage):
2028    """ Page for students to add current study levels
2029    """
2030    grok.context(IStudentStudyCourse)
2031    grok.name('add')
2032    grok.require('waeup.handleStudent')
2033    grok.template('studyleveladdpage')
2034    form_fields = grok.AutoFields(IStudentStudyCourse)
2035    pnav = 4
2036
2037    @property
2038    def label(self):
2039        studylevelsource = StudyLevelSource().factory
2040        code = self.context.current_level
2041        title = studylevelsource.getTitle(self.context, code)
2042        return _('Add current level ${a}', mapping = {'a':title})
2043
2044    def update(self):
2045        if not self.context.is_current:
2046            emit_lock_message(self)
2047            return
2048        if self.context.student.state != PAID:
2049            emit_lock_message(self)
2050            return
2051        super(AddStudyLevelFormPage, self).update()
2052        return
2053
2054    @action(_('Create course list now'), style='primary')
2055    def addStudyLevel(self, **data):
2056        studylevel = createObject(u'waeup.StudentStudyLevel')
2057        studylevel.level = self.context.current_level
2058        studylevel.level_session = self.context.current_session
2059        try:
2060            self.context.addStudentStudyLevel(
2061                self.context.certificate,studylevel)
2062        except KeyError:
2063            self.flash(_('This level exists.'))
2064        self.redirect(self.url(self.context))
2065        return
2066
2067class StudyLevelEditFormPage(KofaEditFormPage):
2068    """ Page to edit the student study level data by students
2069    """
2070    grok.context(IStudentStudyLevel)
2071    grok.name('edit')
2072    grok.require('waeup.handleStudent')
2073    grok.template('studyleveleditpage')
2074    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2075        'level_session', 'level_verdict')
2076    pnav = 4
2077    max_credits = 50
2078
2079    def update(self):
2080        if not self.context.__parent__.is_current:
2081            emit_lock_message(self)
2082            return
2083        if self.context.student.state != PAID or \
2084            not self.context.is_current_level:
2085            emit_lock_message(self)
2086            return
2087        super(StudyLevelEditFormPage, self).update()
2088        datatable.need()
2089        warning.need()
2090        return
2091
2092    @property
2093    def label(self):
2094        # Here we know that the cookie has been set
2095        lang = self.request.cookies.get('kofa.language')
2096        level_title = translate(self.context.level_title, 'waeup.kofa',
2097            target_language=lang)
2098        return _('Edit course list of ${a}',
2099            mapping = {'a':level_title})
2100
2101    @property
2102    def total_credits(self):
2103        total_credits = 0
2104        for key, val in self.context.items():
2105            total_credits += val.credits
2106        return total_credits
2107
2108    @property
2109    def translated_values(self):
2110        return translated_values(self)
2111
2112    @action(_('Add course ticket'))
2113    def addCourseTicket(self, **data):
2114        self.redirect(self.url(self.context, 'ctadd'))
2115
2116    def _delCourseTicket(self, **data):
2117        form = self.request.form
2118        if form.has_key('val_id'):
2119            child_id = form['val_id']
2120        else:
2121            self.flash(_('No ticket selected.'))
2122            self.redirect(self.url(self.context, '@@edit'))
2123            return
2124        if not isinstance(child_id, list):
2125            child_id = [child_id]
2126        deleted = []
2127        for id in child_id:
2128            # Students are not allowed to remove core tickets
2129            if not self.context[id].mandatory:
2130                del self.context[id]
2131                deleted.append(id)
2132        if len(deleted):
2133            self.flash(_('Successfully removed: ${a}',
2134                mapping = {'a':', '.join(deleted)}))
2135            self.context.writeLogMessage(
2136                self,'removed: %s' % ', '.join(deleted))
2137        self.redirect(self.url(self.context, u'@@edit'))
2138        return
2139
2140    @jsaction(_('Remove selected tickets'))
2141    def delCourseTicket(self, **data):
2142        self._delCourseTicket(**data)
2143        return
2144
2145    def _registerCourses(self, **data):
2146        if self.context.student.is_postgrad:
2147            self.flash(_(
2148                "You are a postgraduate student, "
2149                "your course list can't bee registered."))
2150            self.redirect(self.url(self.context))
2151            return
2152        if self.total_credits > self.max_credits:
2153            self.flash(_('Maximum credits of ${a} exceeded.',
2154                mapping = {'a':self.max_credits}))
2155            return
2156        IWorkflowInfo(self.context.student).fireTransition(
2157            'register_courses')
2158        self.flash(_('Course list has been registered.'))
2159        self.redirect(self.url(self.context))
2160        return
2161
2162    @action(_('Register course list'), style='primary')
2163    def registerCourses(self, **data):
2164        self._registerCourses(**data)
2165        return
2166
2167
2168class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2169    """Add a course ticket by student.
2170    """
2171    grok.name('ctadd')
2172    grok.require('waeup.handleStudent')
2173    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
2174        'score', 'mandatory', 'automatic', 'carry_over', 'credits', 'passmark')
2175
2176    def update(self):
2177        if self.context.student.state != PAID or \
2178            not self.context.is_current_level:
2179            emit_lock_message(self)
2180            return
2181        super(CourseTicketAddFormPage2, self).update()
2182        return
2183
2184    @action(_('Add course ticket'))
2185    def addCourseTicket(self, **data):
2186        # Safety belt
2187        if self.context.student.state != PAID:
2188            return
2189        ticket = createObject(u'waeup.CourseTicket')
2190        course = data['course']
2191        ticket.automatic = False
2192        ticket.carry_over = False
2193        try:
2194            self.context.addCourseTicket(ticket, course)
2195        except KeyError:
2196            self.flash(_('The ticket exists.'))
2197            return
2198        self.flash(_('Successfully added ${a}.',
2199            mapping = {'a':ticket.code}))
2200        self.redirect(self.url(self.context, u'@@edit'))
2201        return
2202
2203
2204class SetPasswordPage(KofaPage):
2205    grok.context(IKofaObject)
2206    grok.name('setpassword')
2207    grok.require('waeup.Anonymous')
2208    grok.template('setpassword')
2209    label = _('Set password for first-time login')
2210    ac_prefix = 'PWD'
2211    pnav = 0
2212    set_button = _('Set')
2213
2214    def update(self, SUBMIT=None):
2215        self.reg_number = self.request.form.get('reg_number', None)
2216        self.ac_series = self.request.form.get('ac_series', None)
2217        self.ac_number = self.request.form.get('ac_number', None)
2218
2219        if SUBMIT is None:
2220            return
2221        hitlist = search(query=self.reg_number,
2222            searchtype='reg_number', view=self)
2223        if not hitlist:
2224            self.flash(_('No student found.'))
2225            return
2226        if len(hitlist) != 1:   # Cannot happen but anyway
2227            self.flash(_('More than one student found.'))
2228            return
2229        student = hitlist[0].context
2230        self.student_id = student.student_id
2231        student_pw = student.password
2232        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2233        code = get_access_code(pin)
2234        if not code:
2235            self.flash(_('Access code is invalid.'))
2236            return
2237        if student_pw and pin == student.adm_code:
2238            self.flash(_(
2239                'Password has already been set. Your Student Id is ${a}',
2240                mapping = {'a':self.student_id}))
2241            return
2242        elif student_pw:
2243            self.flash(
2244                _('Password has already been set. You are using the ' +
2245                'wrong Access Code.'))
2246            return
2247        # Mark pin as used (this also fires a pin related transition)
2248        # and set student password
2249        if code.state == USED:
2250            self.flash(_('Access code has already been used.'))
2251            return
2252        else:
2253            comment = _(u"invalidated")
2254            # Here we know that the ac is in state initialized so we do not
2255            # expect an exception
2256            invalidate_accesscode(pin,comment)
2257            IUserAccount(student).setPassword(self.ac_number)
2258            student.adm_code = pin
2259        self.flash(_('Password has been set. Your Student Id is ${a}',
2260            mapping = {'a':self.student_id}))
2261        return
2262
2263class StudentRequestPasswordPage(KofaAddFormPage):
2264    """Captcha'd registration page for applicants.
2265    """
2266    grok.name('requestpw')
2267    grok.require('waeup.Anonymous')
2268    grok.template('requestpw')
2269    form_fields = grok.AutoFields(IStudentRequestPW).select(
2270        'firstname','number','email')
2271    label = _('Request password for first-time login')
2272
2273    def update(self):
2274        # Handle captcha
2275        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2276        self.captcha_result = self.captcha.verify(self.request)
2277        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2278        return
2279
2280    def _redirect(self, email, password, student_id):
2281        # Forward only email to landing page in base package.
2282        self.redirect(self.url(self.context, 'requestpw_complete',
2283            data = dict(email=email)))
2284        return
2285
2286    def _pw_used(self):
2287        # XXX: False if password has not been used. We need an extra
2288        #      attribute which remembers if student logged in.
2289        return True
2290
2291    @action(_('Send login credentials to email address'), style='primary')
2292    def get_credentials(self, **data):
2293        if not self.captcha_result.is_valid:
2294            # Captcha will display error messages automatically.
2295            # No need to flash something.
2296            return
2297        number = data.get('number','')
2298        firstname = data.get('firstname','')
2299        cat = getUtility(ICatalog, name='students_catalog')
2300        results = list(
2301            cat.searchResults(reg_number=(number, number)))
2302        if not results:
2303            results = list(
2304                cat.searchResults(matric_number=(number, number)))
2305        if results:
2306            student = results[0]
2307            if getattr(student,'firstname',None) is None:
2308                self.flash(_('An error occurred.'))
2309                return
2310            elif student.firstname.lower() != firstname.lower():
2311                # Don't tell the truth here. Anonymous must not
2312                # know that a record was found and only the firstname
2313                # verification failed.
2314                self.flash(_('No student record found.'))
2315                return
2316            elif student.password is not None and self._pw_used:
2317                self.flash(_('Your password has already been set and used. '
2318                             'Please proceed to the login page.'))
2319                return
2320            # Store email address but nothing else.
2321            student.email = data['email']
2322            notify(grok.ObjectModifiedEvent(student))
2323        else:
2324            # No record found, this is the truth.
2325            self.flash(_('No student record found.'))
2326            return
2327
2328        kofa_utils = getUtility(IKofaUtils)
2329        password = kofa_utils.genPassword()
2330        mandate = PasswordMandate()
2331        mandate.params['password'] = password
2332        mandate.params['user'] = student
2333        site = grok.getSite()
2334        site['mandates'].addMandate(mandate)
2335        # Send email with credentials
2336        args = {'mandate_id':mandate.mandate_id}
2337        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2338        url_info = u'Confirmation link: %s' % mandate_url
2339        msg = _('You have successfully requested a password for the')
2340        if kofa_utils.sendCredentials(IUserAccount(student),
2341            password, url_info, msg):
2342            email_sent = student.email
2343        else:
2344            email_sent = None
2345        self._redirect(email=email_sent, password=password,
2346            student_id=student.student_id)
2347        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2348        self.context.logger.info(
2349            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2350        return
2351
2352class StudentRequestPasswordEmailSent(KofaPage):
2353    """Landing page after successful password request.
2354
2355    """
2356    grok.name('requestpw_complete')
2357    grok.require('waeup.Public')
2358    grok.template('requestpwmailsent')
2359    label = _('Your password request was successful.')
2360
2361    def update(self, email=None, student_id=None, password=None):
2362        self.email = email
2363        self.password = password
2364        self.student_id = student_id
2365        return
Note: See TracBrowser for help on using the repository browser.