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

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

Add more tests to see if the export file of the course ticket exporter really contains the data expected.

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