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

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

Clean up code. Remove redundant IApplicantBase interface.

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