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

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

Add doclinks.

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