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

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

Provide option to render performance data in custom packages.

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