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

Last change on this file since 10458 was 10458, checked in by Henrik Bettermann, 11 years ago

Add pages and buttons for transcript request processing. Work in progress, tests will follow!

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