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

Last change on this file since 13104 was 13103, checked in by Henrik Bettermann, 10 years ago

Remove manual clearance lock. Replace clearance_locked field by
property attribute.

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