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

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

Change permissions so that only student managers can add balance payment tickets.

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