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

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

Add course_category attribute to course tickets. In the base package,
this attribute is omitted from display and manage pages.

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