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

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

Restrict balance payments to scholl fee balances.

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