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

Last change on this file since 13898 was 13898, checked in by Henrik Bettermann, 8 years ago

Add ExportPDFScoresSlip.

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