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

Last change on this file since 9801 was 9797, checked in by uli, 12 years ago

Local exports for departments.

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