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

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

Show also level courses on slips.

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