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

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

Split course result table. Reformat tables.

  • Property svn:keywords set to Id
File size: 105.5 KB
Line 
1## $Id: browser.py 9906 2013-01-23 09:27:25Z 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_1(self):
1122        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1123        return translate(_('1st Semester Courses'), 'waeup.kofa',
1124            target_language=portal_language)
1125
1126    @property
1127    def content_title_2(self):
1128        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1129        return translate(_('2nd Semester Courses'), 'waeup.kofa',
1130            target_language=portal_language)
1131
1132    @property
1133    def label(self):
1134        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1135        lang = self.request.cookies.get('kofa.language', portal_language)
1136        level_title = translate(self.context.level_title, 'waeup.kofa',
1137            target_language=lang)
1138        return translate(_('Course Registration Slip'),
1139            'waeup.kofa', target_language=portal_language) \
1140            + ' %s' % level_title
1141
1142    def render(self):
1143        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1144        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1145        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1146        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1147        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1148        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1149        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1150        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1151        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1152        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1153        studentview = StudentBasePDFFormPage(self.context.student,
1154            self.request, self.omit_fields)
1155        students_utils = getUtility(IStudentsUtils)
1156        tabledata_1 = sorted(
1157            [value for value in self.context.values() if value.semester == 1],
1158            key=lambda value: str(value.semester) + value.code)
1159        tabledata_2 = sorted(
1160            [value for value in self.context.values() if value.semester == 2],
1161            key=lambda value: str(value.semester) + value.code)
1162        tableheader = [(Code,'code', 2.5),
1163                         (Title,'title', 5),
1164                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1165                         (Cred, 'credits', 1.5),
1166                         #(Mand, 'mandatory', 1.5),
1167                         (Score, 'score', 1.5),
1168                         (Grade, 'grade', 1.5),
1169                         #('Auto', 'automatic', 1.5)
1170                         ]
1171        return students_utils.renderPDF(
1172            self, 'course_registration_slip.pdf',
1173            self.context.student, studentview,
1174            tableheader_1=tableheader,
1175            tabledata_1=tabledata_1,
1176            tableheader_2=tableheader,
1177            tabledata_2=tabledata_2
1178            )
1179
1180class StudyLevelManageFormPage(KofaEditFormPage):
1181    """ Page to edit the student study level data
1182    """
1183    grok.context(IStudentStudyLevel)
1184    grok.name('manage')
1185    grok.require('waeup.manageStudent')
1186    grok.template('studylevelmanagepage')
1187    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1188        'validation_date', 'validated_by', 'total_credits', 'gpa')
1189    pnav = 4
1190    taboneactions = [_('Save'),_('Cancel')]
1191    tabtwoactions = [_('Add course ticket'),
1192        _('Remove selected tickets'),_('Cancel')]
1193
1194    def update(self, ADD=None, course=None):
1195        if not self.context.__parent__.is_current:
1196            emit_lock_message(self)
1197            return
1198        super(StudyLevelManageFormPage, self).update()
1199        tabs.need()
1200        self.tab1 = self.tab2 = ''
1201        qs = self.request.get('QUERY_STRING', '')
1202        if not qs:
1203            qs = 'tab1'
1204        setattr(self, qs, 'active')
1205        warning.need()
1206        datatable.need()
1207        if ADD is not None:
1208            if not course:
1209                self.flash(_('No valid course code entered.'))
1210                self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1211                return
1212            cat = queryUtility(ICatalog, name='courses_catalog')
1213            result = cat.searchResults(code=(course, course))
1214            if len(result) != 1:
1215                self.flash(_('Course not found.'))
1216            else:
1217                course = list(result)[0]
1218                addCourseTicket(self, course)
1219            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1220        return
1221
1222    @property
1223    def translated_values(self):
1224        return translated_values(self)
1225
1226    @property
1227    def label(self):
1228        # Here we know that the cookie has been set
1229        lang = self.request.cookies.get('kofa.language')
1230        level_title = translate(self.context.level_title, 'waeup.kofa',
1231            target_language=lang)
1232        return _('Manage study level ${a}',
1233            mapping = {'a':level_title})
1234
1235    @action(_('Save'), style='primary')
1236    def save(self, **data):
1237        msave(self, **data)
1238        return
1239
1240    @jsaction(_('Remove selected tickets'))
1241    def delCourseTicket(self, **data):
1242        form = self.request.form
1243        if 'val_id' in form:
1244            child_id = form['val_id']
1245        else:
1246            self.flash(_('No ticket selected.'))
1247            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1248            return
1249        if not isinstance(child_id, list):
1250            child_id = [child_id]
1251        deleted = []
1252        for id in child_id:
1253            del self.context[id]
1254            deleted.append(id)
1255        if len(deleted):
1256            self.flash(_('Successfully removed: ${a}',
1257                mapping = {'a':', '.join(deleted)}))
1258            self.context.writeLogMessage(
1259                self,'removed: %s' % ', '.join(deleted))
1260        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1261        return
1262
1263class ValidateCoursesPage(UtilityView, grok.View):
1264    """ Validate course list by course adviser
1265    """
1266    grok.context(IStudentStudyLevel)
1267    grok.name('validate_courses')
1268    grok.require('waeup.validateStudent')
1269
1270    def update(self):
1271        if not self.context.__parent__.is_current:
1272            emit_lock_message(self)
1273            return
1274        if str(self.context.__parent__.current_level) != self.context.__name__:
1275            self.flash(_('This level does not correspond current level.'))
1276        elif self.context.student.state == REGISTERED:
1277            IWorkflowInfo(self.context.student).fireTransition(
1278                'validate_courses')
1279            self.flash(_('Course list has been validated.'))
1280        else:
1281            self.flash(_('Student is in the wrong state.'))
1282        self.redirect(self.url(self.context))
1283        return
1284
1285    def render(self):
1286        return
1287
1288class RejectCoursesPage(UtilityView, grok.View):
1289    """ Reject course list by course adviser
1290    """
1291    grok.context(IStudentStudyLevel)
1292    grok.name('reject_courses')
1293    grok.require('waeup.validateStudent')
1294
1295    def update(self):
1296        if not self.context.__parent__.is_current:
1297            emit_lock_message(self)
1298            return
1299        if str(self.context.__parent__.current_level) != self.context.__name__:
1300            self.flash(_('This level does not correspond current level.'))
1301            self.redirect(self.url(self.context))
1302            return
1303        elif self.context.student.state == VALIDATED:
1304            IWorkflowInfo(self.context.student).fireTransition('reset8')
1305            message = _('Course list request has been annulled.')
1306            self.flash(message)
1307        elif self.context.student.state == REGISTERED:
1308            IWorkflowInfo(self.context.student).fireTransition('reset7')
1309            message = _('Course list request has been rejected:')
1310            self.flash(message)
1311        else:
1312            self.flash(_('Student is in the wrong state.'))
1313            self.redirect(self.url(self.context))
1314            return
1315        args = {'subject':message}
1316        self.redirect(self.url(self.context.student) +
1317            '/contactstudent?%s' % urlencode(args))
1318        return
1319
1320    def render(self):
1321        return
1322
1323class CourseTicketAddFormPage(KofaAddFormPage):
1324    """Add a course ticket.
1325    """
1326    grok.context(IStudentStudyLevel)
1327    grok.name('add')
1328    grok.require('waeup.manageStudent')
1329    label = _('Add course ticket')
1330    form_fields = grok.AutoFields(ICourseTicketAdd)
1331    pnav = 4
1332
1333    def update(self):
1334        if not self.context.__parent__.is_current:
1335            emit_lock_message(self)
1336            return
1337        super(CourseTicketAddFormPage, self).update()
1338        return
1339
1340    @action(_('Add course ticket'))
1341    def addCourseTicket(self, **data):
1342        course = data['course']
1343        success = addCourseTicket(self, course)
1344        if success:
1345            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1346        return
1347
1348    @action(_('Cancel'), validator=NullValidator)
1349    def cancel(self, **data):
1350        self.redirect(self.url(self.context))
1351
1352class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1353    """ Page to display course tickets
1354    """
1355    grok.context(ICourseTicket)
1356    grok.name('index')
1357    grok.require('waeup.viewStudent')
1358    form_fields = grok.AutoFields(ICourseTicket)
1359    grok.template('courseticketpage')
1360    pnav = 4
1361
1362    @property
1363    def label(self):
1364        return _('${a}: Course Ticket ${b}', mapping = {
1365            'a':self.context.student.display_fullname,
1366            'b':self.context.code})
1367
1368class CourseTicketManageFormPage(KofaEditFormPage):
1369    """ Page to manage course tickets
1370    """
1371    grok.context(ICourseTicket)
1372    grok.name('manage')
1373    grok.require('waeup.manageStudent')
1374    form_fields = grok.AutoFields(ICourseTicket)
1375    form_fields['title'].for_display = True
1376    form_fields['fcode'].for_display = True
1377    form_fields['dcode'].for_display = True
1378    form_fields['semester'].for_display = True
1379    form_fields['passmark'].for_display = True
1380    form_fields['credits'].for_display = True
1381    form_fields['mandatory'].for_display = False
1382    form_fields['automatic'].for_display = True
1383    form_fields['carry_over'].for_display = True
1384    pnav = 4
1385    grok.template('courseticketmanagepage')
1386
1387    @property
1388    def label(self):
1389        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1390
1391    @action('Save', style='primary')
1392    def save(self, **data):
1393        msave(self, **data)
1394        return
1395
1396class PaymentsManageFormPage(KofaEditFormPage):
1397    """ Page to manage the student payments
1398
1399    This manage form page is for both students and students officers.
1400    """
1401    grok.context(IStudentPaymentsContainer)
1402    grok.name('index')
1403    grok.require('waeup.payStudent')
1404    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1405    grok.template('paymentsmanagepage')
1406    pnav = 4
1407
1408    def unremovable(self, ticket):
1409        usertype = getattr(self.request.principal, 'user_type', None)
1410        if not usertype:
1411            return False
1412        return (self.request.principal.user_type == 'student' and ticket.r_code)
1413
1414    @property
1415    def label(self):
1416        return _('${a}: Payments',
1417            mapping = {'a':self.context.__parent__.display_fullname})
1418
1419    def update(self):
1420        super(PaymentsManageFormPage, self).update()
1421        datatable.need()
1422        warning.need()
1423        return
1424
1425    @jsaction(_('Remove selected tickets'))
1426    def delPaymentTicket(self, **data):
1427        form = self.request.form
1428        if 'val_id' in form:
1429            child_id = form['val_id']
1430        else:
1431            self.flash(_('No payment selected.'))
1432            self.redirect(self.url(self.context))
1433            return
1434        if not isinstance(child_id, list):
1435            child_id = [child_id]
1436        deleted = []
1437        for id in child_id:
1438            # Students are not allowed to remove used payment tickets
1439            if not self.unremovable(self.context[id]):
1440                del self.context[id]
1441                deleted.append(id)
1442        if len(deleted):
1443            self.flash(_('Successfully removed: ${a}',
1444                mapping = {'a': ', '.join(deleted)}))
1445            self.context.writeLogMessage(
1446                self,'removed: %s' % ', '.join(deleted))
1447        self.redirect(self.url(self.context))
1448        return
1449
1450    #@action(_('Add online payment ticket'))
1451    #def addPaymentTicket(self, **data):
1452    #    self.redirect(self.url(self.context, '@@addop'))
1453
1454class OnlinePaymentAddFormPage(KofaAddFormPage):
1455    """ Page to add an online payment ticket
1456    """
1457    grok.context(IStudentPaymentsContainer)
1458    grok.name('addop')
1459    grok.template('onlinepaymentaddform')
1460    grok.require('waeup.payStudent')
1461    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1462        'p_category')
1463    label = _('Add online payment')
1464    pnav = 4
1465
1466    @property
1467    def selectable_categories(self):
1468        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1469        return sorted(categories.items())
1470
1471    @action(_('Create ticket'), style='primary')
1472    def createTicket(self, **data):
1473        p_category = data['p_category']
1474        previous_session = data.get('p_session', None)
1475        previous_level = data.get('p_level', None)
1476        student = self.context.__parent__
1477        if p_category == 'bed_allocation' and student[
1478            'studycourse'].current_session != grok.getSite()[
1479            'hostels'].accommodation_session:
1480                self.flash(
1481                    _('Your current session does not match ' + \
1482                    'accommodation session.'))
1483                return
1484        if 'maintenance' in p_category:
1485            current_session = str(student['studycourse'].current_session)
1486            if not current_session in student['accommodation']:
1487                self.flash(_('You have not yet booked accommodation.'))
1488                return
1489        students_utils = getUtility(IStudentsUtils)
1490        error, payment = students_utils.setPaymentDetails(
1491            p_category, student, previous_session, previous_level)
1492        if error is not None:
1493            self.flash(error)
1494            return
1495        self.context[payment.p_id] = payment
1496        self.flash(_('Payment ticket created.'))
1497        self.redirect(self.url(self.context))
1498        return
1499
1500    @action(_('Cancel'), validator=NullValidator)
1501    def cancel(self, **data):
1502        self.redirect(self.url(self.context))
1503
1504class PreviousPaymentAddFormPage(KofaAddFormPage):
1505    """ Page to add an online payment ticket for previous sessions
1506    """
1507    grok.context(IStudentPaymentsContainer)
1508    grok.name('addpp')
1509    grok.require('waeup.payStudent')
1510    form_fields = grok.AutoFields(IStudentPreviousPayment)
1511    label = _('Add previous session online payment')
1512    pnav = 4
1513
1514    def update(self):
1515        if self.context.student.before_payment:
1516            self.flash(_("No previous payment to be made."))
1517            self.redirect(self.url(self.context))
1518        super(PreviousPaymentAddFormPage, self).update()
1519        return
1520
1521    @action(_('Create ticket'), style='primary')
1522    def createTicket(self, **data):
1523        p_category = data['p_category']
1524        previous_session = data.get('p_session', None)
1525        previous_level = data.get('p_level', None)
1526        student = self.context.__parent__
1527        students_utils = getUtility(IStudentsUtils)
1528        error, payment = students_utils.setPaymentDetails(
1529            p_category, student, previous_session, previous_level)
1530        if error is not None:
1531            self.flash(error)
1532            return
1533        self.context[payment.p_id] = payment
1534        self.flash(_('Payment ticket created.'))
1535        self.redirect(self.url(self.context))
1536        return
1537
1538    @action(_('Cancel'), validator=NullValidator)
1539    def cancel(self, **data):
1540        self.redirect(self.url(self.context))
1541
1542class BalancePaymentAddFormPage(KofaAddFormPage):
1543    """ Page to add an online payment ticket for balance sessions
1544    """
1545    grok.context(IStudentPaymentsContainer)
1546    grok.name('addbp')
1547    grok.require('waeup.payStudent')
1548    form_fields = grok.AutoFields(IStudentBalancePayment)
1549    label = _('Add balance')
1550    pnav = 4
1551
1552    @action(_('Create ticket'), style='primary')
1553    def createTicket(self, **data):
1554        p_category = data['p_category']
1555        balance_session = data.get('balance_session', None)
1556        balance_level = data.get('balance_level', None)
1557        balance_amount = data.get('balance_amount', None)
1558        student = self.context.__parent__
1559        students_utils = getUtility(IStudentsUtils)
1560        error, payment = students_utils.setBalanceDetails(
1561            p_category, student, balance_session,
1562            balance_level, balance_amount)
1563        if error is not None:
1564            self.flash(error)
1565            return
1566        self.context[payment.p_id] = payment
1567        self.flash(_('Payment ticket created.'))
1568        self.redirect(self.url(self.context))
1569        return
1570
1571    @action(_('Cancel'), validator=NullValidator)
1572    def cancel(self, **data):
1573        self.redirect(self.url(self.context))
1574
1575class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1576    """ Page to view an online payment ticket
1577    """
1578    grok.context(IStudentOnlinePayment)
1579    grok.name('index')
1580    grok.require('waeup.viewStudent')
1581    form_fields = grok.AutoFields(IStudentOnlinePayment)
1582    form_fields[
1583        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1584    form_fields[
1585        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1586    pnav = 4
1587
1588    @property
1589    def label(self):
1590        return _('${a}: Online Payment Ticket ${b}', mapping = {
1591            'a':self.context.student.display_fullname,
1592            'b':self.context.p_id})
1593
1594class OnlinePaymentApprovePage(UtilityView, grok.View):
1595    """ Callback view
1596    """
1597    grok.context(IStudentOnlinePayment)
1598    grok.name('approve')
1599    grok.require('waeup.managePortal')
1600
1601    def update(self):
1602        success, msg, log = self.context.approveStudentPayment()
1603        if log is not None:
1604            # Add log message to students.log
1605            self.context.writeLogMessage(self,log)
1606            # Add log message to payments.log
1607            self.context.logger.info(
1608                '%s,%s,%s,%s,%s,,,,,,' % (
1609                self.context.student.student_id,
1610                self.context.p_id, self.context.p_category,
1611                self.context.amount_auth, self.context.r_code))
1612        self.flash(msg)
1613        return
1614
1615    def render(self):
1616        self.redirect(self.url(self.context, '@@index'))
1617        return
1618
1619class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1620    """ Approval view for students.
1621
1622    This view is used for browser tests only and
1623    must be neutralized in custom pages!
1624    """
1625
1626    grok.name('fake_approve')
1627    grok.require('waeup.payStudent')
1628
1629class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1630    """Deliver a PDF slip of the context.
1631    """
1632    grok.context(IStudentOnlinePayment)
1633    grok.name('payment_slip.pdf')
1634    grok.require('waeup.viewStudent')
1635    form_fields = grok.AutoFields(IStudentOnlinePayment)
1636    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1637    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1638    prefix = 'form'
1639    note = None
1640    omit_fields = (
1641        'password', 'suspended', 'phone',
1642        'adm_code', 'sex', 'suspended_comment')
1643
1644    @property
1645    def title(self):
1646        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1647        return translate(_('Payment Data'), 'waeup.kofa',
1648            target_language=portal_language)
1649
1650    @property
1651    def label(self):
1652        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1653        return translate(_('Online Payment Slip'),
1654            'waeup.kofa', target_language=portal_language) \
1655            + ' %s' % self.context.p_id
1656
1657    def render(self):
1658        #if self.context.p_state != 'paid':
1659        #    self.flash('Ticket not yet paid.')
1660        #    self.redirect(self.url(self.context))
1661        #    return
1662        studentview = StudentBasePDFFormPage(self.context.student,
1663            self.request, self.omit_fields)
1664        students_utils = getUtility(IStudentsUtils)
1665        return students_utils.renderPDF(self, 'payment_slip.pdf',
1666            self.context.student, studentview, note=self.note)
1667
1668
1669class AccommodationManageFormPage(KofaEditFormPage):
1670    """ Page to manage bed tickets.
1671
1672    This manage form page is for both students and students officers.
1673    """
1674    grok.context(IStudentAccommodation)
1675    grok.name('index')
1676    grok.require('waeup.handleAccommodation')
1677    form_fields = grok.AutoFields(IStudentAccommodation)
1678    grok.template('accommodationmanagepage')
1679    pnav = 4
1680    officers_only_actions = [_('Remove selected')]
1681
1682    @property
1683    def label(self):
1684        return _('${a}: Accommodation',
1685            mapping = {'a':self.context.__parent__.display_fullname})
1686
1687    def update(self):
1688        super(AccommodationManageFormPage, self).update()
1689        datatable.need()
1690        warning.need()
1691        return
1692
1693    @jsaction(_('Remove selected'))
1694    def delBedTickets(self, **data):
1695        if getattr(self.request.principal, 'user_type', None) == 'student':
1696            self.flash(_('You are not allowed to remove bed tickets.'))
1697            self.redirect(self.url(self.context))
1698            return
1699        form = self.request.form
1700        if 'val_id' in form:
1701            child_id = form['val_id']
1702        else:
1703            self.flash(_('No bed ticket selected.'))
1704            self.redirect(self.url(self.context))
1705            return
1706        if not isinstance(child_id, list):
1707            child_id = [child_id]
1708        deleted = []
1709        for id in child_id:
1710            del self.context[id]
1711            deleted.append(id)
1712        if len(deleted):
1713            self.flash(_('Successfully removed: ${a}',
1714                mapping = {'a':', '.join(deleted)}))
1715            self.context.writeLogMessage(
1716                self,'removed: % s' % ', '.join(deleted))
1717        self.redirect(self.url(self.context))
1718        return
1719
1720    @property
1721    def selected_actions(self):
1722        if getattr(self.request.principal, 'user_type', None) == 'student':
1723            return [action for action in self.actions
1724                    if not action.label in self.officers_only_actions]
1725        return self.actions
1726
1727class BedTicketAddPage(KofaPage):
1728    """ Page to add an online payment ticket
1729    """
1730    grok.context(IStudentAccommodation)
1731    grok.name('add')
1732    grok.require('waeup.handleAccommodation')
1733    grok.template('enterpin')
1734    ac_prefix = 'HOS'
1735    label = _('Add bed ticket')
1736    pnav = 4
1737    buttonname = _('Create bed ticket')
1738    notice = ''
1739    with_ac = True
1740
1741    def update(self, SUBMIT=None):
1742        student = self.context.student
1743        students_utils = getUtility(IStudentsUtils)
1744        acc_details  = students_utils.getAccommodationDetails(student)
1745        if acc_details.get('expired', False):
1746            startdate = acc_details.get('startdate')
1747            enddate = acc_details.get('enddate')
1748            if startdate and enddate:
1749                tz = getUtility(IKofaUtils).tzinfo
1750                startdate = to_timezone(
1751                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1752                enddate = to_timezone(
1753                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1754                self.flash(_("Outside booking period: ${a} - ${b}",
1755                    mapping = {'a': startdate, 'b': enddate}))
1756            else:
1757                self.flash(_("Outside booking period."))
1758            self.redirect(self.url(self.context))
1759            return
1760        if not acc_details:
1761            self.flash(_("Your data are incomplete."))
1762            self.redirect(self.url(self.context))
1763            return
1764        if not student.state in acc_details['allowed_states']:
1765            self.flash(_("You are in the wrong registration state."))
1766            self.redirect(self.url(self.context))
1767            return
1768        if student['studycourse'].current_session != acc_details[
1769            'booking_session']:
1770            self.flash(
1771                _('Your current session does not match accommodation session.'))
1772            self.redirect(self.url(self.context))
1773            return
1774        if str(acc_details['booking_session']) in self.context.keys():
1775            self.flash(
1776                _('You already booked a bed space in current ' \
1777                    + 'accommodation session.'))
1778            self.redirect(self.url(self.context))
1779            return
1780        if self.with_ac:
1781            self.ac_series = self.request.form.get('ac_series', None)
1782            self.ac_number = self.request.form.get('ac_number', None)
1783        if SUBMIT is None:
1784            return
1785        if self.with_ac:
1786            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1787            code = get_access_code(pin)
1788            if not code:
1789                self.flash(_('Activation code is invalid.'))
1790                return
1791        # Search and book bed
1792        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1793        entries = cat.searchResults(
1794            owner=(student.student_id,student.student_id))
1795        if len(entries):
1796            # If bed space has been manually allocated use this bed
1797            bed = [entry for entry in entries][0]
1798            # Safety belt for paranoids: Does this bed really exist on portal?
1799            # XXX: Can be remove if nobody complains.
1800            if bed.__parent__.__parent__ is None:
1801                self.flash(_('System error: Please contact the adminsitrator.'))
1802                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1803                return
1804        else:
1805            # else search for other available beds
1806            entries = cat.searchResults(
1807                bed_type=(acc_details['bt'],acc_details['bt']))
1808            available_beds = [
1809                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1810            if available_beds:
1811                students_utils = getUtility(IStudentsUtils)
1812                bed = students_utils.selectBed(available_beds)
1813                # Safety belt for paranoids: Does this bed really exist in portal?
1814                # XXX: Can be remove if nobody complains.
1815                if bed.__parent__.__parent__ is None:
1816                    self.flash(_('System error: Please contact the adminsitrator.'))
1817                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1818                    return
1819                bed.bookBed(student.student_id)
1820            else:
1821                self.flash(_('There is no free bed in your category ${a}.',
1822                    mapping = {'a':acc_details['bt']}))
1823                return
1824        if self.with_ac:
1825            # Mark pin as used (this also fires a pin related transition)
1826            if code.state == USED:
1827                self.flash(_('Activation code has already been used.'))
1828                return
1829            else:
1830                comment = _(u'invalidated')
1831                # Here we know that the ac is in state initialized so we do not
1832                # expect an exception, but the owner might be different
1833                if not invalidate_accesscode(
1834                    pin,comment,self.context.student.student_id):
1835                    self.flash(_('You are not the owner of this access code.'))
1836                    return
1837        # Create bed ticket
1838        bedticket = createObject(u'waeup.BedTicket')
1839        if self.with_ac:
1840            bedticket.booking_code = pin
1841        bedticket.booking_session = acc_details['booking_session']
1842        bedticket.bed_type = acc_details['bt']
1843        bedticket.bed = bed
1844        hall_title = bed.__parent__.hostel_name
1845        coordinates = bed.coordinates[1:]
1846        block, room_nr, bed_nr = coordinates
1847        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1848            'a':hall_title, 'b':block,
1849            'c':room_nr, 'd':bed_nr,
1850            'e':bed.bed_type})
1851        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1852        bedticket.bed_coordinates = translate(
1853            bc, 'waeup.kofa',target_language=portal_language)
1854        self.context.addBedTicket(bedticket)
1855        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
1856        self.flash(_('Bed ticket created and bed booked: ${a}',
1857            mapping = {'a':bedticket.bed_coordinates}))
1858        self.redirect(self.url(self.context))
1859        return
1860
1861class BedTicketDisplayFormPage(KofaDisplayFormPage):
1862    """ Page to display bed tickets
1863    """
1864    grok.context(IBedTicket)
1865    grok.name('index')
1866    grok.require('waeup.handleAccommodation')
1867    form_fields = grok.AutoFields(IBedTicket)
1868    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1869    pnav = 4
1870
1871    @property
1872    def label(self):
1873        return _('Bed Ticket for Session ${a}',
1874            mapping = {'a':self.context.getSessionString()})
1875
1876class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1877    """Deliver a PDF slip of the context.
1878    """
1879    grok.context(IBedTicket)
1880    grok.name('bed_allocation_slip.pdf')
1881    grok.require('waeup.handleAccommodation')
1882    form_fields = grok.AutoFields(IBedTicket)
1883    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1884    prefix = 'form'
1885    omit_fields = (
1886        'password', 'suspended', 'phone', 'adm_code', 'suspended_comment')
1887
1888    @property
1889    def title(self):
1890        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1891        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1892            target_language=portal_language)
1893
1894    @property
1895    def label(self):
1896        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1897        #return translate(_('Bed Allocation: '),
1898        #    'waeup.kofa', target_language=portal_language) \
1899        #    + ' %s' % self.context.bed_coordinates
1900        return translate(_('Bed Allocation Slip'),
1901            'waeup.kofa', target_language=portal_language) \
1902            + ' %s' % self.context.getSessionString()
1903
1904    def render(self):
1905        studentview = StudentBasePDFFormPage(self.context.student,
1906            self.request, self.omit_fields)
1907        students_utils = getUtility(IStudentsUtils)
1908        return students_utils.renderPDF(
1909            self, 'bed_allocation_slip.pdf',
1910            self.context.student, studentview)
1911
1912class BedTicketRelocationPage(UtilityView, grok.View):
1913    """ Callback view
1914    """
1915    grok.context(IBedTicket)
1916    grok.name('relocate')
1917    grok.require('waeup.manageHostels')
1918
1919    # Relocate student if student parameters have changed or the bed_type
1920    # of the bed has changed
1921    def update(self):
1922        student = self.context.student
1923        students_utils = getUtility(IStudentsUtils)
1924        acc_details  = students_utils.getAccommodationDetails(student)
1925        if self.context.bed != None and \
1926              'reserved' in self.context.bed.bed_type:
1927            self.flash(_("Students in reserved beds can't be relocated."))
1928            self.redirect(self.url(self.context))
1929            return
1930        if acc_details['bt'] == self.context.bed_type and \
1931                self.context.bed != None and \
1932                self.context.bed.bed_type == self.context.bed_type:
1933            self.flash(_("Student can't be relocated."))
1934            self.redirect(self.url(self.context))
1935            return
1936        # Search a bed
1937        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1938        entries = cat.searchResults(
1939            owner=(student.student_id,student.student_id))
1940        if len(entries) and self.context.bed == None:
1941            # If booking has been cancelled but other bed space has been
1942            # manually allocated after cancellation use this bed
1943            new_bed = [entry for entry in entries][0]
1944        else:
1945            # Search for other available beds
1946            entries = cat.searchResults(
1947                bed_type=(acc_details['bt'],acc_details['bt']))
1948            available_beds = [
1949                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1950            if available_beds:
1951                students_utils = getUtility(IStudentsUtils)
1952                new_bed = students_utils.selectBed(available_beds)
1953                new_bed.bookBed(student.student_id)
1954            else:
1955                self.flash(_('There is no free bed in your category ${a}.',
1956                    mapping = {'a':acc_details['bt']}))
1957                self.redirect(self.url(self.context))
1958                return
1959        # Release old bed if exists
1960        if self.context.bed != None:
1961            self.context.bed.owner = NOT_OCCUPIED
1962            notify(grok.ObjectModifiedEvent(self.context.bed))
1963        # Alocate new bed
1964        self.context.bed_type = acc_details['bt']
1965        self.context.bed = new_bed
1966        hall_title = new_bed.__parent__.hostel_name
1967        coordinates = new_bed.coordinates[1:]
1968        block, room_nr, bed_nr = coordinates
1969        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1970            'a':hall_title, 'b':block,
1971            'c':room_nr, 'd':bed_nr,
1972            'e':new_bed.bed_type})
1973        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1974        self.context.bed_coordinates = translate(
1975            bc, 'waeup.kofa',target_language=portal_language)
1976        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
1977        self.flash(_('Student relocated: ${a}',
1978            mapping = {'a':self.context.bed_coordinates}))
1979        self.redirect(self.url(self.context))
1980        return
1981
1982    def render(self):
1983        return
1984
1985class StudentHistoryPage(KofaPage):
1986    """ Page to display student clearance data
1987    """
1988    grok.context(IStudent)
1989    grok.name('history')
1990    grok.require('waeup.viewStudent')
1991    grok.template('studenthistory')
1992    pnav = 4
1993
1994    @property
1995    def label(self):
1996        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1997
1998# Pages for students only
1999
2000class StudentBaseEditFormPage(KofaEditFormPage):
2001    """ View to edit student base data
2002    """
2003    grok.context(IStudent)
2004    grok.name('edit_base')
2005    grok.require('waeup.handleStudent')
2006    form_fields = grok.AutoFields(IStudentBase).select(
2007        'email', 'phone')
2008    label = _('Edit base data')
2009    pnav = 4
2010
2011    @action(_('Save'), style='primary')
2012    def save(self, **data):
2013        msave(self, **data)
2014        return
2015
2016class StudentChangePasswordPage(KofaEditFormPage):
2017    """ View to manage student base data
2018    """
2019    grok.context(IStudent)
2020    grok.name('change_password')
2021    grok.require('waeup.handleStudent')
2022    grok.template('change_password')
2023    label = _('Change password')
2024    pnav = 4
2025
2026    @action(_('Save'), style='primary')
2027    def save(self, **data):
2028        form = self.request.form
2029        password = form.get('change_password', None)
2030        password_ctl = form.get('change_password_repeat', None)
2031        if password:
2032            validator = getUtility(IPasswordValidator)
2033            errors = validator.validate_password(password, password_ctl)
2034            if not errors:
2035                IUserAccount(self.context).setPassword(password)
2036                self.context.writeLogMessage(self, 'saved: password')
2037                self.flash(_('Password changed.'))
2038            else:
2039                self.flash( ' '.join(errors))
2040        return
2041
2042class StudentFilesUploadPage(KofaPage):
2043    """ View to upload files by student
2044    """
2045    grok.context(IStudent)
2046    grok.name('change_portrait')
2047    grok.require('waeup.uploadStudentFile')
2048    grok.template('filesuploadpage')
2049    label = _('Upload portrait')
2050    pnav = 4
2051
2052    def update(self):
2053        if self.context.student.state != ADMITTED:
2054            emit_lock_message(self)
2055            return
2056        super(StudentFilesUploadPage, self).update()
2057        return
2058
2059class StartClearancePage(KofaPage):
2060    grok.context(IStudent)
2061    grok.name('start_clearance')
2062    grok.require('waeup.handleStudent')
2063    grok.template('enterpin')
2064    label = _('Start clearance')
2065    ac_prefix = 'CLR'
2066    notice = ''
2067    pnav = 4
2068    buttonname = _('Start clearance now')
2069
2070    @property
2071    def all_required_fields_filled(self):
2072        if self.context.email and self.context.phone:
2073            return True
2074        return False
2075
2076    @property
2077    def portrait_uploaded(self):
2078        store = getUtility(IExtFileStore)
2079        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2080            return True
2081        return False
2082
2083    def update(self, SUBMIT=None):
2084        if not self.context.state == ADMITTED:
2085            self.flash(_("Wrong state"))
2086            self.redirect(self.url(self.context))
2087            return
2088        if not self.portrait_uploaded:
2089            self.flash(_("No portrait uploaded."))
2090            self.redirect(self.url(self.context, 'change_portrait'))
2091            return
2092        if not self.all_required_fields_filled:
2093            self.flash(_("Not all required fields filled."))
2094            self.redirect(self.url(self.context, 'edit_base'))
2095            return
2096        self.ac_series = self.request.form.get('ac_series', None)
2097        self.ac_number = self.request.form.get('ac_number', None)
2098
2099        if SUBMIT is None:
2100            return
2101        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2102        code = get_access_code(pin)
2103        if not code:
2104            self.flash(_('Activation code is invalid.'))
2105            return
2106        if code.state == USED:
2107            self.flash(_('Activation code has already been used.'))
2108            return
2109        # Mark pin as used (this also fires a pin related transition)
2110        # and fire transition start_clearance
2111        comment = _(u"invalidated")
2112        # Here we know that the ac is in state initialized so we do not
2113        # expect an exception, but the owner might be different
2114        if not invalidate_accesscode(pin, comment, self.context.student_id):
2115            self.flash(_('You are not the owner of this access code.'))
2116            return
2117        self.context.clr_code = pin
2118        IWorkflowInfo(self.context).fireTransition('start_clearance')
2119        self.flash(_('Clearance process has been started.'))
2120        self.redirect(self.url(self.context,'cedit'))
2121        return
2122
2123class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2124    """ View to edit student clearance data by student
2125    """
2126    grok.context(IStudent)
2127    grok.name('cedit')
2128    grok.require('waeup.handleStudent')
2129    label = _('Edit clearance data')
2130
2131    @property
2132    def form_fields(self):
2133        if self.context.is_postgrad:
2134            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2135                'clearance_locked', 'clr_code', 'officer_comment')
2136        else:
2137            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2138                'clearance_locked', 'clr_code', 'officer_comment')
2139        return form_fields
2140
2141    def update(self):
2142        if self.context.clearance_locked:
2143            emit_lock_message(self)
2144            return
2145        return super(StudentClearanceEditFormPage, self).update()
2146
2147    @action(_('Save'), style='primary')
2148    def save(self, **data):
2149        self.applyData(self.context, **data)
2150        self.flash(_('Clearance form has been saved.'))
2151        return
2152
2153    def dataNotComplete(self):
2154        """To be implemented in the customization package.
2155        """
2156        return False
2157
2158    @action(_('Save and request clearance'), style='primary')
2159    def requestClearance(self, **data):
2160        self.applyData(self.context, **data)
2161        if self.dataNotComplete():
2162            self.flash(self.dataNotComplete())
2163            return
2164        self.flash(_('Clearance form has been saved.'))
2165        if self.context.clr_code:
2166            self.redirect(self.url(self.context, 'request_clearance'))
2167        else:
2168            # We bypass the request_clearance page if student
2169            # has been imported in state 'clearance started' and
2170            # no clr_code was entered before.
2171            state = IWorkflowState(self.context).getState()
2172            if state != CLEARANCE:
2173                # This shouldn't happen, but the application officer
2174                # might have forgotten to lock the form after changing the state
2175                self.flash(_('This form cannot be submitted. Wrong state!'))
2176                return
2177            IWorkflowInfo(self.context).fireTransition('request_clearance')
2178            self.flash(_('Clearance has been requested.'))
2179            self.redirect(self.url(self.context))
2180        return
2181
2182class RequestClearancePage(KofaPage):
2183    grok.context(IStudent)
2184    grok.name('request_clearance')
2185    grok.require('waeup.handleStudent')
2186    grok.template('enterpin')
2187    label = _('Request clearance')
2188    notice = _('Enter the CLR access code used for starting clearance.')
2189    ac_prefix = 'CLR'
2190    pnav = 4
2191    buttonname = _('Request clearance now')
2192
2193    def update(self, SUBMIT=None):
2194        self.ac_series = self.request.form.get('ac_series', None)
2195        self.ac_number = self.request.form.get('ac_number', None)
2196        if SUBMIT is None:
2197            return
2198        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2199        if self.context.clr_code and self.context.clr_code != pin:
2200            self.flash(_("This isn't your CLR access code."))
2201            return
2202        state = IWorkflowState(self.context).getState()
2203        if state != CLEARANCE:
2204            # This shouldn't happen, but the application officer
2205            # might have forgotten to lock the form after changing the state
2206            self.flash(_('This form cannot be submitted. Wrong state!'))
2207            return
2208        IWorkflowInfo(self.context).fireTransition('request_clearance')
2209        self.flash(_('Clearance has been requested.'))
2210        self.redirect(self.url(self.context))
2211        return
2212
2213class StartSessionPage(KofaPage):
2214    grok.context(IStudentStudyCourse)
2215    grok.name('start_session')
2216    grok.require('waeup.handleStudent')
2217    grok.template('enterpin')
2218    label = _('Start session')
2219    ac_prefix = 'SFE'
2220    notice = ''
2221    pnav = 4
2222    buttonname = _('Start now')
2223
2224    def update(self, SUBMIT=None):
2225        if not self.context.is_current:
2226            emit_lock_message(self)
2227            return
2228        super(StartSessionPage, self).update()
2229        if not self.context.next_session_allowed:
2230            self.flash(_("You are not entitled to start session."))
2231            self.redirect(self.url(self.context))
2232            return
2233        self.ac_series = self.request.form.get('ac_series', None)
2234        self.ac_number = self.request.form.get('ac_number', None)
2235
2236        if SUBMIT is None:
2237            return
2238        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2239        code = get_access_code(pin)
2240        if not code:
2241            self.flash(_('Activation code is invalid.'))
2242            return
2243        # Mark pin as used (this also fires a pin related transition)
2244        if code.state == USED:
2245            self.flash(_('Activation code has already been used.'))
2246            return
2247        else:
2248            comment = _(u"invalidated")
2249            # Here we know that the ac is in state initialized so we do not
2250            # expect an error, but the owner might be different
2251            if not invalidate_accesscode(
2252                pin,comment,self.context.student.student_id):
2253                self.flash(_('You are not the owner of this access code.'))
2254                return
2255        try:
2256            if self.context.student.state == CLEARED:
2257                IWorkflowInfo(self.context.student).fireTransition(
2258                    'pay_first_school_fee')
2259            elif self.context.student.state == RETURNING:
2260                IWorkflowInfo(self.context.student).fireTransition(
2261                    'pay_school_fee')
2262            elif self.context.student.state == PAID:
2263                IWorkflowInfo(self.context.student).fireTransition(
2264                    'pay_pg_fee')
2265        except ConstraintNotSatisfied:
2266            self.flash(_('An error occurred, please contact the system administrator.'))
2267            return
2268        self.flash(_('Session started.'))
2269        self.redirect(self.url(self.context))
2270        return
2271
2272class AddStudyLevelFormPage(KofaEditFormPage):
2273    """ Page for students to add current study levels
2274    """
2275    grok.context(IStudentStudyCourse)
2276    grok.name('add')
2277    grok.require('waeup.handleStudent')
2278    grok.template('studyleveladdpage')
2279    form_fields = grok.AutoFields(IStudentStudyCourse)
2280    pnav = 4
2281
2282    @property
2283    def label(self):
2284        studylevelsource = StudyLevelSource().factory
2285        code = self.context.current_level
2286        title = studylevelsource.getTitle(self.context, code)
2287        return _('Add current level ${a}', mapping = {'a':title})
2288
2289    def update(self):
2290        if not self.context.is_current:
2291            emit_lock_message(self)
2292            return
2293        if self.context.student.state != PAID:
2294            emit_lock_message(self)
2295            return
2296        super(AddStudyLevelFormPage, self).update()
2297        return
2298
2299    @action(_('Create course list now'), style='primary')
2300    def addStudyLevel(self, **data):
2301        studylevel = createObject(u'waeup.StudentStudyLevel')
2302        studylevel.level = self.context.current_level
2303        studylevel.level_session = self.context.current_session
2304        try:
2305            self.context.addStudentStudyLevel(
2306                self.context.certificate,studylevel)
2307        except KeyError:
2308            self.flash(_('This level exists.'))
2309        except RequiredMissing:
2310            self.flash(_('Your data are incomplete'))
2311        self.redirect(self.url(self.context))
2312        return
2313
2314class StudyLevelEditFormPage(KofaEditFormPage):
2315    """ Page to edit the student study level data by students
2316    """
2317    grok.context(IStudentStudyLevel)
2318    grok.name('edit')
2319    grok.require('waeup.handleStudent')
2320    grok.template('studyleveleditpage')
2321    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2322        'level_session', 'level_verdict')
2323    pnav = 4
2324
2325    def update(self, ADD=None, course=None):
2326        if not self.context.__parent__.is_current:
2327            emit_lock_message(self)
2328            return
2329        if self.context.student.state != PAID or \
2330            not self.context.is_current_level:
2331            emit_lock_message(self)
2332            return
2333        super(StudyLevelEditFormPage, self).update()
2334        datatable.need()
2335        warning.need()
2336        if ADD is not None:
2337            if not course:
2338                self.flash(_('No valid course code entered.'))
2339                return
2340            cat = queryUtility(ICatalog, name='courses_catalog')
2341            result = cat.searchResults(code=(course, course))
2342            if len(result) != 1:
2343                self.flash(_('Course not found.'))
2344                return
2345            course = list(result)[0]
2346            addCourseTicket(self, course)
2347        return
2348
2349    @property
2350    def label(self):
2351        # Here we know that the cookie has been set
2352        lang = self.request.cookies.get('kofa.language')
2353        level_title = translate(self.context.level_title, 'waeup.kofa',
2354            target_language=lang)
2355        return _('Edit course list of ${a}',
2356            mapping = {'a':level_title})
2357
2358    @property
2359    def translated_values(self):
2360        return translated_values(self)
2361
2362    def _delCourseTicket(self, **data):
2363        form = self.request.form
2364        if 'val_id' in form:
2365            child_id = form['val_id']
2366        else:
2367            self.flash(_('No ticket selected.'))
2368            self.redirect(self.url(self.context, '@@edit'))
2369            return
2370        if not isinstance(child_id, list):
2371            child_id = [child_id]
2372        deleted = []
2373        for id in child_id:
2374            # Students are not allowed to remove core tickets
2375            if id in self.context and \
2376                self.context[id].removable_by_student:
2377                del self.context[id]
2378                deleted.append(id)
2379        if len(deleted):
2380            self.flash(_('Successfully removed: ${a}',
2381                mapping = {'a':', '.join(deleted)}))
2382            self.context.writeLogMessage(
2383                self,'removed: %s' % ', '.join(deleted))
2384        self.redirect(self.url(self.context, u'@@edit'))
2385        return
2386
2387    @jsaction(_('Remove selected tickets'))
2388    def delCourseTicket(self, **data):
2389        self._delCourseTicket(**data)
2390        return
2391
2392    def _registerCourses(self, **data):
2393        if self.context.student.is_postgrad:
2394            self.flash(_(
2395                "You are a postgraduate student, "
2396                "your course list can't bee registered."))
2397            self.redirect(self.url(self.context))
2398            return
2399        students_utils = getUtility(IStudentsUtils)
2400        max_credits = students_utils.maxCredits(self.context)
2401        if self.context.total_credits > max_credits:
2402            self.flash(_('Maximum credits of ${a} exceeded.',
2403                mapping = {'a':max_credits}))
2404            return
2405        IWorkflowInfo(self.context.student).fireTransition(
2406            'register_courses')
2407        self.flash(_('Course list has been registered.'))
2408        self.redirect(self.url(self.context))
2409        return
2410
2411    @action(_('Register course list'))
2412    def registerCourses(self, **data):
2413        self._registerCourses(**data)
2414        return
2415
2416class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2417    """Add a course ticket by student.
2418    """
2419    grok.name('ctadd')
2420    grok.require('waeup.handleStudent')
2421    form_fields = grok.AutoFields(ICourseTicketAdd)
2422
2423    def update(self):
2424        if self.context.student.state != PAID or \
2425            not self.context.is_current_level:
2426            emit_lock_message(self)
2427            return
2428        super(CourseTicketAddFormPage2, self).update()
2429        return
2430
2431    @action(_('Add course ticket'))
2432    def addCourseTicket(self, **data):
2433        # Safety belt
2434        if self.context.student.state != PAID:
2435            return
2436        course = data['course']
2437        success = addCourseTicket(self, course)
2438        if success:
2439            self.redirect(self.url(self.context, u'@@edit'))
2440        return
2441
2442class SetPasswordPage(KofaPage):
2443    grok.context(IKofaObject)
2444    grok.name('setpassword')
2445    grok.require('waeup.Anonymous')
2446    grok.template('setpassword')
2447    label = _('Set password for first-time login')
2448    ac_prefix = 'PWD'
2449    pnav = 0
2450    set_button = _('Set')
2451
2452    def update(self, SUBMIT=None):
2453        self.reg_number = self.request.form.get('reg_number', None)
2454        self.ac_series = self.request.form.get('ac_series', None)
2455        self.ac_number = self.request.form.get('ac_number', None)
2456
2457        if SUBMIT is None:
2458            return
2459        hitlist = search(query=self.reg_number,
2460            searchtype='reg_number', view=self)
2461        if not hitlist:
2462            self.flash(_('No student found.'))
2463            return
2464        if len(hitlist) != 1:   # Cannot happen but anyway
2465            self.flash(_('More than one student found.'))
2466            return
2467        student = hitlist[0].context
2468        self.student_id = student.student_id
2469        student_pw = student.password
2470        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2471        code = get_access_code(pin)
2472        if not code:
2473            self.flash(_('Access code is invalid.'))
2474            return
2475        if student_pw and pin == student.adm_code:
2476            self.flash(_(
2477                'Password has already been set. Your Student Id is ${a}',
2478                mapping = {'a':self.student_id}))
2479            return
2480        elif student_pw:
2481            self.flash(
2482                _('Password has already been set. You are using the ' +
2483                'wrong Access Code.'))
2484            return
2485        # Mark pin as used (this also fires a pin related transition)
2486        # and set student password
2487        if code.state == USED:
2488            self.flash(_('Access code has already been used.'))
2489            return
2490        else:
2491            comment = _(u"invalidated")
2492            # Here we know that the ac is in state initialized so we do not
2493            # expect an exception
2494            invalidate_accesscode(pin,comment)
2495            IUserAccount(student).setPassword(self.ac_number)
2496            student.adm_code = pin
2497        self.flash(_('Password has been set. Your Student Id is ${a}',
2498            mapping = {'a':self.student_id}))
2499        return
2500
2501class StudentRequestPasswordPage(KofaAddFormPage):
2502    """Captcha'd registration page for applicants.
2503    """
2504    grok.name('requestpw')
2505    grok.require('waeup.Anonymous')
2506    grok.template('requestpw')
2507    form_fields = grok.AutoFields(IStudentRequestPW).select(
2508        'firstname','number','email')
2509    label = _('Request password for first-time login')
2510
2511    def update(self):
2512        # Handle captcha
2513        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2514        self.captcha_result = self.captcha.verify(self.request)
2515        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2516        return
2517
2518    def _redirect(self, email, password, student_id):
2519        # Forward only email to landing page in base package.
2520        self.redirect(self.url(self.context, 'requestpw_complete',
2521            data = dict(email=email)))
2522        return
2523
2524    def _pw_used(self):
2525        # XXX: False if password has not been used. We need an extra
2526        #      attribute which remembers if student logged in.
2527        return True
2528
2529    @action(_('Send login credentials to email address'), style='primary')
2530    def get_credentials(self, **data):
2531        if not self.captcha_result.is_valid:
2532            # Captcha will display error messages automatically.
2533            # No need to flash something.
2534            return
2535        number = data.get('number','')
2536        firstname = data.get('firstname','')
2537        cat = getUtility(ICatalog, name='students_catalog')
2538        results = list(
2539            cat.searchResults(reg_number=(number, number)))
2540        if not results:
2541            results = list(
2542                cat.searchResults(matric_number=(number, number)))
2543        if results:
2544            student = results[0]
2545            if getattr(student,'firstname',None) is None:
2546                self.flash(_('An error occurred.'))
2547                return
2548            elif student.firstname.lower() != firstname.lower():
2549                # Don't tell the truth here. Anonymous must not
2550                # know that a record was found and only the firstname
2551                # verification failed.
2552                self.flash(_('No student record found.'))
2553                return
2554            elif student.password is not None and self._pw_used:
2555                self.flash(_('Your password has already been set and used. '
2556                             'Please proceed to the login page.'))
2557                return
2558            # Store email address but nothing else.
2559            student.email = data['email']
2560            notify(grok.ObjectModifiedEvent(student))
2561        else:
2562            # No record found, this is the truth.
2563            self.flash(_('No student record found.'))
2564            return
2565
2566        kofa_utils = getUtility(IKofaUtils)
2567        password = kofa_utils.genPassword()
2568        mandate = PasswordMandate()
2569        mandate.params['password'] = password
2570        mandate.params['user'] = student
2571        site = grok.getSite()
2572        site['mandates'].addMandate(mandate)
2573        # Send email with credentials
2574        args = {'mandate_id':mandate.mandate_id}
2575        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2576        url_info = u'Confirmation link: %s' % mandate_url
2577        msg = _('You have successfully requested a password for the')
2578        if kofa_utils.sendCredentials(IUserAccount(student),
2579            password, url_info, msg):
2580            email_sent = student.email
2581        else:
2582            email_sent = None
2583        self._redirect(email=email_sent, password=password,
2584            student_id=student.student_id)
2585        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2586        self.context.logger.info(
2587            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2588        return
2589
2590class StudentRequestPasswordEmailSent(KofaPage):
2591    """Landing page after successful password request.
2592
2593    """
2594    grok.name('requestpw_complete')
2595    grok.require('waeup.Public')
2596    grok.template('requestpwmailsent')
2597    label = _('Your password request was successful.')
2598
2599    def update(self, email=None, student_id=None, password=None):
2600        self.email = email
2601        self.password = password
2602        self.student_id = student_id
2603        return
2604
2605class FilterStudentsInDepartmentPage(KofaPage):
2606    """Page that filters and lists students.
2607    """
2608    grok.context(IDepartment)
2609    grok.require('waeup.showStudents')
2610    grok.name('students')
2611    grok.template('filterstudentspage')
2612    pnav = 1
2613    session_label = _('Current Session')
2614    level_label = _('Current Level')
2615
2616    def label(self):
2617        return 'Students in %s' % self.context.longtitle()
2618
2619    def _set_session_values(self):
2620        vocab_terms = academic_sessions_vocab.by_value.values()
2621        self.sessions = sorted(
2622            [(x.title, x.token) for x in vocab_terms], reverse=True)
2623        self.sessions += [('All Sessions', 'all')]
2624        return
2625
2626    def _set_level_values(self):
2627        vocab_terms = course_levels.by_value.values()
2628        self.levels = sorted(
2629            [(x.title, x.token) for x in vocab_terms])
2630        self.levels += [('All Levels', 'all')]
2631        return
2632
2633    def _searchCatalog(self, session, level):
2634        if level not in (10, 999, None):
2635            start_level = 100 * (level // 100)
2636            end_level = start_level + 90
2637        else:
2638            start_level = end_level = level
2639        cat = queryUtility(ICatalog, name='students_catalog')
2640        students = cat.searchResults(
2641            current_session=(session, session),
2642            current_level=(start_level, end_level),
2643            depcode=(self.context.code, self.context.code)
2644            )
2645        hitlist = []
2646        for student in students:
2647            hitlist.append(StudentQueryResultItem(student, view=self))
2648        return hitlist
2649
2650    def update(self, SHOW=None, session=None, level=None):
2651        datatable.need()
2652        self.parent_url = self.url(self.context.__parent__)
2653        self._set_session_values()
2654        self._set_level_values()
2655        self.hitlist = []
2656        self.session_default = session
2657        self.level_default = level
2658        if SHOW is not None:
2659            if session != 'all':
2660                self.session = int(session)
2661                self.session_string = '%s %s/%s' % (
2662                    self.session_label, self.session, self.session+1)
2663            else:
2664                self.session = None
2665                self.session_string = _('in any session')
2666            if level != 'all':
2667                self.level = int(level)
2668                self.level_string = '%s %s' % (self.level_label, self.level)
2669            else:
2670                self.level = None
2671                self.level_string = _('at any level')
2672            self.hitlist = self._searchCatalog(self.session, self.level)
2673            if not self.hitlist:
2674                self.flash(_('No student found.'))
2675        return
2676
2677class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2678    """Page that filters and lists students.
2679    """
2680    grok.context(ICertificate)
2681
2682    def label(self):
2683        return 'Students studying %s' % self.context.longtitle()
2684
2685    def _searchCatalog(self, session, level):
2686        if level not in (10, 999, None):
2687            start_level = 100 * (level // 100)
2688            end_level = start_level + 90
2689        else:
2690            start_level = end_level = level
2691        cat = queryUtility(ICatalog, name='students_catalog')
2692        students = cat.searchResults(
2693            current_session=(session, session),
2694            current_level=(start_level, end_level),
2695            certcode=(self.context.code, self.context.code)
2696            )
2697        hitlist = []
2698        for student in students:
2699            hitlist.append(StudentQueryResultItem(student, view=self))
2700        return hitlist
2701
2702class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2703    """Page that filters and lists students.
2704    """
2705    grok.context(ICourse)
2706
2707    def label(self):
2708        return 'Students registered for %s' % self.context.longtitle()
2709
2710    def _searchCatalog(self, session, level):
2711        if level not in (10, 999, None):
2712            start_level = 100 * (level // 100)
2713            end_level = start_level + 90
2714        else:
2715            start_level = end_level = level
2716        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2717        coursetickets = cat.searchResults(
2718            session=(session, session),
2719            level=(start_level, end_level),
2720            code=(self.context.code, self.context.code)
2721            )
2722        hitlist = []
2723        for ticket in coursetickets:
2724            # XXX: If students have registered the same courses twice
2725            # they will be listed twice.
2726            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
2727        return hitlist
2728
2729class ExportJobContainerOverview(KofaPage):
2730    """Page that lists active student data export jobs and provides links
2731    to discard or download CSV files.
2732
2733    """
2734    grok.context(VirtualExportJobContainer)
2735    grok.require('waeup.showStudents')
2736    grok.name('index.html')
2737    grok.template('exportjobsindex')
2738    label = _('Student Data Exports')
2739    pnav = 1
2740
2741    def update(self, CREATE=None, DISCARD=None, job_id=None):
2742        if CREATE:
2743            self.redirect(self.url('@@exportconfig'))
2744            return
2745        if DISCARD and job_id:
2746            entry = self.context.entry_from_job_id(job_id)
2747            self.context.delete_export_entry(entry)
2748            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2749            self.context.logger.info(
2750                '%s - discarded: job_id=%s' % (ob_class, job_id))
2751            self.flash(_('Discarded export') + ' %s' % job_id)
2752        self.entries = doll_up(self, user=self.request.principal.id)
2753        return
2754
2755class ExportJobContainerJobConfig(KofaPage):
2756    """Page that configures a students export job.
2757
2758    This is a baseclass.
2759    """
2760    grok.baseclass()
2761    grok.name('exportconfig')
2762    grok.require('waeup.showStudents')
2763    grok.template('exportconfig')
2764    label = _('Configure student data export')
2765    pnav = 1
2766    redirect_target = ''
2767
2768    def _set_session_values(self):
2769        vocab_terms = academic_sessions_vocab.by_value.values()
2770        self.sessions = sorted(
2771            [(x.title, x.token) for x in vocab_terms], reverse=True)
2772        self.sessions += [(_('All Sessions'), 'all')]
2773        return
2774
2775    def _set_level_values(self):
2776        vocab_terms = course_levels.by_value.values()
2777        self.levels = sorted(
2778            [(x.title, x.token) for x in vocab_terms])
2779        self.levels += [(_('All Levels'), 'all')]
2780        return
2781
2782    def _set_mode_values(self):
2783        utils = getUtility(IKofaUtils)
2784        self.modes = sorted([(value, key) for key, value in
2785                      utils.STUDY_MODES_DICT.items()])
2786        self.modes +=[(_('All Modes'), 'all')]
2787        return
2788
2789    def _set_exporter_values(self):
2790        # We provide all student exporters, nothing else, yet.
2791        exporters = []
2792        for name in EXPORTER_NAMES:
2793            util = getUtility(ICSVExporter, name=name)
2794            exporters.append((util.title, name),)
2795        self.exporters = exporters
2796
2797    @property
2798    def depcode(self):
2799        return None
2800
2801    @property
2802    def certcode(self):
2803        return None
2804
2805    def update(self, START=None, session=None, level=None, mode=None,
2806               exporter=None):
2807        self._set_session_values()
2808        self._set_level_values()
2809        self._set_mode_values()
2810        self._set_exporter_values()
2811        if START is None:
2812            return
2813        if session == 'all':
2814            session=None
2815        if level == 'all':
2816            level = None
2817        if mode == 'all':
2818            mode = None
2819        job_id = self.context.start_export_job(exporter,
2820                                      self.request.principal.id,
2821                                      current_session=session,
2822                                      current_level=level,
2823                                      current_mode=mode,
2824                                      depcode=self.depcode,
2825                                      certcode=self.certcode)
2826        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2827        self.context.logger.info(
2828            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
2829            % (ob_class, exporter, session, level, mode, self.depcode,
2830            self.certcode, job_id))
2831        self.flash(_('Export started for students with') +
2832                   ' current_session=%s, current_level=%s, study_mode=%s' % (
2833                   session, level, mode))
2834        self.redirect(self.url(self.redirect_target))
2835        return
2836
2837class ExportJobContainerDownload(ExportCSVView):
2838    """Page that downloads a students export csv file.
2839
2840    """
2841    grok.context(VirtualExportJobContainer)
2842    grok.require('waeup.showStudents')
2843
2844class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
2845    """Page that configures a students export job in datacenter.
2846
2847    """
2848    grok.context(IDataCenter)
2849    redirect_target = '@@export'
2850
2851class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
2852    """Page that configures a students export job in departments.
2853
2854    """
2855    grok.context(VirtualDepartmentExportJobContainer)
2856
2857    @property
2858    def depcode(self):
2859        return self.context.__parent__.code
2860
2861class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
2862    """Page that configures a students export job for certificates.
2863
2864    """
2865    grok.context(VirtualCertificateExportJobContainer)
2866    grok.template('exportconfig_certificate')
2867
2868    @property
2869    def certcode(self):
2870        return self.context.__parent__.code
2871
2872class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
2873    """Page that configures a students export job for courses.
2874
2875    In contrast to department or certificate student data exports the
2876    coursetickets_catalog is searched here. Therefore the update
2877    method from the base class is customized.
2878    """
2879    grok.context(VirtualCourseExportJobContainer)
2880    grok.template('exportconfig_course')
2881
2882    def _set_exporter_values(self):
2883        # We provide only two exporters.
2884        exporters = []
2885        for name in ('students', 'coursetickets'):
2886            util = getUtility(ICSVExporter, name=name)
2887            exporters.append((util.title, name),)
2888        self.exporters = exporters
2889
2890    def update(self, START=None, session=None, level=None, mode=None,
2891               exporter=None):
2892        self._set_session_values()
2893        self._set_level_values()
2894        self._set_mode_values()
2895        self._set_exporter_values()
2896        if START is None:
2897            return
2898        if session == 'all':
2899            session=None
2900        if level == 'all':
2901            level = None
2902        job_id = self.context.start_export_job(exporter,
2903                                      self.request.principal.id,
2904                                      # Use a different catalog and
2905                                      # pass different keywords than
2906                                      # for the (default) students_catalog
2907                                      catalog='coursetickets',
2908                                      session=session,
2909                                      level=level,
2910                                      code=self.context.__parent__.code)
2911        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2912        self.context.logger.info(
2913            '%s - exported: %s (%s, %s, %s), job_id=%s'
2914            % (ob_class, exporter, session, level,
2915            self.context.__parent__.code, job_id))
2916        self.flash(_('Export started for course tickets with') +
2917                   ' level_session=%s, level=%s' % (
2918                   session, level))
2919        self.redirect(self.url(self.redirect_target))
2920        return
Note: See TracBrowser for help on using the repository browser.