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

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

Disable score editing on department manage page.

Add tests.

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