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

Last change on this file since 10242 was 10179, checked in by Henrik Bettermann, 12 years ago

Add level column.

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