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

Last change on this file since 13236 was 13201, checked in by Henrik Bettermann, 9 years ago

Remove duplicate code.

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