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

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

Replace course_registration_allowed by course_registration_forbidden method.

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