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

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

Use new attrs_to_fields function to display gpa and total_credits on level pages.

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