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

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

Some universities bypass access code validation. We can now easily configure this by setting with_ac False.

  • Property svn:keywords set to Id
File size: 106.5 KB
RevLine 
[7191]1## $Id: browser.py 9952 2013-02-15 09:21:12Z 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
[9010]694    def _signatures(self):
[9548]695        isStudent = getattr(
696            self.request.principal, 'user_type', None) == 'student'
697        if not isStudent and self.context.state in (CLEARED, ):
[9010]698            return (_('Student Signature'), _('Clearance Officer Signature'))
699        return
700
[9555]701    def _sigsInFooter(self):
[9548]702        isStudent = getattr(
703            self.request.principal, 'user_type', None) == 'student'
704        if not isStudent and self.context.state in (CLEARED, ):
[9555]705            return (_('Date, Student Signature'),
706                    _('Date, Clearance Officer Signature'),
707                    )
[9557]708        return ()
[9548]709
[7277]710    def render(self):
[9141]711        studentview = StudentBasePDFFormPage(self.context.student,
[9375]712            self.request, self.omit_fields)
[7277]713        students_utils = getUtility(IStudentsUtils)
714        return students_utils.renderPDF(
[9452]715            self, 'clearance_slip.pdf',
[9548]716            self.context.student, studentview, signatures=self._signatures(),
[9555]717            sigs_in_footer=self._sigsInFooter())
[7277]718
[7819]719class StudentClearanceManageFormPage(KofaEditFormPage):
[8120]720    """ Page to manage student clearance data
[6631]721    """
722    grok.context(IStudent)
[8119]723    grok.name('manage_clearance')
[7136]724    grok.require('waeup.manageStudent')
[7134]725    grok.template('clearanceeditpage')
[7723]726    label = _('Manage clearance data')
[6642]727    pnav = 4
[6650]728
[7993]729    @property
[8099]730    def separators(self):
731        return getUtility(IStudentsUtils).SEPARATORS_DICT
732
733    @property
[7993]734    def form_fields(self):
[8472]735        if self.context.is_postgrad:
[8977]736            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
[7993]737        else:
[8977]738            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
[7993]739        return form_fields
740
[6650]741    def update(self):
742        datepicker.need() # Enable jQuery datepicker in date fields.
[7134]743        tabs.need()
[7490]744        self.tab1 = self.tab2 = ''
745        qs = self.request.get('QUERY_STRING', '')
746        if not qs:
747            qs = 'tab1'
748        setattr(self, qs, 'active')
[6650]749        return super(StudentClearanceManageFormPage, self).update()
750
[7723]751    @action(_('Save'), style='primary')
[6695]752    def save(self, **data):
[6762]753        msave(self, **data)
[6695]754        return
755
[7459]756class StudentClearPage(UtilityView, grok.View):
[7158]757    """ Clear student by clearance officer
758    """
759    grok.context(IStudent)
760    grok.name('clear')
761    grok.require('waeup.clearStudent')
762
763    def update(self):
[9814]764        if clearance_disabled_message(self.context):
765            self.flash(clearance_disabled_message(self.context))
766            self.redirect(self.url(self.context,'view_clearance'))
767            return
[7158]768        if self.context.state == REQUESTED:
769            IWorkflowInfo(self.context).fireTransition('clear')
[7723]770            self.flash(_('Student has been cleared.'))
[7158]771        else:
[7723]772            self.flash(_('Student is in wrong state.'))
[7158]773        self.redirect(self.url(self.context,'view_clearance'))
774        return
775
776    def render(self):
777        return
778
[9484]779class StudentRejectClearancePage(KofaEditFormPage):
[7158]780    """ Reject clearance by clearance officers
781    """
782    grok.context(IStudent)
783    grok.name('reject_clearance')
[9484]784    label = _('Reject clearance')
[7158]785    grok.require('waeup.clearStudent')
[9484]786    form_fields = grok.AutoFields(
[9486]787        IUGStudentClearance).select('officer_comment')
[7158]788
[9814]789    def update(self):
790        if clearance_disabled_message(self.context):
791            self.flash(clearance_disabled_message(self.context))
792            self.redirect(self.url(self.context,'view_clearance'))
793            return
794        return super(StudentRejectClearancePage, self).update()
795
[9484]796    @action(_('Save comment and reject clearance now'), style='primary')
797    def reject(self, **data):
[7158]798        if self.context.state == CLEARED:
799            IWorkflowInfo(self.context).fireTransition('reset4')
[7723]800            message = _('Clearance has been annulled.')
[7275]801            self.flash(message)
[7158]802        elif self.context.state == REQUESTED:
803            IWorkflowInfo(self.context).fireTransition('reset3')
[7723]804            message = _('Clearance request has been rejected.')
[7275]805            self.flash(message)
[7158]806        else:
[7723]807            self.flash(_('Student is in wrong state.'))
[7334]808            self.redirect(self.url(self.context,'view_clearance'))
[7275]809            return
[9484]810        self.applyData(self.context, **data)
[9486]811        comment = data['officer_comment']
[9556]812        if comment:
813            self.context.writeLogMessage(
814                self, 'comment: %s' % comment.replace('\n', '<br>'))
815            args = {'subject':message, 'body':comment}
816        else:
817            args = {'subject':message,}
[7275]818        self.redirect(self.url(self.context) +
819            '/contactstudent?%s' % urlencode(args))
[7158]820        return
821
822
[7819]823class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
[6631]824    """ Page to display student personal data
825    """
826    grok.context(IStudent)
827    grok.name('view_personal')
[6660]828    grok.require('waeup.viewStudent')
[6631]829    form_fields = grok.AutoFields(IStudentPersonal)
[7386]830    form_fields['perm_address'].custom_widget = BytesDisplayWidget
[9543]831    form_fields[
832        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6642]833    pnav = 4
[6631]834
835    @property
836    def label(self):
[7723]837        return _('${a}: Personal Data',
838            mapping = {'a':self.context.display_fullname})
[6631]839
[8903]840class StudentPersonalManageFormPage(KofaEditFormPage):
841    """ Page to manage personal data
[6631]842    """
843    grok.context(IStudent)
[8903]844    grok.name('manage_personal')
845    grok.require('waeup.manageStudent')
[9553]846    form_fields = grok.AutoFields(IStudentPersonal)
847    form_fields['personal_updated'].for_display = True
[9571]848    form_fields[
849        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[8903]850    label = _('Manage personal data')
[6642]851    pnav = 4
[6631]852
[7723]853    @action(_('Save'), style='primary')
[6762]854    def save(self, **data):
855        msave(self, **data)
856        return
857
[9543]858class StudentPersonalEditFormPage(KofaEditFormPage):
[8903]859    """ Page to edit personal data
860    """
[9543]861    grok.context(IStudent)
[8903]862    grok.name('edit_personal')
863    grok.require('waeup.handleStudent')
[9563]864    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
[8903]865    label = _('Edit personal data')
866    pnav = 4
867
[9543]868    @action(_('Save/Confirm'), style='primary')
869    def save(self, **data):
870        msave(self, **data)
[9569]871        self.context.personal_updated = datetime.utcnow()
[9543]872        return
873
[7819]874class StudyCourseDisplayFormPage(KofaDisplayFormPage):
[6635]875    """ Page to display the student study course data
876    """
877    grok.context(IStudentStudyCourse)
878    grok.name('index')
[6660]879    grok.require('waeup.viewStudent')
[6775]880    grok.template('studycoursepage')
[6642]881    pnav = 4
[6635]882
883    @property
[8972]884    def form_fields(self):
885        if self.context.is_postgrad:
886            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
[9723]887                'previous_verdict')
[8972]888        else:
889            form_fields = grok.AutoFields(IStudentStudyCourse)
890        return form_fields
891
892    @property
[6635]893    def label(self):
[9140]894        if self.context.is_current:
895            return _('${a}: Study Course',
896                mapping = {'a':self.context.__parent__.display_fullname})
897        else:
898            return _('${a}: Previous Study Course',
899                mapping = {'a':self.context.__parent__.display_fullname})
[6635]900
[6912]901    @property
902    def current_mode(self):
[7641]903        if self.context.certificate is not None:
[7841]904            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
[7681]905            return studymodes_dict[self.context.certificate.study_mode]
[7171]906        return
[7642]907
[7171]908    @property
909    def department(self):
[7205]910        if self.context.certificate is not None:
[7171]911            return self.context.certificate.__parent__.__parent__
912        return
[6912]913
[7171]914    @property
915    def faculty(self):
[7205]916        if self.context.certificate is not None:
[7171]917            return self.context.certificate.__parent__.__parent__.__parent__
918        return
919
[9140]920    @property
921    def prev_studycourses(self):
922        if self.context.is_current:
923            if self.context.__parent__.get('studycourse_2', None) is not None:
924                return (
925                        {'href':self.url(self.context.student) + '/studycourse_1',
926                        'title':_('First Study Course, ')},
927                        {'href':self.url(self.context.student) + '/studycourse_2',
928                        'title':_('Second Study Course')}
929                        )
930            if self.context.__parent__.get('studycourse_1', None) is not None:
931                return (
932                        {'href':self.url(self.context.student) + '/studycourse_1',
933                        'title':_('First Study Course')},
934                        )
935        return
936
[7819]937class StudyCourseManageFormPage(KofaEditFormPage):
[6649]938    """ Page to edit the student study course data
939    """
940    grok.context(IStudentStudyCourse)
[6775]941    grok.name('manage')
[7136]942    grok.require('waeup.manageStudent')
[6775]943    grok.template('studycoursemanagepage')
[7723]944    label = _('Manage study course')
[6649]945    pnav = 4
[7723]946    taboneactions = [_('Save'),_('Cancel')]
947    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
948    tabthreeactions = [_('Add study level')]
[6649]949
[8972]950    @property
951    def form_fields(self):
952        if self.context.is_postgrad:
953            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
[9723]954                'previous_verdict')
[8972]955        else:
956            form_fields = grok.AutoFields(IStudentStudyCourse)
957        return form_fields
958
[6775]959    def update(self):
[9139]960        if not self.context.is_current:
961            emit_lock_message(self)
962            return
[6775]963        super(StudyCourseManageFormPage, self).update()
964        tabs.need()
[7484]965        self.tab1 = self.tab2 = ''
966        qs = self.request.get('QUERY_STRING', '')
967        if not qs:
968            qs = 'tab1'
969        setattr(self, qs, 'active')
[7490]970        warning.need()
971        datatable.need()
972        return
[6775]973
[7723]974    @action(_('Save'), style='primary')
[6761]975    def save(self, **data):
[8099]976        try:
977            msave(self, **data)
978        except ConstraintNotSatisfied:
979            # The selected level might not exist in certificate
980            self.flash(_('Current level not available for certificate.'))
981            return
[8081]982        notify(grok.ObjectModifiedEvent(self.context.__parent__))
[6761]983        return
984
[6775]985    @property
986    def level_dict(self):
987        studylevelsource = StudyLevelSource().factory
988        for code in studylevelsource.getValues(self.context):
989            title = studylevelsource.getTitle(self.context, code)
990            yield(dict(code=code, title=title))
991
[9437]992    @property
993    def session_dict(self):
994        yield(dict(code='', title='--'))
995        for item in academic_sessions():
996            code = item[1]
997            title = item[0]
998            yield(dict(code=code, title=title))
999
[7723]1000    @action(_('Add study level'))
[6774]1001    def addStudyLevel(self, **data):
[6775]1002        level_code = self.request.form.get('addlevel', None)
[9437]1003        level_session = self.request.form.get('level_session', None)
1004        if not level_session:
1005            self.flash(_('You must select a session for the level.'))
1006            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1007            return
[8323]1008        studylevel = createObject(u'waeup.StudentStudyLevel')
[6775]1009        studylevel.level = int(level_code)
[9437]1010        studylevel.level_session = int(level_session)
[6775]1011        try:
[6782]1012            self.context.addStudentStudyLevel(
1013                self.context.certificate,studylevel)
[7723]1014            self.flash(_('Study level has been added.'))
[6775]1015        except KeyError:
[7723]1016            self.flash(_('This level exists.'))
[7484]1017        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6774]1018        return
1019
[7723]1020    @jsaction(_('Remove selected levels'))
[6775]1021    def delStudyLevels(self, **data):
1022        form = self.request.form
[9701]1023        if 'val_id' in form:
[6775]1024            child_id = form['val_id']
1025        else:
[7723]1026            self.flash(_('No study level selected.'))
[7484]1027            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6775]1028            return
1029        if not isinstance(child_id, list):
1030            child_id = [child_id]
1031        deleted = []
1032        for id in child_id:
[7723]1033            del self.context[id]
1034            deleted.append(id)
[6775]1035        if len(deleted):
[7723]1036            self.flash(_('Successfully removed: ${a}',
1037                mapping = {'a':', '.join(deleted)}))
[9332]1038            self.context.writeLogMessage(
1039                self,'removed: %s' % ', '.join(deleted))
[7484]1040        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6775]1041        return
[6774]1042
[9138]1043class StudentTransferFormPage(KofaAddFormPage):
1044    """Page to transfer the student.
1045    """
1046    grok.context(IStudent)
1047    grok.name('transfer')
1048    grok.require('waeup.manageStudent')
1049    label = _('Transfer student')
1050    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1051        'entry_mode', 'entry_session')
1052    pnav = 4
1053
1054    def update(self):
1055        super(StudentTransferFormPage, self).update()
1056        warning.need()
1057        return
1058
1059    @jsaction(_('Transfer'))
1060    def transferStudent(self, **data):
1061        error = self.context.transfer(**data)
1062        if error == -1:
1063            self.flash(_('Current level does not match certificate levels.'))
1064        elif error == -2:
1065            self.flash(_('Former study course record incomplete.'))
1066        elif error == -3:
1067            self.flash(_('Maximum number of transfers exceeded.'))
1068        else:
1069            self.flash(_('Successfully transferred.'))
1070        return
1071
[7819]1072class StudyLevelDisplayFormPage(KofaDisplayFormPage):
[6774]1073    """ Page to display student study levels
1074    """
1075    grok.context(IStudentStudyLevel)
1076    grok.name('index')
1077    grok.require('waeup.viewStudent')
[6775]1078    form_fields = grok.AutoFields(IStudentStudyLevel)
[9161]1079    form_fields[
1080        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6783]1081    grok.template('studylevelpage')
[6774]1082    pnav = 4
1083
[7310]1084    def update(self):
1085        super(StudyLevelDisplayFormPage, self).update()
1086        datatable.need()
1087        return
1088
[6774]1089    @property
[8141]1090    def translated_values(self):
[8921]1091        return translated_values(self)
[8141]1092
1093    @property
[6774]1094    def label(self):
[7833]1095        # Here we know that the cookie has been set
1096        lang = self.request.cookies.get('kofa.language')
[7811]1097        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]1098            target_language=lang)
1099        return _('${a}: Study Level ${b}', mapping = {
[8736]1100            'a':self.context.student.display_fullname,
[7723]1101            'b':level_title})
[6774]1102
[7459]1103class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
[7028]1104    """Deliver a PDF slip of the context.
1105    """
1106    grok.context(IStudentStudyLevel)
[9452]1107    grok.name('course_registration_slip.pdf')
[7028]1108    grok.require('waeup.viewStudent')
1109    form_fields = grok.AutoFields(IStudentStudyLevel)
[9683]1110    form_fields[
1111        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7028]1112    prefix = 'form'
[9702]1113    omit_fields = (
1114        'password', 'suspended', 'phone',
1115        'adm_code', 'sex', 'suspended_comment')
[7028]1116
1117    @property
[7723]1118    def title(self):
[7819]1119        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]1120        return translate(_('Level Data'), 'waeup.kofa',
[7723]1121            target_language=portal_language)
1122
1123    @property
[9906]1124    def content_title_1(self):
[7819]1125        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9906]1126        return translate(_('1st Semester Courses'), 'waeup.kofa',
[7723]1127            target_language=portal_language)
1128
1129    @property
[9906]1130    def content_title_2(self):
1131        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1132        return translate(_('2nd Semester Courses'), 'waeup.kofa',
1133            target_language=portal_language)
1134
1135    @property
[7028]1136    def label(self):
[7819]1137        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]1138        lang = self.request.cookies.get('kofa.language', portal_language)
1139        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]1140            target_language=lang)
[8141]1141        return translate(_('Course Registration Slip'),
[7811]1142            'waeup.kofa', target_language=portal_language) \
[7723]1143            + ' %s' % level_title
[7028]1144
1145    def render(self):
[7819]1146        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]1147        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1148        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1149        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1150        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1151        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1152        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
[9906]1153        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
[7811]1154        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
[9810]1155        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
[9141]1156        studentview = StudentBasePDFFormPage(self.context.student,
[9375]1157            self.request, self.omit_fields)
[7150]1158        students_utils = getUtility(IStudentsUtils)
[9906]1159        tabledata_1 = sorted(
1160            [value for value in self.context.values() if value.semester == 1],
[7318]1161            key=lambda value: str(value.semester) + value.code)
[9906]1162        tabledata_2 = sorted(
1163            [value for value in self.context.values() if value.semester == 2],
1164            key=lambda value: str(value.semester) + value.code)
1165        tableheader = [(Code,'code', 2.5),
[7723]1166                         (Title,'title', 5),
1167                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1168                         (Cred, 'credits', 1.5),
[9906]1169                         #(Mand, 'mandatory', 1.5),
[8141]1170                         (Score, 'score', 1.5),
[9810]1171                         (Grade, 'grade', 1.5),
[8141]1172                         #('Auto', 'automatic', 1.5)
[9906]1173                         ]
1174        return students_utils.renderPDF(
1175            self, 'course_registration_slip.pdf',
1176            self.context.student, studentview,
1177            tableheader_1=tableheader,
1178            tabledata_1=tabledata_1,
1179            tableheader_2=tableheader,
1180            tabledata_2=tabledata_2
1181            )
[7028]1182
[7819]1183class StudyLevelManageFormPage(KofaEditFormPage):
[6792]1184    """ Page to edit the student study level data
1185    """
1186    grok.context(IStudentStudyLevel)
1187    grok.name('manage')
[7136]1188    grok.require('waeup.manageStudent')
[6792]1189    grok.template('studylevelmanagepage')
[9161]1190    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
[9690]1191        'validation_date', 'validated_by', 'total_credits', 'gpa')
[6792]1192    pnav = 4
[7723]1193    taboneactions = [_('Save'),_('Cancel')]
1194    tabtwoactions = [_('Add course ticket'),
1195        _('Remove selected tickets'),_('Cancel')]
[6792]1196
[9895]1197    def update(self, ADD=None, course=None):
[9139]1198        if not self.context.__parent__.is_current:
1199            emit_lock_message(self)
1200            return
[6792]1201        super(StudyLevelManageFormPage, self).update()
1202        tabs.need()
[7484]1203        self.tab1 = self.tab2 = ''
1204        qs = self.request.get('QUERY_STRING', '')
1205        if not qs:
1206            qs = 'tab1'
1207        setattr(self, qs, 'active')
[7490]1208        warning.need()
1209        datatable.need()
[9895]1210        if ADD is not None:
1211            if not course:
1212                self.flash(_('No valid course code entered.'))
1213                self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1214                return
1215            cat = queryUtility(ICatalog, name='courses_catalog')
1216            result = cat.searchResults(code=(course, course))
1217            if len(result) != 1:
1218                self.flash(_('Course not found.'))
1219            else:
1220                course = list(result)[0]
1221                addCourseTicket(self, course)
1222            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6792]1223        return
1224
1225    @property
[8921]1226    def translated_values(self):
1227        return translated_values(self)
1228
1229    @property
[6792]1230    def label(self):
[7833]1231        # Here we know that the cookie has been set
1232        lang = self.request.cookies.get('kofa.language')
[7811]1233        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]1234            target_language=lang)
1235        return _('Manage study level ${a}',
1236            mapping = {'a':level_title})
[6792]1237
[7723]1238    @action(_('Save'), style='primary')
[6792]1239    def save(self, **data):
1240        msave(self, **data)
1241        return
1242
[7723]1243    @jsaction(_('Remove selected tickets'))
[6792]1244    def delCourseTicket(self, **data):
1245        form = self.request.form
[9701]1246        if 'val_id' in form:
[6792]1247            child_id = form['val_id']
1248        else:
[7723]1249            self.flash(_('No ticket selected.'))
[7484]1250            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6792]1251            return
1252        if not isinstance(child_id, list):
1253            child_id = [child_id]
1254        deleted = []
1255        for id in child_id:
[7723]1256            del self.context[id]
1257            deleted.append(id)
[6792]1258        if len(deleted):
[7723]1259            self.flash(_('Successfully removed: ${a}',
1260                mapping = {'a':', '.join(deleted)}))
[9332]1261            self.context.writeLogMessage(
[9924]1262                self,'removed: %s at %s' %
1263                (', '.join(deleted), self.context.level))
[7484]1264        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6792]1265        return
1266
[7459]1267class ValidateCoursesPage(UtilityView, grok.View):
[7334]1268    """ Validate course list by course adviser
1269    """
1270    grok.context(IStudentStudyLevel)
1271    grok.name('validate_courses')
1272    grok.require('waeup.validateStudent')
1273
1274    def update(self):
[9139]1275        if not self.context.__parent__.is_current:
1276            emit_lock_message(self)
1277            return
[7334]1278        if str(self.context.__parent__.current_level) != self.context.__name__:
[7723]1279            self.flash(_('This level does not correspond current level.'))
[8736]1280        elif self.context.student.state == REGISTERED:
1281            IWorkflowInfo(self.context.student).fireTransition(
[7642]1282                'validate_courses')
[7723]1283            self.flash(_('Course list has been validated.'))
[7334]1284        else:
[7723]1285            self.flash(_('Student is in the wrong state.'))
[7334]1286        self.redirect(self.url(self.context))
1287        return
1288
1289    def render(self):
1290        return
1291
[7459]1292class RejectCoursesPage(UtilityView, grok.View):
[7334]1293    """ Reject course list by course adviser
1294    """
1295    grok.context(IStudentStudyLevel)
1296    grok.name('reject_courses')
1297    grok.require('waeup.validateStudent')
1298
1299    def update(self):
[9139]1300        if not self.context.__parent__.is_current:
1301            emit_lock_message(self)
1302            return
[7334]1303        if str(self.context.__parent__.current_level) != self.context.__name__:
[7723]1304            self.flash(_('This level does not correspond current level.'))
[7334]1305            self.redirect(self.url(self.context))
1306            return
[8736]1307        elif self.context.student.state == VALIDATED:
1308            IWorkflowInfo(self.context.student).fireTransition('reset8')
[7723]1309            message = _('Course list request has been annulled.')
[7334]1310            self.flash(message)
[8736]1311        elif self.context.student.state == REGISTERED:
1312            IWorkflowInfo(self.context.student).fireTransition('reset7')
[7723]1313            message = _('Course list request has been rejected:')
[7334]1314            self.flash(message)
1315        else:
[7723]1316            self.flash(_('Student is in the wrong state.'))
[7334]1317            self.redirect(self.url(self.context))
1318            return
1319        args = {'subject':message}
[8736]1320        self.redirect(self.url(self.context.student) +
[7334]1321            '/contactstudent?%s' % urlencode(args))
1322        return
1323
1324    def render(self):
1325        return
1326
[7819]1327class CourseTicketAddFormPage(KofaAddFormPage):
[6808]1328    """Add a course ticket.
[6795]1329    """
1330    grok.context(IStudentStudyLevel)
1331    grok.name('add')
[7136]1332    grok.require('waeup.manageStudent')
[7723]1333    label = _('Add course ticket')
[9420]1334    form_fields = grok.AutoFields(ICourseTicketAdd)
[6795]1335    pnav = 4
1336
[9139]1337    def update(self):
1338        if not self.context.__parent__.is_current:
1339            emit_lock_message(self)
1340            return
1341        super(CourseTicketAddFormPage, self).update()
1342        return
1343
[7723]1344    @action(_('Add course ticket'))
[6795]1345    def addCourseTicket(self, **data):
1346        course = data['course']
[9895]1347        success = addCourseTicket(self, course)
1348        if success:
1349            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6795]1350        return
1351
[7834]1352    @action(_('Cancel'), validator=NullValidator)
[6795]1353    def cancel(self, **data):
1354        self.redirect(self.url(self.context))
1355
[7819]1356class CourseTicketDisplayFormPage(KofaDisplayFormPage):
[6796]1357    """ Page to display course tickets
1358    """
1359    grok.context(ICourseTicket)
1360    grok.name('index')
1361    grok.require('waeup.viewStudent')
1362    form_fields = grok.AutoFields(ICourseTicket)
[9684]1363    grok.template('courseticketpage')
[6796]1364    pnav = 4
1365
1366    @property
1367    def label(self):
[7723]1368        return _('${a}: Course Ticket ${b}', mapping = {
[8736]1369            'a':self.context.student.display_fullname,
[7723]1370            'b':self.context.code})
[6796]1371
[7819]1372class CourseTicketManageFormPage(KofaEditFormPage):
[6796]1373    """ Page to manage course tickets
1374    """
1375    grok.context(ICourseTicket)
1376    grok.name('manage')
[7136]1377    grok.require('waeup.manageStudent')
[9420]1378    form_fields = grok.AutoFields(ICourseTicket)
1379    form_fields['title'].for_display = True
1380    form_fields['fcode'].for_display = True
1381    form_fields['dcode'].for_display = True
1382    form_fields['semester'].for_display = True
1383    form_fields['passmark'].for_display = True
1384    form_fields['credits'].for_display = True
[9698]1385    form_fields['mandatory'].for_display = False
[9420]1386    form_fields['automatic'].for_display = True
[9422]1387    form_fields['carry_over'].for_display = True
[6796]1388    pnav = 4
[9697]1389    grok.template('courseticketmanagepage')
[6796]1390
1391    @property
1392    def label(self):
[7723]1393        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
[6796]1394
[7459]1395    @action('Save', style='primary')
[6796]1396    def save(self, **data):
1397        msave(self, **data)
1398        return
1399
[7819]1400class PaymentsManageFormPage(KofaEditFormPage):
[6869]1401    """ Page to manage the student payments
[7642]1402
1403    This manage form page is for both students and students officers.
[6869]1404    """
1405    grok.context(IStudentPaymentsContainer)
[6940]1406    grok.name('index')
[7181]1407    grok.require('waeup.payStudent')
[6869]1408    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1409    grok.template('paymentsmanagepage')
1410    pnav = 4
1411
[6940]1412    def unremovable(self, ticket):
[7251]1413        usertype = getattr(self.request.principal, 'user_type', None)
1414        if not usertype:
1415            return False
1416        return (self.request.principal.user_type == 'student' and ticket.r_code)
[6940]1417
[6869]1418    @property
1419    def label(self):
[7723]1420        return _('${a}: Payments',
1421            mapping = {'a':self.context.__parent__.display_fullname})
[6869]1422
1423    def update(self):
1424        super(PaymentsManageFormPage, self).update()
1425        datatable.need()
[7329]1426        warning.need()
[6869]1427        return
1428
[7723]1429    @jsaction(_('Remove selected tickets'))
[6869]1430    def delPaymentTicket(self, **data):
1431        form = self.request.form
[9701]1432        if 'val_id' in form:
[6869]1433            child_id = form['val_id']
1434        else:
[7723]1435            self.flash(_('No payment selected.'))
[6940]1436            self.redirect(self.url(self.context))
[6869]1437            return
1438        if not isinstance(child_id, list):
1439            child_id = [child_id]
1440        deleted = []
1441        for id in child_id:
[6992]1442            # Students are not allowed to remove used payment tickets
[6940]1443            if not self.unremovable(self.context[id]):
[7723]1444                del self.context[id]
1445                deleted.append(id)
[6869]1446        if len(deleted):
[7723]1447            self.flash(_('Successfully removed: ${a}',
1448                mapping = {'a': ', '.join(deleted)}))
[8735]1449            self.context.writeLogMessage(
[8885]1450                self,'removed: %s' % ', '.join(deleted))
[6940]1451        self.redirect(self.url(self.context))
[6869]1452        return
1453
[9517]1454    #@action(_('Add online payment ticket'))
1455    #def addPaymentTicket(self, **data):
1456    #    self.redirect(self.url(self.context, '@@addop'))
[6869]1457
[7819]1458class OnlinePaymentAddFormPage(KofaAddFormPage):
[6869]1459    """ Page to add an online payment ticket
1460    """
1461    grok.context(IStudentPaymentsContainer)
1462    grok.name('addop')
[9729]1463    grok.template('onlinepaymentaddform')
[7181]1464    grok.require('waeup.payStudent')
[6877]1465    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
[6869]1466        'p_category')
[7723]1467    label = _('Add online payment')
[6869]1468    pnav = 4
[7642]1469
[9729]1470    @property
1471    def selectable_categories(self):
1472        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1473        return sorted(categories.items())
1474
[7723]1475    @action(_('Create ticket'), style='primary')
[6869]1476    def createTicket(self, **data):
[7024]1477        p_category = data['p_category']
[9148]1478        previous_session = data.get('p_session', None)
1479        previous_level = data.get('p_level', None)
[7024]1480        student = self.context.__parent__
1481        if p_category == 'bed_allocation' and student[
1482            'studycourse'].current_session != grok.getSite()[
[8685]1483            'hostels'].accommodation_session:
[7024]1484                self.flash(
[7723]1485                    _('Your current session does not match ' + \
1486                    'accommodation session.'))
[7024]1487                return
[9423]1488        if 'maintenance' in p_category:
1489            current_session = str(student['studycourse'].current_session)
[9701]1490            if not current_session in student['accommodation']:
[9424]1491                self.flash(_('You have not yet booked accommodation.'))
[9423]1492                return
[7150]1493        students_utils = getUtility(IStudentsUtils)
[9148]1494        error, payment = students_utils.setPaymentDetails(
1495            p_category, student, previous_session, previous_level)
[8595]1496        if error is not None:
1497            self.flash(error)
[8081]1498            return
[6869]1499        self.context[payment.p_id] = payment
[7723]1500        self.flash(_('Payment ticket created.'))
[6940]1501        self.redirect(self.url(self.context))
[6869]1502        return
1503
[9383]1504    @action(_('Cancel'), validator=NullValidator)
1505    def cancel(self, **data):
1506        self.redirect(self.url(self.context))
1507
[9862]1508class PreviousPaymentAddFormPage(KofaAddFormPage):
[9148]1509    """ Page to add an online payment ticket for previous sessions
1510    """
1511    grok.context(IStudentPaymentsContainer)
1512    grok.name('addpp')
1513    grok.require('waeup.payStudent')
[9864]1514    form_fields = grok.AutoFields(IStudentPreviousPayment)
[9148]1515    label = _('Add previous session online payment')
1516    pnav = 4
1517
[9517]1518    def update(self):
[9521]1519        if self.context.student.before_payment:
1520            self.flash(_("No previous payment to be made."))
[9517]1521            self.redirect(self.url(self.context))
1522        super(PreviousPaymentAddFormPage, self).update()
1523        return
1524
[9862]1525    @action(_('Create ticket'), style='primary')
1526    def createTicket(self, **data):
1527        p_category = data['p_category']
1528        previous_session = data.get('p_session', None)
1529        previous_level = data.get('p_level', None)
1530        student = self.context.__parent__
1531        students_utils = getUtility(IStudentsUtils)
1532        error, payment = students_utils.setPaymentDetails(
1533            p_category, student, previous_session, previous_level)
1534        if error is not None:
1535            self.flash(error)
1536            return
1537        self.context[payment.p_id] = payment
1538        self.flash(_('Payment ticket created.'))
1539        self.redirect(self.url(self.context))
1540        return
1541
1542    @action(_('Cancel'), validator=NullValidator)
1543    def cancel(self, **data):
1544        self.redirect(self.url(self.context))
1545
[9864]1546class BalancePaymentAddFormPage(KofaAddFormPage):
1547    """ Page to add an online payment ticket for balance sessions
1548    """
1549    grok.context(IStudentPaymentsContainer)
1550    grok.name('addbp')
[9938]1551    grok.require('waeup.manageStudent')
[9864]1552    form_fields = grok.AutoFields(IStudentBalancePayment)
1553    label = _('Add balance')
1554    pnav = 4
1555
1556    @action(_('Create ticket'), style='primary')
1557    def createTicket(self, **data):
[9868]1558        p_category = data['p_category']
[9864]1559        balance_session = data.get('balance_session', None)
1560        balance_level = data.get('balance_level', None)
1561        balance_amount = data.get('balance_amount', None)
1562        student = self.context.__parent__
1563        students_utils = getUtility(IStudentsUtils)
1564        error, payment = students_utils.setBalanceDetails(
[9868]1565            p_category, student, balance_session,
[9864]1566            balance_level, balance_amount)
1567        if error is not None:
1568            self.flash(error)
1569            return
1570        self.context[payment.p_id] = payment
1571        self.flash(_('Payment ticket created.'))
1572        self.redirect(self.url(self.context))
1573        return
1574
1575    @action(_('Cancel'), validator=NullValidator)
1576    def cancel(self, **data):
1577        self.redirect(self.url(self.context))
1578
[7819]1579class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
[6869]1580    """ Page to view an online payment ticket
1581    """
[6877]1582    grok.context(IStudentOnlinePayment)
[6869]1583    grok.name('index')
1584    grok.require('waeup.viewStudent')
[6877]1585    form_fields = grok.AutoFields(IStudentOnlinePayment)
[8170]1586    form_fields[
1587        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1588    form_fields[
1589        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6869]1590    pnav = 4
1591
1592    @property
1593    def label(self):
[7723]1594        return _('${a}: Online Payment Ticket ${b}', mapping = {
[8736]1595            'a':self.context.student.display_fullname,
[7723]1596            'b':self.context.p_id})
[6869]1597
[8420]1598class OnlinePaymentApprovePage(UtilityView, grok.View):
[6930]1599    """ Callback view
1600    """
1601    grok.context(IStudentOnlinePayment)
[8420]1602    grok.name('approve')
1603    grok.require('waeup.managePortal')
[6930]1604
1605    def update(self):
[8428]1606        success, msg, log = self.context.approveStudentPayment()
1607        if log is not None:
[9770]1608            # Add log message to students.log
[8735]1609            self.context.writeLogMessage(self,log)
[9770]1610            # Add log message to payments.log
1611            self.context.logger.info(
[9779]1612                '%s,%s,%s,%s,%s,,,,,,' % (
[9770]1613                self.context.student.student_id,
1614                self.context.p_id, self.context.p_category,
1615                self.context.amount_auth, self.context.r_code))
[8420]1616        self.flash(msg)
[6940]1617        return
[6930]1618
1619    def render(self):
[6940]1620        self.redirect(self.url(self.context, '@@index'))
[6930]1621        return
1622
[8420]1623class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1624    """ Approval view for students.
1625
1626    This view is used for browser tests only and
1627    must be neutralized in custom pages!
1628    """
1629
1630    grok.name('fake_approve')
1631    grok.require('waeup.payStudent')
1632
[7459]1633class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7019]1634    """Deliver a PDF slip of the context.
1635    """
1636    grok.context(IStudentOnlinePayment)
[8262]1637    grok.name('payment_slip.pdf')
[7019]1638    grok.require('waeup.viewStudent')
1639    form_fields = grok.AutoFields(IStudentOnlinePayment)
[8173]1640    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1641    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7019]1642    prefix = 'form'
[8258]1643    note = None
[9702]1644    omit_fields = (
1645        'password', 'suspended', 'phone',
1646        'adm_code', 'sex', 'suspended_comment')
[7019]1647
1648    @property
[8262]1649    def title(self):
1650        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1651        return translate(_('Payment Data'), 'waeup.kofa',
1652            target_language=portal_language)
1653
1654    @property
[7019]1655    def label(self):
[8262]1656        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1657        return translate(_('Online Payment Slip'),
1658            'waeup.kofa', target_language=portal_language) \
1659            + ' %s' % self.context.p_id
[7019]1660
1661    def render(self):
[8262]1662        #if self.context.p_state != 'paid':
1663        #    self.flash('Ticket not yet paid.')
1664        #    self.redirect(self.url(self.context))
1665        #    return
[9141]1666        studentview = StudentBasePDFFormPage(self.context.student,
[9375]1667            self.request, self.omit_fields)
[7150]1668        students_utils = getUtility(IStudentsUtils)
[8262]1669        return students_utils.renderPDF(self, 'payment_slip.pdf',
[8736]1670            self.context.student, studentview, note=self.note)
[7019]1671
[6992]1672
[7819]1673class AccommodationManageFormPage(KofaEditFormPage):
[7009]1674    """ Page to manage bed tickets.
[7642]1675
1676    This manage form page is for both students and students officers.
[6635]1677    """
1678    grok.context(IStudentAccommodation)
1679    grok.name('index')
[7181]1680    grok.require('waeup.handleAccommodation')
[6635]1681    form_fields = grok.AutoFields(IStudentAccommodation)
[6992]1682    grok.template('accommodationmanagepage')
[6642]1683    pnav = 4
[7723]1684    officers_only_actions = [_('Remove selected')]
[6635]1685
1686    @property
1687    def label(self):
[7723]1688        return _('${a}: Accommodation',
1689            mapping = {'a':self.context.__parent__.display_fullname})
[6637]1690
[6992]1691    def update(self):
1692        super(AccommodationManageFormPage, self).update()
1693        datatable.need()
[7329]1694        warning.need()
[6992]1695        return
1696
[7723]1697    @jsaction(_('Remove selected'))
[7009]1698    def delBedTickets(self, **data):
[7240]1699        if getattr(self.request.principal, 'user_type', None) == 'student':
[7723]1700            self.flash(_('You are not allowed to remove bed tickets.'))
[7017]1701            self.redirect(self.url(self.context))
1702            return
[6992]1703        form = self.request.form
[9701]1704        if 'val_id' in form:
[6992]1705            child_id = form['val_id']
1706        else:
[7723]1707            self.flash(_('No bed ticket selected.'))
[6992]1708            self.redirect(self.url(self.context))
1709            return
1710        if not isinstance(child_id, list):
1711            child_id = [child_id]
1712        deleted = []
1713        for id in child_id:
[7068]1714            del self.context[id]
1715            deleted.append(id)
[6992]1716        if len(deleted):
[7723]1717            self.flash(_('Successfully removed: ${a}',
1718                mapping = {'a':', '.join(deleted)}))
[8735]1719            self.context.writeLogMessage(
1720                self,'removed: % s' % ', '.join(deleted))
[6992]1721        self.redirect(self.url(self.context))
1722        return
1723
[7009]1724    @property
1725    def selected_actions(self):
[7240]1726        if getattr(self.request.principal, 'user_type', None) == 'student':
[7642]1727            return [action for action in self.actions
1728                    if not action.label in self.officers_only_actions]
1729        return self.actions
[7009]1730
[7819]1731class BedTicketAddPage(KofaPage):
[6992]1732    """ Page to add an online payment ticket
1733    """
1734    grok.context(IStudentAccommodation)
1735    grok.name('add')
[7181]1736    grok.require('waeup.handleAccommodation')
[6992]1737    grok.template('enterpin')
[6993]1738    ac_prefix = 'HOS'
[7723]1739    label = _('Add bed ticket')
[6992]1740    pnav = 4
[7723]1741    buttonname = _('Create bed ticket')
[6993]1742    notice = ''
[9188]1743    with_ac = True
[6992]1744
1745    def update(self, SUBMIT=None):
[8736]1746        student = self.context.student
[7150]1747        students_utils = getUtility(IStudentsUtils)
[7186]1748        acc_details  = students_utils.getAccommodationDetails(student)
[8688]1749        if acc_details.get('expired', False):
1750            startdate = acc_details.get('startdate')
1751            enddate = acc_details.get('enddate')
1752            if startdate and enddate:
1753                tz = getUtility(IKofaUtils).tzinfo
1754                startdate = to_timezone(
1755                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1756                enddate = to_timezone(
1757                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1758                self.flash(_("Outside booking period: ${a} - ${b}",
1759                    mapping = {'a': startdate, 'b': enddate}))
1760            else:
1761                self.flash(_("Outside booking period."))
1762            self.redirect(self.url(self.context))
1763            return
[7369]1764        if not acc_details:
[7723]1765            self.flash(_("Your data are incomplete."))
[7369]1766            self.redirect(self.url(self.context))
1767            return
[6996]1768        if not student.state in acc_details['allowed_states']:
[7723]1769            self.flash(_("You are in the wrong registration state."))
[6992]1770            self.redirect(self.url(self.context))
1771            return
[7642]1772        if student['studycourse'].current_session != acc_details[
1773            'booking_session']:
[7061]1774            self.flash(
[7723]1775                _('Your current session does not match accommodation session.'))
[7061]1776            self.redirect(self.url(self.context))
1777            return
1778        if str(acc_details['booking_session']) in self.context.keys():
[7642]1779            self.flash(
[7723]1780                _('You already booked a bed space in current ' \
1781                    + 'accommodation session.'))
[7004]1782            self.redirect(self.url(self.context))
1783            return
[9188]1784        if self.with_ac:
1785            self.ac_series = self.request.form.get('ac_series', None)
1786            self.ac_number = self.request.form.get('ac_number', None)
[6992]1787        if SUBMIT is None:
1788            return
[9188]1789        if self.with_ac:
1790            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1791            code = get_access_code(pin)
1792            if not code:
1793                self.flash(_('Activation code is invalid.'))
1794                return
[7060]1795        # Search and book bed
[6997]1796        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1797        entries = cat.searchResults(
[7003]1798            owner=(student.student_id,student.student_id))
1799        if len(entries):
[9188]1800            # If bed space has been manually allocated use this bed
[7003]1801            bed = [entry for entry in entries][0]
[9424]1802            # Safety belt for paranoids: Does this bed really exist on portal?
1803            # XXX: Can be remove if nobody complains.
1804            if bed.__parent__.__parent__ is None:
1805                self.flash(_('System error: Please contact the adminsitrator.'))
[9428]1806                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
[9424]1807                return
[7060]1808        else:
1809            # else search for other available beds
1810            entries = cat.searchResults(
1811                bed_type=(acc_details['bt'],acc_details['bt']))
1812            available_beds = [
1813                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1814            if available_beds:
[7150]1815                students_utils = getUtility(IStudentsUtils)
[7186]1816                bed = students_utils.selectBed(available_beds)
[9424]1817                # Safety belt for paranoids: Does this bed really exist in portal?
1818                # XXX: Can be remove if nobody complains.
1819                if bed.__parent__.__parent__ is None:
1820                    self.flash(_('System error: Please contact the adminsitrator.'))
[9428]1821                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
[9424]1822                    return
[7060]1823                bed.bookBed(student.student_id)
1824            else:
[7723]1825                self.flash(_('There is no free bed in your category ${a}.',
1826                    mapping = {'a':acc_details['bt']}))
[7060]1827                return
[9188]1828        if self.with_ac:
1829            # Mark pin as used (this also fires a pin related transition)
1830            if code.state == USED:
1831                self.flash(_('Activation code has already been used.'))
[6992]1832                return
[9188]1833            else:
1834                comment = _(u'invalidated')
1835                # Here we know that the ac is in state initialized so we do not
1836                # expect an exception, but the owner might be different
1837                if not invalidate_accesscode(
1838                    pin,comment,self.context.student.student_id):
1839                    self.flash(_('You are not the owner of this access code.'))
1840                    return
[7060]1841        # Create bed ticket
[6992]1842        bedticket = createObject(u'waeup.BedTicket')
[9189]1843        if self.with_ac:
1844            bedticket.booking_code = pin
[6994]1845        bedticket.booking_session = acc_details['booking_session']
[6996]1846        bedticket.bed_type = acc_details['bt']
[7006]1847        bedticket.bed = bed
[6996]1848        hall_title = bed.__parent__.hostel_name
[9199]1849        coordinates = bed.coordinates[1:]
[6996]1850        block, room_nr, bed_nr = coordinates
[7723]1851        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1852            'a':hall_title, 'b':block,
1853            'c':room_nr, 'd':bed_nr,
1854            'e':bed.bed_type})
[7819]1855        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7723]1856        bedticket.bed_coordinates = translate(
[7811]1857            bc, 'waeup.kofa',target_language=portal_language)
[9423]1858        self.context.addBedTicket(bedticket)
[9411]1859        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
[7723]1860        self.flash(_('Bed ticket created and bed booked: ${a}',
1861            mapping = {'a':bedticket.bed_coordinates}))
[6992]1862        self.redirect(self.url(self.context))
1863        return
1864
[7819]1865class BedTicketDisplayFormPage(KofaDisplayFormPage):
[6994]1866    """ Page to display bed tickets
1867    """
1868    grok.context(IBedTicket)
1869    grok.name('index')
[7181]1870    grok.require('waeup.handleAccommodation')
[6994]1871    form_fields = grok.AutoFields(IBedTicket)
[9201]1872    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6994]1873    pnav = 4
1874
1875    @property
1876    def label(self):
[7723]1877        return _('Bed Ticket for Session ${a}',
1878            mapping = {'a':self.context.getSessionString()})
[6994]1879
[7459]1880class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
[7027]1881    """Deliver a PDF slip of the context.
1882    """
1883    grok.context(IBedTicket)
[9452]1884    grok.name('bed_allocation_slip.pdf')
[7181]1885    grok.require('waeup.handleAccommodation')
[7027]1886    form_fields = grok.AutoFields(IBedTicket)
[8173]1887    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7027]1888    prefix = 'form'
[9702]1889    omit_fields = (
1890        'password', 'suspended', 'phone', 'adm_code', 'suspended_comment')
[7027]1891
1892    @property
[7723]1893    def title(self):
[7819]1894        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]1895        return translate(_('Bed Allocation Data'), 'waeup.kofa',
[7723]1896            target_language=portal_language)
1897
1898    @property
[7027]1899    def label(self):
[7819]1900        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9201]1901        #return translate(_('Bed Allocation: '),
1902        #    'waeup.kofa', target_language=portal_language) \
1903        #    + ' %s' % self.context.bed_coordinates
1904        return translate(_('Bed Allocation Slip'),
[7811]1905            'waeup.kofa', target_language=portal_language) \
[9201]1906            + ' %s' % self.context.getSessionString()
[7027]1907
1908    def render(self):
[9141]1909        studentview = StudentBasePDFFormPage(self.context.student,
[9375]1910            self.request, self.omit_fields)
[7150]1911        students_utils = getUtility(IStudentsUtils)
[7186]1912        return students_utils.renderPDF(
[9452]1913            self, 'bed_allocation_slip.pdf',
[8736]1914            self.context.student, studentview)
[7027]1915
[7459]1916class BedTicketRelocationPage(UtilityView, grok.View):
[7015]1917    """ Callback view
1918    """
1919    grok.context(IBedTicket)
1920    grok.name('relocate')
1921    grok.require('waeup.manageHostels')
1922
[7059]1923    # Relocate student if student parameters have changed or the bed_type
1924    # of the bed has changed
[7015]1925    def update(self):
[8736]1926        student = self.context.student
[7150]1927        students_utils = getUtility(IStudentsUtils)
[7186]1928        acc_details  = students_utils.getAccommodationDetails(student)
[7068]1929        if self.context.bed != None and \
1930              'reserved' in self.context.bed.bed_type:
[7723]1931            self.flash(_("Students in reserved beds can't be relocated."))
[7068]1932            self.redirect(self.url(self.context))
1933            return
[7059]1934        if acc_details['bt'] == self.context.bed_type and \
[7068]1935                self.context.bed != None and \
[7059]1936                self.context.bed.bed_type == self.context.bed_type:
[7723]1937            self.flash(_("Student can't be relocated."))
[7068]1938            self.redirect(self.url(self.context))
[7015]1939            return
[7068]1940        # Search a bed
[7015]1941        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1942        entries = cat.searchResults(
[7068]1943            owner=(student.student_id,student.student_id))
1944        if len(entries) and self.context.bed == None:
1945            # If booking has been cancelled but other bed space has been
1946            # manually allocated after cancellation use this bed
1947            new_bed = [entry for entry in entries][0]
1948        else:
1949            # Search for other available beds
1950            entries = cat.searchResults(
1951                bed_type=(acc_details['bt'],acc_details['bt']))
1952            available_beds = [
1953                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1954            if available_beds:
[7150]1955                students_utils = getUtility(IStudentsUtils)
[7186]1956                new_bed = students_utils.selectBed(available_beds)
[7068]1957                new_bed.bookBed(student.student_id)
1958            else:
[7723]1959                self.flash(_('There is no free bed in your category ${a}.',
1960                    mapping = {'a':acc_details['bt']}))
[7068]1961                self.redirect(self.url(self.context))
1962                return
[7642]1963        # Release old bed if exists
[7068]1964        if self.context.bed != None:
1965            self.context.bed.owner = NOT_OCCUPIED
1966            notify(grok.ObjectModifiedEvent(self.context.bed))
[7015]1967        # Alocate new bed
1968        self.context.bed_type = acc_details['bt']
[7068]1969        self.context.bed = new_bed
1970        hall_title = new_bed.__parent__.hostel_name
[9199]1971        coordinates = new_bed.coordinates[1:]
[7015]1972        block, room_nr, bed_nr = coordinates
[7723]1973        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1974            'a':hall_title, 'b':block,
1975            'c':room_nr, 'd':bed_nr,
1976            'e':new_bed.bed_type})
[7819]1977        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7723]1978        self.context.bed_coordinates = translate(
[7811]1979            bc, 'waeup.kofa',target_language=portal_language)
[9411]1980        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
[7723]1981        self.flash(_('Student relocated: ${a}',
1982            mapping = {'a':self.context.bed_coordinates}))
[7015]1983        self.redirect(self.url(self.context))
1984        return
1985
1986    def render(self):
1987        return
1988
[7819]1989class StudentHistoryPage(KofaPage):
[6637]1990    """ Page to display student clearance data
1991    """
1992    grok.context(IStudent)
1993    grok.name('history')
[6660]1994    grok.require('waeup.viewStudent')
[6637]1995    grok.template('studenthistory')
[6642]1996    pnav = 4
[6637]1997
1998    @property
1999    def label(self):
[7723]2000        return _('${a}: History', mapping = {'a':self.context.display_fullname})
[6694]2001
2002# Pages for students only
2003
[7819]2004class StudentBaseEditFormPage(KofaEditFormPage):
[7133]2005    """ View to edit student base data
2006    """
2007    grok.context(IStudent)
2008    grok.name('edit_base')
2009    grok.require('waeup.handleStudent')
2010    form_fields = grok.AutoFields(IStudentBase).select(
2011        'email', 'phone')
[7723]2012    label = _('Edit base data')
[7133]2013    pnav = 4
2014
[7723]2015    @action(_('Save'), style='primary')
[7133]2016    def save(self, **data):
2017        msave(self, **data)
2018        return
2019
[7819]2020class StudentChangePasswordPage(KofaEditFormPage):
[7144]2021    """ View to manage student base data
[6756]2022    """
2023    grok.context(IStudent)
[7114]2024    grok.name('change_password')
[6694]2025    grok.require('waeup.handleStudent')
[7144]2026    grok.template('change_password')
[7723]2027    label = _('Change password')
[6694]2028    pnav = 4
2029
[7723]2030    @action(_('Save'), style='primary')
[7144]2031    def save(self, **data):
2032        form = self.request.form
2033        password = form.get('change_password', None)
2034        password_ctl = form.get('change_password_repeat', None)
2035        if password:
[7147]2036            validator = getUtility(IPasswordValidator)
2037            errors = validator.validate_password(password, password_ctl)
2038            if not errors:
2039                IUserAccount(self.context).setPassword(password)
[8735]2040                self.context.writeLogMessage(self, 'saved: password')
[7723]2041                self.flash(_('Password changed.'))
[6756]2042            else:
[7147]2043                self.flash( ' '.join(errors))
[6756]2044        return
2045
[7819]2046class StudentFilesUploadPage(KofaPage):
[7114]2047    """ View to upload files by student
2048    """
2049    grok.context(IStudent)
2050    grok.name('change_portrait')
[7127]2051    grok.require('waeup.uploadStudentFile')
[7114]2052    grok.template('filesuploadpage')
[7723]2053    label = _('Upload portrait')
[7114]2054    pnav = 4
2055
[7133]2056    def update(self):
[8736]2057        if self.context.student.state != ADMITTED:
[7145]2058            emit_lock_message(self)
[7133]2059            return
2060        super(StudentFilesUploadPage, self).update()
2061        return
2062
[7819]2063class StartClearancePage(KofaPage):
[6770]2064    grok.context(IStudent)
2065    grok.name('start_clearance')
2066    grok.require('waeup.handleStudent')
2067    grok.template('enterpin')
[7723]2068    label = _('Start clearance')
[6770]2069    ac_prefix = 'CLR'
2070    notice = ''
2071    pnav = 4
[7723]2072    buttonname = _('Start clearance now')
[9952]2073    with_ac = True
[6770]2074
[7133]2075    @property
2076    def all_required_fields_filled(self):
2077        if self.context.email and self.context.phone:
2078            return True
2079        return False
2080
2081    @property
2082    def portrait_uploaded(self):
2083        store = getUtility(IExtFileStore)
2084        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2085            return True
2086        return False
2087
[6770]2088    def update(self, SUBMIT=None):
[7671]2089        if not self.context.state == ADMITTED:
[7745]2090            self.flash(_("Wrong state"))
[6936]2091            self.redirect(self.url(self.context))
2092            return
[7133]2093        if not self.portrait_uploaded:
[7723]2094            self.flash(_("No portrait uploaded."))
[7133]2095            self.redirect(self.url(self.context, 'change_portrait'))
2096            return
2097        if not self.all_required_fields_filled:
[7723]2098            self.flash(_("Not all required fields filled."))
[7133]2099            self.redirect(self.url(self.context, 'edit_base'))
2100            return
[9952]2101        if self.with_ac:
2102            self.ac_series = self.request.form.get('ac_series', None)
2103            self.ac_number = self.request.form.get('ac_number', None)
[6770]2104        if SUBMIT is None:
2105            return
[9952]2106        if self.with_ac:
2107            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2108            code = get_access_code(pin)
2109            if not code:
2110                self.flash(_('Activation code is invalid.'))
2111                return
2112            if code.state == USED:
2113                self.flash(_('Activation code has already been used.'))
2114                return
2115            # Mark pin as used (this also fires a pin related transition)
2116            # and fire transition start_clearance
2117            comment = _(u"invalidated")
2118            # Here we know that the ac is in state initialized so we do not
2119            # expect an exception, but the owner might be different
2120            if not invalidate_accesscode(pin, comment, self.context.student_id):
2121                self.flash(_('You are not the owner of this access code.'))
2122                return
2123            self.context.clr_code = pin
[6770]2124        IWorkflowInfo(self.context).fireTransition('start_clearance')
[7723]2125        self.flash(_('Clearance process has been started.'))
[6770]2126        self.redirect(self.url(self.context,'cedit'))
2127        return
2128
[6695]2129class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2130    """ View to edit student clearance data by student
2131    """
2132    grok.context(IStudent)
2133    grok.name('cedit')
2134    grok.require('waeup.handleStudent')
[7723]2135    label = _('Edit clearance data')
[6718]2136
[7993]2137    @property
2138    def form_fields(self):
[8472]2139        if self.context.is_postgrad:
[8974]2140            form_fields = grok.AutoFields(IPGStudentClearance).omit(
[9486]2141                'clearance_locked', 'clr_code', 'officer_comment')
[7993]2142        else:
[8974]2143            form_fields = grok.AutoFields(IUGStudentClearance).omit(
[9486]2144                'clearance_locked', 'clr_code', 'officer_comment')
[7993]2145        return form_fields
2146
[6718]2147    def update(self):
2148        if self.context.clearance_locked:
[7145]2149            emit_lock_message(self)
[6718]2150            return
2151        return super(StudentClearanceEditFormPage, self).update()
[6719]2152
[7723]2153    @action(_('Save'), style='primary')
[6722]2154    def save(self, **data):
2155        self.applyData(self.context, **data)
[7723]2156        self.flash(_('Clearance form has been saved.'))
[6722]2157        return
2158
[7253]2159    def dataNotComplete(self):
[7642]2160        """To be implemented in the customization package.
2161        """
[7253]2162        return False
2163
[7723]2164    @action(_('Save and request clearance'), style='primary')
[7186]2165    def requestClearance(self, **data):
[6722]2166        self.applyData(self.context, **data)
[7253]2167        if self.dataNotComplete():
2168            self.flash(self.dataNotComplete())
2169            return
[7723]2170        self.flash(_('Clearance form has been saved.'))
[9021]2171        if self.context.clr_code:
2172            self.redirect(self.url(self.context, 'request_clearance'))
2173        else:
2174            # We bypass the request_clearance page if student
2175            # has been imported in state 'clearance started' and
2176            # no clr_code was entered before.
2177            state = IWorkflowState(self.context).getState()
2178            if state != CLEARANCE:
2179                # This shouldn't happen, but the application officer
2180                # might have forgotten to lock the form after changing the state
2181                self.flash(_('This form cannot be submitted. Wrong state!'))
2182                return
2183            IWorkflowInfo(self.context).fireTransition('request_clearance')
2184            self.flash(_('Clearance has been requested.'))
2185            self.redirect(self.url(self.context))
[6722]2186        return
2187
[7819]2188class RequestClearancePage(KofaPage):
[6769]2189    grok.context(IStudent)
2190    grok.name('request_clearance')
2191    grok.require('waeup.handleStudent')
2192    grok.template('enterpin')
[7723]2193    label = _('Request clearance')
2194    notice = _('Enter the CLR access code used for starting clearance.')
[6769]2195    ac_prefix = 'CLR'
2196    pnav = 4
[7723]2197    buttonname = _('Request clearance now')
[9952]2198    with_ac = True
[6769]2199
2200    def update(self, SUBMIT=None):
[9952]2201        if self.with_ac:
2202            self.ac_series = self.request.form.get('ac_series', None)
2203            self.ac_number = self.request.form.get('ac_number', None)
[6769]2204        if SUBMIT is None:
2205            return
[9952]2206        if self.with_ac:
2207            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2208            if self.context.clr_code and self.context.clr_code != pin:
2209                self.flash(_("This isn't your CLR access code."))
2210                return
[6769]2211        state = IWorkflowState(self.context).getState()
2212        if state != CLEARANCE:
[9021]2213            # This shouldn't happen, but the application officer
2214            # might have forgotten to lock the form after changing the state
[7723]2215            self.flash(_('This form cannot be submitted. Wrong state!'))
[6769]2216            return
2217        IWorkflowInfo(self.context).fireTransition('request_clearance')
[7723]2218        self.flash(_('Clearance has been requested.'))
[6769]2219        self.redirect(self.url(self.context))
[6789]2220        return
[6806]2221
[8471]2222class StartSessionPage(KofaPage):
[6944]2223    grok.context(IStudentStudyCourse)
[8471]2224    grok.name('start_session')
[6944]2225    grok.require('waeup.handleStudent')
2226    grok.template('enterpin')
[8471]2227    label = _('Start session')
[6944]2228    ac_prefix = 'SFE'
2229    notice = ''
2230    pnav = 4
[8471]2231    buttonname = _('Start now')
[9952]2232    with_ac = True
[6944]2233
2234    def update(self, SUBMIT=None):
[9139]2235        if not self.context.is_current:
2236            emit_lock_message(self)
2237            return
2238        super(StartSessionPage, self).update()
[8471]2239        if not self.context.next_session_allowed:
2240            self.flash(_("You are not entitled to start session."))
[6944]2241            self.redirect(self.url(self.context))
2242            return
[9952]2243        if self.with_ac:
2244            self.ac_series = self.request.form.get('ac_series', None)
2245            self.ac_number = self.request.form.get('ac_number', None)
[6944]2246        if SUBMIT is None:
2247            return
[9952]2248        if self.with_ac:
2249            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2250            code = get_access_code(pin)
2251            if not code:
2252                self.flash(_('Activation code is invalid.'))
[6944]2253                return
[9952]2254            # Mark pin as used (this also fires a pin related transition)
2255            if code.state == USED:
2256                self.flash(_('Activation code has already been used.'))
2257                return
2258            else:
2259                comment = _(u"invalidated")
2260                # Here we know that the ac is in state initialized so we do not
2261                # expect an error, but the owner might be different
2262                if not invalidate_accesscode(
2263                    pin,comment,self.context.student.student_id):
2264                    self.flash(_('You are not the owner of this access code.'))
2265                    return
[9637]2266        try:
2267            if self.context.student.state == CLEARED:
2268                IWorkflowInfo(self.context.student).fireTransition(
2269                    'pay_first_school_fee')
2270            elif self.context.student.state == RETURNING:
2271                IWorkflowInfo(self.context.student).fireTransition(
2272                    'pay_school_fee')
2273            elif self.context.student.state == PAID:
2274                IWorkflowInfo(self.context.student).fireTransition(
2275                    'pay_pg_fee')
2276        except ConstraintNotSatisfied:
2277            self.flash(_('An error occurred, please contact the system administrator.'))
2278            return
[8471]2279        self.flash(_('Session started.'))
[6944]2280        self.redirect(self.url(self.context))
2281        return
2282
[7819]2283class AddStudyLevelFormPage(KofaEditFormPage):
[6806]2284    """ Page for students to add current study levels
2285    """
2286    grok.context(IStudentStudyCourse)
2287    grok.name('add')
2288    grok.require('waeup.handleStudent')
2289    grok.template('studyleveladdpage')
2290    form_fields = grok.AutoFields(IStudentStudyCourse)
2291    pnav = 4
2292
2293    @property
2294    def label(self):
2295        studylevelsource = StudyLevelSource().factory
2296        code = self.context.current_level
2297        title = studylevelsource.getTitle(self.context, code)
[7723]2298        return _('Add current level ${a}', mapping = {'a':title})
[6806]2299
2300    def update(self):
[9139]2301        if not self.context.is_current:
2302            emit_lock_message(self)
2303            return
[8736]2304        if self.context.student.state != PAID:
[7145]2305            emit_lock_message(self)
[6806]2306            return
2307        super(AddStudyLevelFormPage, self).update()
2308        return
2309
[7723]2310    @action(_('Create course list now'), style='primary')
[6806]2311    def addStudyLevel(self, **data):
[8323]2312        studylevel = createObject(u'waeup.StudentStudyLevel')
[6806]2313        studylevel.level = self.context.current_level
2314        studylevel.level_session = self.context.current_session
2315        try:
2316            self.context.addStudentStudyLevel(
2317                self.context.certificate,studylevel)
2318        except KeyError:
[7723]2319            self.flash(_('This level exists.'))
[9467]2320        except RequiredMissing:
2321            self.flash(_('Your data are incomplete'))
[6806]2322        self.redirect(self.url(self.context))
2323        return
[6808]2324
[7819]2325class StudyLevelEditFormPage(KofaEditFormPage):
[6808]2326    """ Page to edit the student study level data by students
2327    """
2328    grok.context(IStudentStudyLevel)
2329    grok.name('edit')
[9924]2330    grok.require('waeup.editStudyLevel')
[6808]2331    grok.template('studyleveleditpage')
2332    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2333        'level_session', 'level_verdict')
2334    pnav = 4
2335
[9895]2336    def update(self, ADD=None, course=None):
[9139]2337        if not self.context.__parent__.is_current:
2338            emit_lock_message(self)
2339            return
[9257]2340        if self.context.student.state != PAID or \
2341            not self.context.is_current_level:
[7539]2342            emit_lock_message(self)
2343            return
[6808]2344        super(StudyLevelEditFormPage, self).update()
2345        datatable.need()
[7329]2346        warning.need()
[9895]2347        if ADD is not None:
2348            if not course:
2349                self.flash(_('No valid course code entered.'))
2350                return
2351            cat = queryUtility(ICatalog, name='courses_catalog')
2352            result = cat.searchResults(code=(course, course))
2353            if len(result) != 1:
2354                self.flash(_('Course not found.'))
2355                return
2356            course = list(result)[0]
2357            addCourseTicket(self, course)
[6808]2358        return
2359
2360    @property
2361    def label(self):
[7833]2362        # Here we know that the cookie has been set
2363        lang = self.request.cookies.get('kofa.language')
[7811]2364        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]2365            target_language=lang)
[8920]2366        return _('Edit course list of ${a}',
[7723]2367            mapping = {'a':level_title})
[6808]2368
2369    @property
[8921]2370    def translated_values(self):
2371        return translated_values(self)
2372
[9280]2373    def _delCourseTicket(self, **data):
[6808]2374        form = self.request.form
[9701]2375        if 'val_id' in form:
[6808]2376            child_id = form['val_id']
2377        else:
[7723]2378            self.flash(_('No ticket selected.'))
[6808]2379            self.redirect(self.url(self.context, '@@edit'))
2380            return
2381        if not isinstance(child_id, list):
2382            child_id = [child_id]
2383        deleted = []
2384        for id in child_id:
[6940]2385            # Students are not allowed to remove core tickets
[9700]2386            if id in self.context and \
2387                self.context[id].removable_by_student:
[7723]2388                del self.context[id]
2389                deleted.append(id)
[6808]2390        if len(deleted):
[7723]2391            self.flash(_('Successfully removed: ${a}',
2392                mapping = {'a':', '.join(deleted)}))
[9332]2393            self.context.writeLogMessage(
[9924]2394                self,'removed: %s at %s' %
2395                (', '.join(deleted), self.context.level))
[6808]2396        self.redirect(self.url(self.context, u'@@edit'))
2397        return
2398
[9280]2399    @jsaction(_('Remove selected tickets'))
2400    def delCourseTicket(self, **data):
2401        self._delCourseTicket(**data)
2402        return
2403
2404    def _registerCourses(self, **data):
[9252]2405        if self.context.student.is_postgrad:
2406            self.flash(_(
2407                "You are a postgraduate student, "
2408                "your course list can't bee registered."))
2409            self.redirect(self.url(self.context))
2410            return
[9830]2411        students_utils = getUtility(IStudentsUtils)
2412        max_credits = students_utils.maxCredits(self.context)
2413        if self.context.total_credits > max_credits:
[8642]2414            self.flash(_('Maximum credits of ${a} exceeded.',
[9830]2415                mapping = {'a':max_credits}))
[8642]2416            return
[8736]2417        IWorkflowInfo(self.context.student).fireTransition(
[7642]2418            'register_courses')
[7723]2419        self.flash(_('Course list has been registered.'))
[6810]2420        self.redirect(self.url(self.context))
2421        return
2422
[9895]2423    @action(_('Register course list'))
[9280]2424    def registerCourses(self, **data):
2425        self._registerCourses(**data)
2426        return
2427
[6808]2428class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2429    """Add a course ticket by student.
2430    """
2431    grok.name('ctadd')
2432    grok.require('waeup.handleStudent')
[9420]2433    form_fields = grok.AutoFields(ICourseTicketAdd)
[6808]2434
[7539]2435    def update(self):
[9257]2436        if self.context.student.state != PAID or \
2437            not self.context.is_current_level:
[7539]2438            emit_lock_message(self)
2439            return
2440        super(CourseTicketAddFormPage2, self).update()
2441        return
2442
[7723]2443    @action(_('Add course ticket'))
[6808]2444    def addCourseTicket(self, **data):
[7642]2445        # Safety belt
[8736]2446        if self.context.student.state != PAID:
[7539]2447            return
[6808]2448        course = data['course']
[9895]2449        success = addCourseTicket(self, course)
2450        if success:
2451            self.redirect(self.url(self.context, u'@@edit'))
[6808]2452        return
[7369]2453
[7819]2454class SetPasswordPage(KofaPage):
2455    grok.context(IKofaObject)
[7660]2456    grok.name('setpassword')
2457    grok.require('waeup.Anonymous')
2458    grok.template('setpassword')
[7723]2459    label = _('Set password for first-time login')
[7660]2460    ac_prefix = 'PWD'
2461    pnav = 0
[7738]2462    set_button = _('Set')
[7660]2463
2464    def update(self, SUBMIT=None):
2465        self.reg_number = self.request.form.get('reg_number', None)
2466        self.ac_series = self.request.form.get('ac_series', None)
2467        self.ac_number = self.request.form.get('ac_number', None)
2468
2469        if SUBMIT is None:
2470            return
2471        hitlist = search(query=self.reg_number,
2472            searchtype='reg_number', view=self)
2473        if not hitlist:
[7723]2474            self.flash(_('No student found.'))
[7660]2475            return
2476        if len(hitlist) != 1:   # Cannot happen but anyway
[7723]2477            self.flash(_('More than one student found.'))
[7660]2478            return
2479        student = hitlist[0].context
2480        self.student_id = student.student_id
2481        student_pw = student.password
2482        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2483        code = get_access_code(pin)
2484        if not code:
[7723]2485            self.flash(_('Access code is invalid.'))
[7660]2486            return
2487        if student_pw and pin == student.adm_code:
[7723]2488            self.flash(_(
2489                'Password has already been set. Your Student Id is ${a}',
2490                mapping = {'a':self.student_id}))
[7660]2491            return
2492        elif student_pw:
2493            self.flash(
[7723]2494                _('Password has already been set. You are using the ' +
2495                'wrong Access Code.'))
[7660]2496            return
2497        # Mark pin as used (this also fires a pin related transition)
2498        # and set student password
2499        if code.state == USED:
[7723]2500            self.flash(_('Access code has already been used.'))
[7660]2501            return
2502        else:
[7723]2503            comment = _(u"invalidated")
[7660]2504            # Here we know that the ac is in state initialized so we do not
2505            # expect an exception
2506            invalidate_accesscode(pin,comment)
2507            IUserAccount(student).setPassword(self.ac_number)
2508            student.adm_code = pin
[7723]2509        self.flash(_('Password has been set. Your Student Id is ${a}',
2510            mapping = {'a':self.student_id}))
[7811]2511        return
[8779]2512
2513class StudentRequestPasswordPage(KofaAddFormPage):
2514    """Captcha'd registration page for applicants.
2515    """
2516    grok.name('requestpw')
2517    grok.require('waeup.Anonymous')
2518    grok.template('requestpw')
2519    form_fields = grok.AutoFields(IStudentRequestPW).select(
[8854]2520        'firstname','number','email')
[8779]2521    label = _('Request password for first-time login')
2522
2523    def update(self):
2524        # Handle captcha
2525        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2526        self.captcha_result = self.captcha.verify(self.request)
2527        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2528        return
2529
2530    def _redirect(self, email, password, student_id):
2531        # Forward only email to landing page in base package.
2532        self.redirect(self.url(self.context, 'requestpw_complete',
2533            data = dict(email=email)))
2534        return
2535
2536    def _pw_used(self):
[8780]2537        # XXX: False if password has not been used. We need an extra
2538        #      attribute which remembers if student logged in.
[8779]2539        return True
2540
[8854]2541    @action(_('Send login credentials to email address'), style='primary')
[8779]2542    def get_credentials(self, **data):
2543        if not self.captcha_result.is_valid:
2544            # Captcha will display error messages automatically.
2545            # No need to flash something.
2546            return
[8854]2547        number = data.get('number','')
[8779]2548        firstname = data.get('firstname','')
2549        cat = getUtility(ICatalog, name='students_catalog')
2550        results = list(
[8854]2551            cat.searchResults(reg_number=(number, number)))
2552        if not results:
2553            results = list(
2554                cat.searchResults(matric_number=(number, number)))
[8779]2555        if results:
2556            student = results[0]
2557            if getattr(student,'firstname',None) is None:
2558                self.flash(_('An error occurred.'))
2559                return
2560            elif student.firstname.lower() != firstname.lower():
2561                # Don't tell the truth here. Anonymous must not
2562                # know that a record was found and only the firstname
2563                # verification failed.
2564                self.flash(_('No student record found.'))
2565                return
2566            elif student.password is not None and self._pw_used:
2567                self.flash(_('Your password has already been set and used. '
2568                             'Please proceed to the login page.'))
2569                return
2570            # Store email address but nothing else.
2571            student.email = data['email']
2572            notify(grok.ObjectModifiedEvent(student))
2573        else:
2574            # No record found, this is the truth.
2575            self.flash(_('No student record found.'))
2576            return
2577
2578        kofa_utils = getUtility(IKofaUtils)
2579        password = kofa_utils.genPassword()
[8857]2580        mandate = PasswordMandate()
[8853]2581        mandate.params['password'] = password
[8858]2582        mandate.params['user'] = student
[8853]2583        site = grok.getSite()
2584        site['mandates'].addMandate(mandate)
[8779]2585        # Send email with credentials
[8853]2586        args = {'mandate_id':mandate.mandate_id}
2587        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2588        url_info = u'Confirmation link: %s' % mandate_url
[8779]2589        msg = _('You have successfully requested a password for the')
2590        if kofa_utils.sendCredentials(IUserAccount(student),
[8853]2591            password, url_info, msg):
[8779]2592            email_sent = student.email
2593        else:
2594            email_sent = None
2595        self._redirect(email=email_sent, password=password,
2596            student_id=student.student_id)
[8856]2597        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2598        self.context.logger.info(
2599            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
[8779]2600        return
2601
2602class StudentRequestPasswordEmailSent(KofaPage):
2603    """Landing page after successful password request.
2604
2605    """
2606    grok.name('requestpw_complete')
2607    grok.require('waeup.Public')
2608    grok.template('requestpwmailsent')
2609    label = _('Your password request was successful.')
2610
2611    def update(self, email=None, student_id=None, password=None):
2612        self.email = email
2613        self.password = password
2614        self.student_id = student_id
[8974]2615        return
[9797]2616
[9806]2617class FilterStudentsInDepartmentPage(KofaPage):
2618    """Page that filters and lists students.
2619    """
2620    grok.context(IDepartment)
2621    grok.require('waeup.showStudents')
2622    grok.name('students')
2623    grok.template('filterstudentspage')
2624    pnav = 1
[9819]2625    session_label = _('Current Session')
2626    level_label = _('Current Level')
[9806]2627
2628    def label(self):
[9819]2629        return 'Students in %s' % self.context.longtitle()
[9806]2630
2631    def _set_session_values(self):
2632        vocab_terms = academic_sessions_vocab.by_value.values()
2633        self.sessions = sorted(
2634            [(x.title, x.token) for x in vocab_terms], reverse=True)
2635        self.sessions += [('All Sessions', 'all')]
2636        return
2637
2638    def _set_level_values(self):
2639        vocab_terms = course_levels.by_value.values()
2640        self.levels = sorted(
2641            [(x.title, x.token) for x in vocab_terms])
2642        self.levels += [('All Levels', 'all')]
2643        return
2644
2645    def _searchCatalog(self, session, level):
2646        if level not in (10, 999, None):
2647            start_level = 100 * (level // 100)
2648            end_level = start_level + 90
2649        else:
2650            start_level = end_level = level
2651        cat = queryUtility(ICatalog, name='students_catalog')
2652        students = cat.searchResults(
2653            current_session=(session, session),
2654            current_level=(start_level, end_level),
2655            depcode=(self.context.code, self.context.code)
2656            )
2657        hitlist = []
2658        for student in students:
2659            hitlist.append(StudentQueryResultItem(student, view=self))
2660        return hitlist
2661
2662    def update(self, SHOW=None, session=None, level=None):
2663        datatable.need()
2664        self.parent_url = self.url(self.context.__parent__)
2665        self._set_session_values()
2666        self._set_level_values()
2667        self.hitlist = []
2668        self.session_default = session
2669        self.level_default = level
2670        if SHOW is not None:
2671            if session != 'all':
2672                self.session = int(session)
2673                self.session_string = '%s %s/%s' % (
2674                    self.session_label, self.session, self.session+1)
2675            else:
2676                self.session = None
2677                self.session_string = _('in any session')
2678            if level != 'all':
2679                self.level = int(level)
2680                self.level_string = '%s %s' % (self.level_label, self.level)
2681            else:
2682                self.level = None
2683                self.level_string = _('at any level')
2684            self.hitlist = self._searchCatalog(self.session, self.level)
2685            if not self.hitlist:
2686                self.flash(_('No student found.'))
2687        return
2688
2689class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2690    """Page that filters and lists students.
2691    """
2692    grok.context(ICertificate)
2693
2694    def label(self):
[9819]2695        return 'Students studying %s' % self.context.longtitle()
[9806]2696
2697    def _searchCatalog(self, session, level):
2698        if level not in (10, 999, None):
2699            start_level = 100 * (level // 100)
2700            end_level = start_level + 90
2701        else:
2702            start_level = end_level = level
2703        cat = queryUtility(ICatalog, name='students_catalog')
2704        students = cat.searchResults(
2705            current_session=(session, session),
2706            current_level=(start_level, end_level),
2707            certcode=(self.context.code, self.context.code)
2708            )
2709        hitlist = []
2710        for student in students:
2711            hitlist.append(StudentQueryResultItem(student, view=self))
2712        return hitlist
2713
2714class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2715    """Page that filters and lists students.
2716    """
2717    grok.context(ICourse)
2718
2719    def label(self):
[9819]2720        return 'Students registered for %s' % self.context.longtitle()
[9806]2721
2722    def _searchCatalog(self, session, level):
2723        if level not in (10, 999, None):
2724            start_level = 100 * (level // 100)
2725            end_level = start_level + 90
2726        else:
2727            start_level = end_level = level
2728        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2729        coursetickets = cat.searchResults(
2730            session=(session, session),
2731            level=(start_level, end_level),
2732            code=(self.context.code, self.context.code)
2733            )
2734        hitlist = []
2735        for ticket in coursetickets:
2736            # XXX: If students have registered the same courses twice
2737            # they will be listed twice.
2738            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
2739        return hitlist
2740
[9813]2741class ExportJobContainerOverview(KofaPage):
[9835]2742    """Page that lists active student data export jobs and provides links
2743    to discard or download CSV files.
2744
[9797]2745    """
[9813]2746    grok.context(VirtualExportJobContainer)
[9797]2747    grok.require('waeup.showStudents')
2748    grok.name('index.html')
2749    grok.template('exportjobsindex')
[9813]2750    label = _('Student Data Exports')
[9797]2751    pnav = 1
2752
2753    def update(self, CREATE=None, DISCARD=None, job_id=None):
2754        if CREATE:
[9836]2755            self.redirect(self.url('@@exportconfig'))
[9797]2756            return
2757        if DISCARD and job_id:
2758            entry = self.context.entry_from_job_id(job_id)
2759            self.context.delete_export_entry(entry)
[9836]2760            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2761            self.context.logger.info(
2762                '%s - discarded: job_id=%s' % (ob_class, job_id))
[9819]2763            self.flash(_('Discarded export') + ' %s' % job_id)
[9822]2764        self.entries = doll_up(self, user=self.request.principal.id)
[9797]2765        return
2766
[9833]2767class ExportJobContainerJobConfig(KofaPage):
[9797]2768    """Page that configures a students export job.
[9833]2769
2770    This is a baseclass.
[9797]2771    """
[9833]2772    grok.baseclass()
[9836]2773    grok.name('exportconfig')
[9797]2774    grok.require('waeup.showStudents')
[9836]2775    grok.template('exportconfig')
[9833]2776    label = _('Configure student data export')
[9797]2777    pnav = 1
[9835]2778    redirect_target = ''
[9797]2779
2780    def _set_session_values(self):
2781        vocab_terms = academic_sessions_vocab.by_value.values()
2782        self.sessions = sorted(
2783            [(x.title, x.token) for x in vocab_terms], reverse=True)
[9819]2784        self.sessions += [(_('All Sessions'), 'all')]
[9797]2785        return
2786
2787    def _set_level_values(self):
2788        vocab_terms = course_levels.by_value.values()
2789        self.levels = sorted(
2790            [(x.title, x.token) for x in vocab_terms])
[9819]2791        self.levels += [(_('All Levels'), 'all')]
[9797]2792        return
2793
[9803]2794    def _set_mode_values(self):
2795        utils = getUtility(IKofaUtils)
[9838]2796        self.modes = sorted([(value, key) for key, value in
2797                      utils.STUDY_MODES_DICT.items()])
[9819]2798        self.modes +=[(_('All Modes'), 'all')]
[9803]2799        return
2800
[9804]2801    def _set_exporter_values(self):
2802        # We provide all student exporters, nothing else, yet.
2803        exporters = []
2804        for name in EXPORTER_NAMES:
2805            util = getUtility(ICSVExporter, name=name)
2806            exporters.append((util.title, name),)
2807        self.exporters = exporters
2808
[9833]2809    @property
2810    def depcode(self):
2811        return None
2812
[9842]2813    @property
2814    def certcode(self):
2815        return None
2816
[9804]2817    def update(self, START=None, session=None, level=None, mode=None,
2818               exporter=None):
[9797]2819        self._set_session_values()
2820        self._set_level_values()
[9803]2821        self._set_mode_values()
[9804]2822        self._set_exporter_values()
[9797]2823        if START is None:
2824            return
2825        if session == 'all':
2826            session=None
2827        if level == 'all':
2828            level = None
[9803]2829        if mode == 'all':
2830            mode = None
[9933]2831        if (mode, level, session,
2832            self.depcode, self.certcode) == (None, None, None, None, None):
2833            # Export all students including those without certificate
2834            job_id = self.context.start_export_job(exporter,
2835                                          self.request.principal.id)
2836        else:
2837            job_id = self.context.start_export_job(exporter,
2838                                          self.request.principal.id,
2839                                          current_session=session,
2840                                          current_level=level,
2841                                          current_mode=mode,
2842                                          depcode=self.depcode,
2843                                          certcode=self.certcode)
[9836]2844        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2845        self.context.logger.info(
[9842]2846            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
2847            % (ob_class, exporter, session, level, mode, self.depcode,
2848            self.certcode, job_id))
[9833]2849        self.flash(_('Export started for students with') +
2850                   ' current_session=%s, current_level=%s, study_mode=%s' % (
2851                   session, level, mode))
[9835]2852        self.redirect(self.url(self.redirect_target))
[9797]2853        return
2854
[9822]2855class ExportJobContainerDownload(ExportCSVView):
[9835]2856    """Page that downloads a students export csv file.
2857
[9797]2858    """
[9813]2859    grok.context(VirtualExportJobContainer)
[9797]2860    grok.require('waeup.showStudents')
[9833]2861
2862class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
2863    """Page that configures a students export job in datacenter.
2864
2865    """
2866    grok.context(IDataCenter)
[9835]2867    redirect_target = '@@export'
[9833]2868
2869class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
2870    """Page that configures a students export job in departments.
2871
2872    """
2873    grok.context(VirtualDepartmentExportJobContainer)
2874
2875    @property
2876    def depcode(self):
[9835]2877        return self.context.__parent__.code
[9842]2878
2879class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
2880    """Page that configures a students export job for certificates.
2881
2882    """
2883    grok.context(VirtualCertificateExportJobContainer)
[9843]2884    grok.template('exportconfig_certificate')
[9842]2885
2886    @property
2887    def certcode(self):
2888        return self.context.__parent__.code
[9843]2889
2890class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
2891    """Page that configures a students export job for courses.
2892
2893    In contrast to department or certificate student data exports the
2894    coursetickets_catalog is searched here. Therefore the update
2895    method from the base class is customized.
2896    """
2897    grok.context(VirtualCourseExportJobContainer)
2898    grok.template('exportconfig_course')
2899
2900    def _set_exporter_values(self):
[9844]2901        # We provide only two exporters.
[9843]2902        exporters = []
[9844]2903        for name in ('students', 'coursetickets'):
[9843]2904            util = getUtility(ICSVExporter, name=name)
2905            exporters.append((util.title, name),)
2906        self.exporters = exporters
2907
2908    def update(self, START=None, session=None, level=None, mode=None,
2909               exporter=None):
2910        self._set_session_values()
2911        self._set_level_values()
2912        self._set_mode_values()
2913        self._set_exporter_values()
2914        if START is None:
2915            return
2916        if session == 'all':
2917            session=None
2918        if level == 'all':
2919            level = None
2920        job_id = self.context.start_export_job(exporter,
2921                                      self.request.principal.id,
2922                                      # Use a different catalog and
2923                                      # pass different keywords than
2924                                      # for the (default) students_catalog
[9845]2925                                      catalog='coursetickets',
[9843]2926                                      session=session,
2927                                      level=level,
2928                                      code=self.context.__parent__.code)
2929        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2930        self.context.logger.info(
2931            '%s - exported: %s (%s, %s, %s), job_id=%s'
2932            % (ob_class, exporter, session, level,
2933            self.context.__parent__.code, job_id))
2934        self.flash(_('Export started for course tickets with') +
2935                   ' level_session=%s, level=%s' % (
2936                   session, level))
2937        self.redirect(self.url(self.redirect_target))
2938        return
Note: See TracBrowser for help on using the repository browser.