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

Last change on this file since 10262 was 10262, checked in by Henrik Bettermann, 11 years ago

Add signature box to footer.

Rename 'Final GPA'.

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