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

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

Let students and officers add course tickets by entering the course code on the StudyLevelEditFormPage? or StudyLevelManageFormPage? respectively. The course ticket add forms can still be used in case course code is unknown.

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