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

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

Beautify term column.

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