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

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

Disable states and transitions which are not allowed for pg students. Not yet fully tested.

  • Property svn:keywords set to Id
File size: 73.9 KB
RevLine 
[7191]1## $Id: browser.py 9028 2012-07-20 08:56:34Z 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
[8143]24from copy import deepcopy
[7015]25from zope.event import notify
[7723]26from zope.i18n import translate
[6996]27from zope.catalog.interfaces import ICatalog
[7386]28from zope.component import queryUtility, getUtility, createObject
[8099]29from zope.schema.interfaces import ConstraintNotSatisfied
[7386]30from zope.formlib.textwidgets import BytesDisplayWidget
[6621]31from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
[7811]32from waeup.kofa.accesscodes import (
[8420]33    invalidate_accesscode, get_access_code)
[7811]34from waeup.kofa.accesscodes.workflow import USED
35from waeup.kofa.browser import (
[7819]36    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
[7834]37    ContactAdminForm, KofaForm, NullValidator)
[7811]38from waeup.kofa.browser.breadcrumbs import Breadcrumb
39from waeup.kofa.browser.resources import datepicker, datatable, tabs, warning
40from waeup.kofa.browser.layout import jsaction, action, UtilityView
[8779]41from waeup.kofa.browser.interfaces import ICaptchaManager
[7811]42from waeup.kofa.interfaces import (
[7819]43    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
44    IKofaUtils, IUniversity)
[7811]45from waeup.kofa.interfaces import MessageFactory as _
[8170]46from waeup.kofa.widgets.datewidget import (
47    FriendlyDateWidget, FriendlyDateDisplayWidget,
48    FriendlyDatetimeDisplayWidget)
[7868]49from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
[7811]50from waeup.kofa.students.interfaces import (
[7993]51    IStudentsContainer, IStudent,
52    IUGStudentClearance,IPGStudentClearance,
[6859]53    IStudentPersonal, IStudentBase, IStudentStudyCourse,
[7538]54    IStudentAccommodation, IStudentStudyLevel,
[6877]55    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
[8779]56    IStudentOnlinePayment, IBedTicket, IStudentsUtils, IStudentRequestPW
[6621]57    )
[7811]58from waeup.kofa.students.catalog import search
[8779]59from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
[9028]60    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
61    FORBIDDEN_POSTGRAD_TRANS)
[7811]62from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
63from waeup.kofa.students.vocabularies import StudyLevelSource
64from waeup.kofa.browser.resources import toggleall
65from waeup.kofa.hostels.hostel import NOT_OCCUPIED
[8737]66from waeup.kofa.utils.helpers import get_current_principal, to_timezone
[8857]67from waeup.kofa.mandates.mandate import PasswordMandate
[6621]68
[8779]69grok.context(IKofaObject) # Make IKofaObject the default context
70
[8737]71# Save function used for save methods in pages
72def msave(view, **data):
73    changed_fields = view.applyData(view.context, **data)
74    # Turn list of lists into single list
75    if changed_fields:
76        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
77    # Inform catalog if certificate has changed
78    # (applyData does this only for the context)
79    if 'certificate' in changed_fields:
80        notify(grok.ObjectModifiedEvent(view.context.student))
81    fields_string = ' + '.join(changed_fields)
82    view.flash(_('Form has been saved.'))
83    if fields_string:
84        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
85    return
86
[7145]87def emit_lock_message(view):
[7642]88    """Flash a lock message.
89    """
[7723]90    view.flash(_('The requested form is locked (read-only).'))
[7133]91    view.redirect(view.url(view.context))
92    return
93
[8921]94def translated_values(view):
95    lang = view.request.cookies.get('kofa.language')
96    for value in view.context.values():
97        value_dict = dict([i for i in value.__dict__.items()])
98        value_dict['mandatory_bool'] = value.mandatory
99        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
100            target_language=lang)
101        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
102            target_language=lang)
103        value_dict['automatic'] = translate(str(value.automatic), 'zope',
104            target_language=lang)
105        yield value_dict
106
[6629]107class StudentsBreadcrumb(Breadcrumb):
108    """A breadcrumb for the students container.
109    """
110    grok.context(IStudentsContainer)
[7723]111    title = _('Students')
[6629]112
[7459]113    @property
114    def target(self):
115        user = get_current_principal()
116        if getattr(user, 'user_type', None) == 'student':
117            return None
118        return self.viewname
119
[6818]120class StudentBreadcrumb(Breadcrumb):
121    """A breadcrumb for the student container.
122    """
123    grok.context(IStudent)
124
125    def title(self):
[7364]126        return self.context.display_fullname
[6818]127
[6635]128class SudyCourseBreadcrumb(Breadcrumb):
129    """A breadcrumb for the student study course.
130    """
131    grok.context(IStudentStudyCourse)
[7723]132    title = _('Study Course')
[6635]133
134class PaymentsBreadcrumb(Breadcrumb):
135    """A breadcrumb for the student payments folder.
136    """
[6859]137    grok.context(IStudentPaymentsContainer)
[7723]138    title = _('Payments')
[6635]139
[6870]140class OnlinePaymentBreadcrumb(Breadcrumb):
[7251]141    """A breadcrumb for payments.
[6870]142    """
[6877]143    grok.context(IStudentOnlinePayment)
[6870]144
145    @property
146    def title(self):
147        return self.context.p_id
148
[6635]149class AccommodationBreadcrumb(Breadcrumb):
150    """A breadcrumb for the student accommodation folder.
151    """
152    grok.context(IStudentAccommodation)
[7723]153    title = _('Accommodation')
[6635]154
[6994]155class BedTicketBreadcrumb(Breadcrumb):
156    """A breadcrumb for bed tickets.
157    """
158    grok.context(IBedTicket)
[7009]159
[6994]160    @property
161    def title(self):
[7723]162        return _('Bed Ticket ${a}',
163            mapping = {'a':self.context.getSessionString()})
[6994]164
[6776]165class StudyLevelBreadcrumb(Breadcrumb):
166    """A breadcrumb for course lists.
167    """
168    grok.context(IStudentStudyLevel)
169
170    @property
171    def title(self):
[7834]172        return self.context.level_title
[6776]173
[7819]174class StudentsContainerPage(KofaPage):
[6626]175    """The standard view for student containers.
[6621]176    """
177    grok.context(IStudentsContainer)
178    grok.name('index')
[7240]179    grok.require('waeup.viewStudentsContainer')
[6695]180    grok.template('containerpage')
[7723]181    label = _('Student Section')
[7735]182    search_button = _('Search')
[6642]183    pnav = 4
[6621]184
[6626]185    def update(self, *args, **kw):
186        datatable.need()
187        form = self.request.form
188        self.hitlist = []
189        if 'searchterm' in form and form['searchterm']:
190            self.searchterm = form['searchterm']
191            self.searchtype = form['searchtype']
192        elif 'old_searchterm' in form:
193            self.searchterm = form['old_searchterm']
194            self.searchtype = form['old_searchtype']
195        else:
196            if 'search' in form:
[7745]197                self.flash(_('Empty search string'))
[6626]198            return
[7068]199        if self.searchtype == 'current_session':
[8081]200            try:
201                self.searchterm = int(self.searchterm)
202            except ValueError:
[8404]203                self.flash(_('Only year dates allowed (e.g. 2011).'))
[8081]204                return
[6626]205        self.hitlist = search(query=self.searchterm,
206            searchtype=self.searchtype, view=self)
207        if not self.hitlist:
[8404]208            self.flash(_('No student found.'))
[6626]209        return
210
[7819]211class StudentsContainerManagePage(KofaPage):
[6626]212    """The manage page for student containers.
[6622]213    """
214    grok.context(IStudentsContainer)
215    grok.name('manage')
[7136]216    grok.require('waeup.manageStudent')
[6695]217    grok.template('containermanagepage')
[6642]218    pnav = 4
[7723]219    label = _('Manage student section')
[7735]220    search_button = _('Search')
221    remove_button = _('Remove selected')
[6622]222
[6626]223    def update(self, *args, **kw):
224        datatable.need()
[6820]225        toggleall.need()
[7329]226        warning.need()
[6626]227        form = self.request.form
228        self.hitlist = []
229        if 'searchterm' in form and form['searchterm']:
230            self.searchterm = form['searchterm']
231            self.searchtype = form['searchtype']
232        elif 'old_searchterm' in form:
233            self.searchterm = form['old_searchterm']
234            self.searchtype = form['old_searchtype']
235        else:
236            if 'search' in form:
[7745]237                self.flash(_('Empty search string'))
[6626]238            return
[8082]239        if self.searchtype == 'current_session':
240            try:
241                self.searchterm = int(self.searchterm)
242            except ValueError:
243                self.flash('Only year dates allowed (e.g. 2011).')
244                return
[6626]245        if not 'entries' in form:
246            self.hitlist = search(query=self.searchterm,
247                searchtype=self.searchtype, view=self)
248            if not self.hitlist:
[7723]249                self.flash(_('No student found.'))
[7459]250            if 'remove' in form:
[7723]251                self.flash(_('No item selected.'))
[6626]252            return
253        entries = form['entries']
254        if isinstance(entries, basestring):
255            entries = [entries]
256        deleted = []
257        for entry in entries:
258            if 'remove' in form:
259                del self.context[entry]
260                deleted.append(entry)
261        self.hitlist = search(query=self.searchterm,
262            searchtype=self.searchtype, view=self)
263        if len(deleted):
[7723]264            self.flash(_('Successfully removed: ${a}',
265                mapping = {'a':', '.join(deleted)}))
[6622]266        return
267
[7819]268class StudentAddFormPage(KofaAddFormPage):
[6622]269    """Add-form to add a student.
270    """
271    grok.context(IStudentsContainer)
[7136]272    grok.require('waeup.manageStudent')
[6622]273    grok.name('addstudent')
[7357]274    form_fields = grok.AutoFields(IStudent).select(
[7520]275        'firstname', 'middlename', 'lastname', 'reg_number')
[7723]276    label = _('Add student')
[6642]277    pnav = 4
[6622]278
[7723]279    @action(_('Create student record'), style='primary')
[6622]280    def addStudent(self, **data):
281        student = createObject(u'waeup.Student')
282        self.applyData(student, **data)
[6652]283        self.context.addStudent(student)
[7723]284        self.flash(_('Student record created.'))
[6651]285        self.redirect(self.url(self.context[student.student_id], 'index'))
[6622]286        return
287
[7819]288class StudentBaseDisplayFormPage(KofaDisplayFormPage):
[6631]289    """ Page to display student base data
290    """
[6622]291    grok.context(IStudent)
292    grok.name('index')
[6660]293    grok.require('waeup.viewStudent')
[6695]294    grok.template('basepage')
[8983]295    form_fields = grok.AutoFields(IStudentBase).omit('password', 'suspended')
[6642]296    pnav = 4
[6622]297
298    @property
299    def label(self):
[8983]300        if self.context.suspended:
301            return _('${a}: Base Data (account suspended)',
302                mapping = {'a':self.context.display_fullname})
303        return  _('${a}: Base Data',
[7723]304            mapping = {'a':self.context.display_fullname})
[6631]305
[6699]306    @property
307    def hasPassword(self):
308        if self.context.password:
[7723]309            return _('set')
310        return _('unset')
[6699]311
[7229]312class ContactStudentForm(ContactAdminForm):
313    grok.context(IStudent)
[7230]314    grok.name('contactstudent')
[7275]315    grok.require('waeup.viewStudent')
[7229]316    pnav = 4
317    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
318
[7275]319    def update(self, subject=u''):
320        self.form_fields.get('subject').field.default = subject
321        self.subject = subject
322        return
323
[7229]324    def label(self):
[7723]325        return _(u'Send message to ${a}',
326            mapping = {'a':self.context.display_fullname})
[7229]327
[7459]328    @action('Send message now', style='primary')
[7229]329    def send(self, *args, **data):
[7234]330        try:
[7403]331            email = self.request.principal.email
[7234]332        except AttributeError:
[7403]333            email = self.config.email_admin
334        usertype = getattr(self.request.principal,
335                           'user_type', 'system').title()
[7819]336        kofa_utils = getUtility(IKofaUtils)
[7811]337        success = kofa_utils.sendContactForm(
[7403]338                self.request.principal.title,email,
339                self.context.display_fullname,self.context.email,
340                self.request.principal.id,usertype,
341                self.config.name,
342                data['body'],data['subject'])
[7229]343        if success:
[7723]344            self.flash(_('Your message has been sent.'))
[7229]345        else:
[7723]346            self.flash(_('An smtp server error occurred.'))
[7229]347        return
348
[7819]349class StudentBaseManageFormPage(KofaEditFormPage):
[7133]350    """ View to manage student base data
[6631]351    """
352    grok.context(IStudent)
[7133]353    grok.name('manage_base')
[7136]354    grok.require('waeup.manageStudent')
[8977]355    form_fields = grok.AutoFields(IStudentBase).omit('student_id', 'adm_code')
[6695]356    grok.template('basemanagepage')
[7723]357    label = _('Manage base data')
[6642]358    pnav = 4
[6631]359
[6638]360    def update(self):
361        datepicker.need() # Enable jQuery datepicker in date fields.
[7134]362        tabs.need()
[7490]363        self.tab1 = self.tab2 = ''
364        qs = self.request.get('QUERY_STRING', '')
365        if not qs:
366            qs = 'tab1'
367        setattr(self, qs, 'active')
[6638]368        super(StudentBaseManageFormPage, self).update()
369        self.wf_info = IWorkflowInfo(self.context)
370        return
371
372    def getTransitions(self):
373        """Return a list of dicts of allowed transition ids and titles.
374
375        Each list entry provides keys ``name`` and ``title`` for
376        internal name and (human readable) title of a single
377        transition.
378        """
[8434]379        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
380            if not t[0].startswith('pay')]
[9028]381        if self.context.is_postgrad:
382            allowed_transitions = [t for t in allowed_transitions
383                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
[7687]384        return [dict(name='', title=_('No transition'))] +[
[6638]385            dict(name=x, title=y) for x, y in allowed_transitions]
386
[7723]387    @action(_('Save'), style='primary')
[6638]388    def save(self, **data):
[6701]389        form = self.request.form
[6790]390        password = form.get('password', None)
391        password_ctl = form.get('control_password', None)
392        if password:
[7147]393            validator = getUtility(IPasswordValidator)
394            errors = validator.validate_password(password, password_ctl)
395            if errors:
396                self.flash( ' '.join(errors))
397                return
398        changed_fields = self.applyData(self.context, **data)
[6771]399        # Turn list of lists into single list
400        if changed_fields:
401            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7147]402        else:
403            changed_fields = []
404        if password:
405            # Now we know that the form has no errors and can set password ...
406            IUserAccount(self.context).setPassword(password)
407            changed_fields.append('password')
408        # ... and execute transition
[6638]409        if form.has_key('transition') and form['transition']:
410            transition_id = form['transition']
411            self.wf_info.fireTransition(transition_id)
[7147]412        fields_string = ' + '.join(changed_fields)
[7723]413        self.flash(_('Form has been saved.'))
[6644]414        if fields_string:
[8735]415            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
[6638]416        return
417
[7819]418class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
[6631]419    """ Page to display student clearance data
420    """
421    grok.context(IStudent)
422    grok.name('view_clearance')
[6660]423    grok.require('waeup.viewStudent')
[6642]424    pnav = 4
[6631]425
426    @property
[8099]427    def separators(self):
428        return getUtility(IStudentsUtils).SEPARATORS_DICT
429
430    @property
[7993]431    def form_fields(self):
[8472]432        if self.context.is_postgrad:
[8977]433            form_fields = grok.AutoFields(
434                IPGStudentClearance).omit('clearance_locked')
[7993]435        else:
[8977]436            form_fields = grok.AutoFields(
437                IUGStudentClearance).omit('clearance_locked')
[7993]438        return form_fields
439
440    @property
[6631]441    def label(self):
[7723]442        return _('${a}: Clearance Data',
443            mapping = {'a':self.context.display_fullname})
[6631]444
[7277]445class ExportPDFClearanceSlipPage(grok.View):
446    """Deliver a PDF slip of the context.
447    """
448    grok.context(IStudent)
449    grok.name('clearance.pdf')
450    grok.require('waeup.viewStudent')
451    prefix = 'form'
452
453    @property
[7993]454    def form_fields(self):
[8472]455        if self.context.is_postgrad:
[8977]456            form_fields = grok.AutoFields(
457                IPGStudentClearance).omit('clearance_locked')
[7993]458        else:
[8977]459            form_fields = grok.AutoFields(
460                IUGStudentClearance).omit('clearance_locked')
[7993]461        return form_fields
462
463    @property
[7723]464    def title(self):
[7819]465        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]466        return translate(_('Clearance Data'), 'waeup.kofa',
[7723]467            target_language=portal_language)
468
469    @property
[7277]470    def label(self):
[7819]471        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[9026]472        return translate(_('Clearance Slip of'),
[7811]473            'waeup.kofa', target_language=portal_language) \
[7723]474            + ' %s' % self.context.display_fullname
[7277]475
[9010]476    def _signatures(self):
477        if self.context.state == CLEARED:
478            return (_('Student Signature'), _('Clearance Officer Signature'))
479        return
480
[7277]481    def render(self):
[8736]482        studentview = StudentBaseDisplayFormPage(self.context.student,
[7277]483            self.request)
484        students_utils = getUtility(IStudentsUtils)
485        return students_utils.renderPDF(
[7318]486            self, 'clearance.pdf',
[9010]487            self.context.student, studentview, signatures=self._signatures())
[7277]488
[7819]489class StudentClearanceManageFormPage(KofaEditFormPage):
[8120]490    """ Page to manage student clearance data
[6631]491    """
492    grok.context(IStudent)
[8119]493    grok.name('manage_clearance')
[7136]494    grok.require('waeup.manageStudent')
[7134]495    grok.template('clearanceeditpage')
[7723]496    label = _('Manage clearance data')
[6642]497    pnav = 4
[6650]498
[7993]499    @property
[8099]500    def separators(self):
501        return getUtility(IStudentsUtils).SEPARATORS_DICT
502
503    @property
[7993]504    def form_fields(self):
[8472]505        if self.context.is_postgrad:
[8977]506            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
[7993]507        else:
[8977]508            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
[7993]509        return form_fields
510
[6650]511    def update(self):
512        datepicker.need() # Enable jQuery datepicker in date fields.
[7134]513        tabs.need()
[7490]514        self.tab1 = self.tab2 = ''
515        qs = self.request.get('QUERY_STRING', '')
516        if not qs:
517            qs = 'tab1'
518        setattr(self, qs, 'active')
[6650]519        return super(StudentClearanceManageFormPage, self).update()
520
[7723]521    @action(_('Save'), style='primary')
[6695]522    def save(self, **data):
[6762]523        msave(self, **data)
[6695]524        return
525
[7459]526class StudentClearPage(UtilityView, grok.View):
[7158]527    """ Clear student by clearance officer
528    """
529    grok.context(IStudent)
530    grok.name('clear')
531    grok.require('waeup.clearStudent')
532
533    def update(self):
534        if self.context.state == REQUESTED:
535            IWorkflowInfo(self.context).fireTransition('clear')
[7723]536            self.flash(_('Student has been cleared.'))
[7158]537        else:
[7723]538            self.flash(_('Student is in wrong state.'))
[7158]539        self.redirect(self.url(self.context,'view_clearance'))
540        return
541
542    def render(self):
543        return
544
[7459]545class StudentRejectClearancePage(UtilityView, grok.View):
[7158]546    """ Reject clearance by clearance officers
547    """
548    grok.context(IStudent)
549    grok.name('reject_clearance')
550    grok.require('waeup.clearStudent')
551
552    def update(self):
553        if self.context.state == CLEARED:
554            IWorkflowInfo(self.context).fireTransition('reset4')
[7723]555            message = _('Clearance has been annulled.')
[7275]556            self.flash(message)
[7158]557        elif self.context.state == REQUESTED:
558            IWorkflowInfo(self.context).fireTransition('reset3')
[7723]559            message = _('Clearance request has been rejected.')
[7275]560            self.flash(message)
[7158]561        else:
[7723]562            self.flash(_('Student is in wrong state.'))
[7334]563            self.redirect(self.url(self.context,'view_clearance'))
[7275]564            return
565        args = {'subject':message}
566        self.redirect(self.url(self.context) +
567            '/contactstudent?%s' % urlencode(args))
[7158]568        return
569
570    def render(self):
571        return
572
[7819]573class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
[6631]574    """ Page to display student personal data
575    """
576    grok.context(IStudent)
577    grok.name('view_personal')
[6660]578    grok.require('waeup.viewStudent')
[6631]579    form_fields = grok.AutoFields(IStudentPersonal)
[7386]580    form_fields['perm_address'].custom_widget = BytesDisplayWidget
[6642]581    pnav = 4
[6631]582
583    @property
584    def label(self):
[7723]585        return _('${a}: Personal Data',
586            mapping = {'a':self.context.display_fullname})
[6631]587
[8903]588class StudentPersonalManageFormPage(KofaEditFormPage):
589    """ Page to manage personal data
[6631]590    """
591    grok.context(IStudent)
[8903]592    grok.name('manage_personal')
593    grok.require('waeup.manageStudent')
[6631]594    form_fields = grok.AutoFields(IStudentPersonal)
[8903]595    label = _('Manage personal data')
[6642]596    pnav = 4
[6631]597
[7723]598    @action(_('Save'), style='primary')
[6762]599    def save(self, **data):
600        msave(self, **data)
601        return
602
[8903]603class StudentPersonalEditFormPage(StudentPersonalManageFormPage):
604    """ Page to edit personal data
605    """
606    grok.name('edit_personal')
607    grok.require('waeup.handleStudent')
608    label = _('Edit personal data')
609    pnav = 4
610
[7819]611class StudyCourseDisplayFormPage(KofaDisplayFormPage):
[6635]612    """ Page to display the student study course data
613    """
614    grok.context(IStudentStudyCourse)
615    grok.name('index')
[6660]616    grok.require('waeup.viewStudent')
[6775]617    grok.template('studycoursepage')
[6642]618    pnav = 4
[6635]619
620    @property
[8972]621    def form_fields(self):
622        if self.context.is_postgrad:
623            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
624                'current_verdict', 'previous_verdict')
625        else:
626            form_fields = grok.AutoFields(IStudentStudyCourse)
627        return form_fields
628
629    @property
[6635]630    def label(self):
[7723]631        return _('${a}: Study Course',
632            mapping = {'a':self.context.__parent__.display_fullname})
[6635]633
[6912]634    @property
635    def current_mode(self):
[7641]636        if self.context.certificate is not None:
[7841]637            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
[7681]638            return studymodes_dict[self.context.certificate.study_mode]
[7171]639        return
[7642]640
[7171]641    @property
642    def department(self):
[7205]643        if self.context.certificate is not None:
[7171]644            return self.context.certificate.__parent__.__parent__
645        return
[6912]646
[7171]647    @property
648    def faculty(self):
[7205]649        if self.context.certificate is not None:
[7171]650            return self.context.certificate.__parent__.__parent__.__parent__
651        return
652
[7819]653class StudyCourseManageFormPage(KofaEditFormPage):
[6649]654    """ Page to edit the student study course data
655    """
656    grok.context(IStudentStudyCourse)
[6775]657    grok.name('manage')
[7136]658    grok.require('waeup.manageStudent')
[6775]659    grok.template('studycoursemanagepage')
[7723]660    label = _('Manage study course')
[6649]661    pnav = 4
[7723]662    taboneactions = [_('Save'),_('Cancel')]
663    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
664    tabthreeactions = [_('Add study level')]
[6649]665
[8972]666    @property
667    def form_fields(self):
668        if self.context.is_postgrad:
669            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
670                'current_verdict', 'previous_verdict')
671        else:
672            form_fields = grok.AutoFields(IStudentStudyCourse)
673        return form_fields
674
[6775]675    def update(self):
676        super(StudyCourseManageFormPage, self).update()
677        tabs.need()
[7484]678        self.tab1 = self.tab2 = ''
679        qs = self.request.get('QUERY_STRING', '')
680        if not qs:
681            qs = 'tab1'
682        setattr(self, qs, 'active')
[7490]683        warning.need()
684        datatable.need()
685        return
[6775]686
[7723]687    @action(_('Save'), style='primary')
[6761]688    def save(self, **data):
[8099]689        try:
690            msave(self, **data)
691        except ConstraintNotSatisfied:
692            # The selected level might not exist in certificate
693            self.flash(_('Current level not available for certificate.'))
694            return
[8081]695        notify(grok.ObjectModifiedEvent(self.context.__parent__))
[6761]696        return
697
[6775]698    @property
699    def level_dict(self):
700        studylevelsource = StudyLevelSource().factory
701        for code in studylevelsource.getValues(self.context):
702            title = studylevelsource.getTitle(self.context, code)
703            yield(dict(code=code, title=title))
704
[7723]705    @action(_('Add study level'))
[6774]706    def addStudyLevel(self, **data):
[6775]707        level_code = self.request.form.get('addlevel', None)
[8323]708        studylevel = createObject(u'waeup.StudentStudyLevel')
[6775]709        studylevel.level = int(level_code)
710        try:
[6782]711            self.context.addStudentStudyLevel(
712                self.context.certificate,studylevel)
[7723]713            self.flash(_('Study level has been added.'))
[6775]714        except KeyError:
[7723]715            self.flash(_('This level exists.'))
[7484]716        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6774]717        return
718
[7723]719    @jsaction(_('Remove selected levels'))
[6775]720    def delStudyLevels(self, **data):
721        form = self.request.form
722        if form.has_key('val_id'):
723            child_id = form['val_id']
724        else:
[7723]725            self.flash(_('No study level selected.'))
[7484]726            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6775]727            return
728        if not isinstance(child_id, list):
729            child_id = [child_id]
730        deleted = []
731        for id in child_id:
[7723]732            del self.context[id]
733            deleted.append(id)
[6775]734        if len(deleted):
[7723]735            self.flash(_('Successfully removed: ${a}',
736                mapping = {'a':', '.join(deleted)}))
[7484]737        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6775]738        return
[6774]739
[7819]740class StudyLevelDisplayFormPage(KofaDisplayFormPage):
[6774]741    """ Page to display student study levels
742    """
743    grok.context(IStudentStudyLevel)
744    grok.name('index')
745    grok.require('waeup.viewStudent')
[6775]746    form_fields = grok.AutoFields(IStudentStudyLevel)
[6783]747    grok.template('studylevelpage')
[6774]748    pnav = 4
749
[7310]750    def update(self):
751        super(StudyLevelDisplayFormPage, self).update()
752        datatable.need()
753        return
754
[6774]755    @property
[8141]756    def translated_values(self):
[8921]757        return translated_values(self)
[8141]758
759    @property
[6774]760    def label(self):
[7833]761        # Here we know that the cookie has been set
762        lang = self.request.cookies.get('kofa.language')
[7811]763        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]764            target_language=lang)
765        return _('${a}: Study Level ${b}', mapping = {
[8736]766            'a':self.context.student.display_fullname,
[7723]767            'b':level_title})
[6774]768
[6803]769    @property
770    def total_credits(self):
771        total_credits = 0
772        for key, val in self.context.items():
773            total_credits += val.credits
774        return total_credits
775
[7459]776class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
[7028]777    """Deliver a PDF slip of the context.
778    """
779    grok.context(IStudentStudyLevel)
780    grok.name('course_registration.pdf')
781    grok.require('waeup.viewStudent')
782    form_fields = grok.AutoFields(IStudentStudyLevel)
783    prefix = 'form'
784
785    @property
[7723]786    def title(self):
[7819]787        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]788        return translate(_('Level Data'), 'waeup.kofa',
[7723]789            target_language=portal_language)
790
791    @property
792    def content_title(self):
[7819]793        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]794        return translate(_('Course List'), 'waeup.kofa',
[7723]795            target_language=portal_language)
796
797    @property
[7028]798    def label(self):
[7819]799        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]800        lang = self.request.cookies.get('kofa.language', portal_language)
801        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]802            target_language=lang)
[8141]803        return translate(_('Course Registration Slip'),
[7811]804            'waeup.kofa', target_language=portal_language) \
[7723]805            + ' %s' % level_title
[7028]806
807    def render(self):
[7819]808        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]809        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
810        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
811        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
812        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
813        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
814        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
815        Mand = translate(_('Mand.'), 'waeup.kofa', target_language=portal_language)
816        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
[8736]817        studentview = StudentBaseDisplayFormPage(self.context.student,
[7028]818            self.request)
[7150]819        students_utils = getUtility(IStudentsUtils)
[7318]820        tabledata = sorted(self.context.values(),
821            key=lambda value: str(value.semester) + value.code)
[7186]822        return students_utils.renderPDF(
[7318]823            self, 'course_registration.pdf',
[8736]824            self.context.student, studentview,
[7723]825            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
826                         (Title,'title', 5),
827                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
828                         (Cred, 'credits', 1.5),
829                         (Mand, 'mandatory', 1.5),
[8141]830                         (Score, 'score', 1.5),
831                         #('Auto', 'automatic', 1.5)
[7304]832                         ],
[7318]833            tabledata=tabledata)
[7028]834
[7819]835class StudyLevelManageFormPage(KofaEditFormPage):
[6792]836    """ Page to edit the student study level data
837    """
838    grok.context(IStudentStudyLevel)
839    grok.name('manage')
[7136]840    grok.require('waeup.manageStudent')
[6792]841    grok.template('studylevelmanagepage')
842    form_fields = grok.AutoFields(IStudentStudyLevel)
843    pnav = 4
[7723]844    taboneactions = [_('Save'),_('Cancel')]
845    tabtwoactions = [_('Add course ticket'),
846        _('Remove selected tickets'),_('Cancel')]
[6792]847
848    def update(self):
849        super(StudyLevelManageFormPage, self).update()
850        tabs.need()
[7484]851        self.tab1 = self.tab2 = ''
852        qs = self.request.get('QUERY_STRING', '')
853        if not qs:
854            qs = 'tab1'
855        setattr(self, qs, 'active')
[7490]856        warning.need()
857        datatable.need()
[6792]858        return
859
860    @property
[8921]861    def translated_values(self):
862        return translated_values(self)
863
864    @property
[6792]865    def label(self):
[7833]866        # Here we know that the cookie has been set
867        lang = self.request.cookies.get('kofa.language')
[7811]868        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]869            target_language=lang)
870        return _('Manage study level ${a}',
871            mapping = {'a':level_title})
[6792]872
[7723]873    @action(_('Save'), style='primary')
[6792]874    def save(self, **data):
875        msave(self, **data)
876        return
877
[7723]878    @action(_('Add course ticket'))
[6795]879    def addCourseTicket(self, **data):
880        self.redirect(self.url(self.context, '@@add'))
[6792]881
[7723]882    @jsaction(_('Remove selected tickets'))
[6792]883    def delCourseTicket(self, **data):
884        form = self.request.form
885        if form.has_key('val_id'):
886            child_id = form['val_id']
887        else:
[7723]888            self.flash(_('No ticket selected.'))
[7484]889            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6792]890            return
891        if not isinstance(child_id, list):
892            child_id = [child_id]
893        deleted = []
894        for id in child_id:
[7723]895            del self.context[id]
896            deleted.append(id)
[6792]897        if len(deleted):
[7723]898            self.flash(_('Successfully removed: ${a}',
899                mapping = {'a':', '.join(deleted)}))
[7484]900        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6792]901        return
902
[7459]903class ValidateCoursesPage(UtilityView, grok.View):
[7334]904    """ Validate course list by course adviser
905    """
906    grok.context(IStudentStudyLevel)
907    grok.name('validate_courses')
908    grok.require('waeup.validateStudent')
909
910    def update(self):
911        if str(self.context.__parent__.current_level) != self.context.__name__:
[7723]912            self.flash(_('This level does not correspond current level.'))
[8736]913        elif self.context.student.state == REGISTERED:
914            IWorkflowInfo(self.context.student).fireTransition(
[7642]915                'validate_courses')
[7723]916            self.flash(_('Course list has been validated.'))
[7334]917        else:
[7723]918            self.flash(_('Student is in the wrong state.'))
[7334]919        self.redirect(self.url(self.context))
920        return
921
922    def render(self):
923        return
924
[7459]925class RejectCoursesPage(UtilityView, grok.View):
[7334]926    """ Reject course list by course adviser
927    """
928    grok.context(IStudentStudyLevel)
929    grok.name('reject_courses')
930    grok.require('waeup.validateStudent')
931
932    def update(self):
933        if str(self.context.__parent__.current_level) != self.context.__name__:
[7723]934            self.flash(_('This level does not correspond current level.'))
[7334]935            self.redirect(self.url(self.context))
936            return
[8736]937        elif self.context.student.state == VALIDATED:
938            IWorkflowInfo(self.context.student).fireTransition('reset8')
[7723]939            message = _('Course list request has been annulled.')
[7334]940            self.flash(message)
[8736]941        elif self.context.student.state == REGISTERED:
942            IWorkflowInfo(self.context.student).fireTransition('reset7')
[7723]943            message = _('Course list request has been rejected:')
[7334]944            self.flash(message)
945        else:
[7723]946            self.flash(_('Student is in the wrong state.'))
[7334]947            self.redirect(self.url(self.context))
948            return
949        args = {'subject':message}
[8736]950        self.redirect(self.url(self.context.student) +
[7334]951            '/contactstudent?%s' % urlencode(args))
952        return
953
954    def render(self):
955        return
956
[7819]957class CourseTicketAddFormPage(KofaAddFormPage):
[6808]958    """Add a course ticket.
[6795]959    """
960    grok.context(IStudentStudyLevel)
961    grok.name('add')
[7136]962    grok.require('waeup.manageStudent')
[7723]963    label = _('Add course ticket')
[6808]964    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
[8141]965        'score', 'automatic', 'carry_over')
[6795]966    pnav = 4
967
[7723]968    @action(_('Add course ticket'))
[6795]969    def addCourseTicket(self, **data):
[8325]970        ticket = createObject(u'waeup.CourseTicket')
[6795]971        course = data['course']
[6802]972        ticket.automatic = False
[8141]973        ticket.carry_over = False
[6795]974        try:
[8920]975            self.context.addCourseTicket(ticket, course)
[6795]976        except KeyError:
[7723]977            self.flash(_('The ticket exists.'))
[6795]978            return
[7723]979        self.flash(_('Successfully added ${a}.',
980            mapping = {'a':ticket.code}))
[7484]981        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6795]982        return
983
[7834]984    @action(_('Cancel'), validator=NullValidator)
[6795]985    def cancel(self, **data):
986        self.redirect(self.url(self.context))
987
[7819]988class CourseTicketDisplayFormPage(KofaDisplayFormPage):
[6796]989    """ Page to display course tickets
990    """
991    grok.context(ICourseTicket)
992    grok.name('index')
993    grok.require('waeup.viewStudent')
994    form_fields = grok.AutoFields(ICourseTicket)
995    grok.template('courseticketpage')
996    pnav = 4
997
998    @property
999    def label(self):
[7723]1000        return _('${a}: Course Ticket ${b}', mapping = {
[8736]1001            'a':self.context.student.display_fullname,
[7723]1002            'b':self.context.code})
[6796]1003
[7819]1004class CourseTicketManageFormPage(KofaEditFormPage):
[6796]1005    """ Page to manage course tickets
1006    """
1007    grok.context(ICourseTicket)
1008    grok.name('manage')
[7136]1009    grok.require('waeup.manageStudent')
[6796]1010    form_fields = grok.AutoFields(ICourseTicket)
1011    grok.template('courseticketmanagepage')
1012    pnav = 4
1013
1014    @property
1015    def label(self):
[7723]1016        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
[6796]1017
[7459]1018    @action('Save', style='primary')
[6796]1019    def save(self, **data):
1020        msave(self, **data)
1021        return
1022
[7819]1023class PaymentsManageFormPage(KofaEditFormPage):
[6869]1024    """ Page to manage the student payments
[7642]1025
1026    This manage form page is for both students and students officers.
[6869]1027    """
1028    grok.context(IStudentPaymentsContainer)
[6940]1029    grok.name('index')
[7181]1030    grok.require('waeup.payStudent')
[6869]1031    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1032    grok.template('paymentsmanagepage')
1033    pnav = 4
1034
[6940]1035    def unremovable(self, ticket):
[7251]1036        usertype = getattr(self.request.principal, 'user_type', None)
1037        if not usertype:
1038            return False
1039        return (self.request.principal.user_type == 'student' and ticket.r_code)
[6940]1040
[6869]1041    @property
1042    def label(self):
[7723]1043        return _('${a}: Payments',
1044            mapping = {'a':self.context.__parent__.display_fullname})
[6869]1045
1046    def update(self):
1047        super(PaymentsManageFormPage, self).update()
1048        datatable.need()
[7329]1049        warning.need()
[6869]1050        return
1051
[7723]1052    @jsaction(_('Remove selected tickets'))
[6869]1053    def delPaymentTicket(self, **data):
1054        form = self.request.form
1055        if form.has_key('val_id'):
1056            child_id = form['val_id']
1057        else:
[7723]1058            self.flash(_('No payment selected.'))
[6940]1059            self.redirect(self.url(self.context))
[6869]1060            return
1061        if not isinstance(child_id, list):
1062            child_id = [child_id]
1063        deleted = []
1064        for id in child_id:
[6992]1065            # Students are not allowed to remove used payment tickets
[6940]1066            if not self.unremovable(self.context[id]):
[7723]1067                del self.context[id]
1068                deleted.append(id)
[6869]1069        if len(deleted):
[7723]1070            self.flash(_('Successfully removed: ${a}',
1071                mapping = {'a': ', '.join(deleted)}))
[8735]1072            self.context.writeLogMessage(
[8885]1073                self,'removed: %s' % ', '.join(deleted))
[6940]1074        self.redirect(self.url(self.context))
[6869]1075        return
1076
[7723]1077    @action(_('Add online payment ticket'))
[6869]1078    def addPaymentTicket(self, **data):
1079        self.redirect(self.url(self.context, '@@addop'))
1080
[7819]1081class OnlinePaymentAddFormPage(KofaAddFormPage):
[6869]1082    """ Page to add an online payment ticket
1083    """
1084    grok.context(IStudentPaymentsContainer)
1085    grok.name('addop')
[7181]1086    grok.require('waeup.payStudent')
[6877]1087    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
[6869]1088        'p_category')
[7723]1089    label = _('Add online payment')
[6869]1090    pnav = 4
[7642]1091
[7723]1092    @action(_('Create ticket'), style='primary')
[6869]1093    def createTicket(self, **data):
[7024]1094        p_category = data['p_category']
1095        student = self.context.__parent__
1096        if p_category == 'bed_allocation' and student[
1097            'studycourse'].current_session != grok.getSite()[
[8685]1098            'hostels'].accommodation_session:
[7024]1099                self.flash(
[7723]1100                    _('Your current session does not match ' + \
1101                    'accommodation session.'))
[7024]1102                self.redirect(self.url(self.context))
1103                return
[7150]1104        students_utils = getUtility(IStudentsUtils)
[8595]1105        error, payment = students_utils.setPaymentDetails(p_category, student)
1106        if error is not None:
1107            self.flash(error)
[8081]1108            self.redirect(self.url(self.context))
1109            return
[6869]1110        self.context[payment.p_id] = payment
[7723]1111        self.flash(_('Payment ticket created.'))
[6940]1112        self.redirect(self.url(self.context))
[6869]1113        return
1114
[7819]1115class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
[6869]1116    """ Page to view an online payment ticket
1117    """
[6877]1118    grok.context(IStudentOnlinePayment)
[6869]1119    grok.name('index')
1120    grok.require('waeup.viewStudent')
[6877]1121    form_fields = grok.AutoFields(IStudentOnlinePayment)
[8170]1122    form_fields[
1123        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1124    form_fields[
1125        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6869]1126    pnav = 4
1127
1128    @property
1129    def label(self):
[7723]1130        return _('${a}: Online Payment Ticket ${b}', mapping = {
[8736]1131            'a':self.context.student.display_fullname,
[7723]1132            'b':self.context.p_id})
[6869]1133
[8420]1134class OnlinePaymentApprovePage(UtilityView, grok.View):
[6930]1135    """ Callback view
1136    """
1137    grok.context(IStudentOnlinePayment)
[8420]1138    grok.name('approve')
1139    grok.require('waeup.managePortal')
[6930]1140
1141    def update(self):
[8428]1142        success, msg, log = self.context.approveStudentPayment()
1143        if log is not None:
[8735]1144            self.context.writeLogMessage(self,log)
[8420]1145        self.flash(msg)
[6940]1146        return
[6930]1147
1148    def render(self):
[6940]1149        self.redirect(self.url(self.context, '@@index'))
[6930]1150        return
1151
[8420]1152class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1153    """ Approval view for students.
1154
1155    This view is used for browser tests only and
1156    must be neutralized in custom pages!
1157    """
1158
1159    grok.name('fake_approve')
1160    grok.require('waeup.payStudent')
1161
[7459]1162class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7019]1163    """Deliver a PDF slip of the context.
1164    """
1165    grok.context(IStudentOnlinePayment)
[8262]1166    grok.name('payment_slip.pdf')
[7019]1167    grok.require('waeup.viewStudent')
1168    form_fields = grok.AutoFields(IStudentOnlinePayment)
[8173]1169    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1170    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7019]1171    prefix = 'form'
[8258]1172    note = None
[7019]1173
1174    @property
[8262]1175    def title(self):
1176        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1177        return translate(_('Payment Data'), 'waeup.kofa',
1178            target_language=portal_language)
1179
1180    @property
[7019]1181    def label(self):
[8262]1182        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1183        return translate(_('Online Payment Slip'),
1184            'waeup.kofa', target_language=portal_language) \
1185            + ' %s' % self.context.p_id
[7019]1186
1187    def render(self):
[8262]1188        #if self.context.p_state != 'paid':
1189        #    self.flash('Ticket not yet paid.')
1190        #    self.redirect(self.url(self.context))
1191        #    return
[8736]1192        studentview = StudentBaseDisplayFormPage(self.context.student,
[7019]1193            self.request)
[7150]1194        students_utils = getUtility(IStudentsUtils)
[8262]1195        return students_utils.renderPDF(self, 'payment_slip.pdf',
[8736]1196            self.context.student, studentview, note=self.note)
[7019]1197
[6992]1198
[7819]1199class AccommodationManageFormPage(KofaEditFormPage):
[7009]1200    """ Page to manage bed tickets.
[7642]1201
1202    This manage form page is for both students and students officers.
[6635]1203    """
1204    grok.context(IStudentAccommodation)
1205    grok.name('index')
[7181]1206    grok.require('waeup.handleAccommodation')
[6635]1207    form_fields = grok.AutoFields(IStudentAccommodation)
[6992]1208    grok.template('accommodationmanagepage')
[6642]1209    pnav = 4
[7723]1210    officers_only_actions = [_('Remove selected')]
[6635]1211
1212    @property
1213    def label(self):
[7723]1214        return _('${a}: Accommodation',
1215            mapping = {'a':self.context.__parent__.display_fullname})
[6637]1216
[6992]1217    def update(self):
1218        super(AccommodationManageFormPage, self).update()
1219        datatable.need()
[7329]1220        warning.need()
[6992]1221        return
1222
[7723]1223    @jsaction(_('Remove selected'))
[7009]1224    def delBedTickets(self, **data):
[7240]1225        if getattr(self.request.principal, 'user_type', None) == 'student':
[7723]1226            self.flash(_('You are not allowed to remove bed tickets.'))
[7017]1227            self.redirect(self.url(self.context))
1228            return
[6992]1229        form = self.request.form
1230        if form.has_key('val_id'):
1231            child_id = form['val_id']
1232        else:
[7723]1233            self.flash(_('No bed ticket selected.'))
[6992]1234            self.redirect(self.url(self.context))
1235            return
1236        if not isinstance(child_id, list):
1237            child_id = [child_id]
1238        deleted = []
1239        for id in child_id:
[7068]1240            del self.context[id]
1241            deleted.append(id)
[6992]1242        if len(deleted):
[7723]1243            self.flash(_('Successfully removed: ${a}',
1244                mapping = {'a':', '.join(deleted)}))
[8735]1245            self.context.writeLogMessage(
1246                self,'removed: % s' % ', '.join(deleted))
[6992]1247        self.redirect(self.url(self.context))
1248        return
1249
[7009]1250    @property
1251    def selected_actions(self):
[7240]1252        if getattr(self.request.principal, 'user_type', None) == 'student':
[7642]1253            return [action for action in self.actions
1254                    if not action.label in self.officers_only_actions]
1255        return self.actions
[7009]1256
[7819]1257class BedTicketAddPage(KofaPage):
[6992]1258    """ Page to add an online payment ticket
1259    """
1260    grok.context(IStudentAccommodation)
1261    grok.name('add')
[7181]1262    grok.require('waeup.handleAccommodation')
[6992]1263    grok.template('enterpin')
[6993]1264    ac_prefix = 'HOS'
[7723]1265    label = _('Add bed ticket')
[6992]1266    pnav = 4
[7723]1267    buttonname = _('Create bed ticket')
[6993]1268    notice = ''
[6992]1269
1270    def update(self, SUBMIT=None):
[8736]1271        student = self.context.student
[7150]1272        students_utils = getUtility(IStudentsUtils)
[7186]1273        acc_details  = students_utils.getAccommodationDetails(student)
[8688]1274        if acc_details.get('expired', False):
1275            startdate = acc_details.get('startdate')
1276            enddate = acc_details.get('enddate')
1277            if startdate and enddate:
1278                tz = getUtility(IKofaUtils).tzinfo
1279                startdate = to_timezone(
1280                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1281                enddate = to_timezone(
1282                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1283                self.flash(_("Outside booking period: ${a} - ${b}",
1284                    mapping = {'a': startdate, 'b': enddate}))
1285            else:
1286                self.flash(_("Outside booking period."))
1287            self.redirect(self.url(self.context))
1288            return
[7369]1289        if not acc_details:
[7723]1290            self.flash(_("Your data are incomplete."))
[7369]1291            self.redirect(self.url(self.context))
1292            return
[6996]1293        if not student.state in acc_details['allowed_states']:
[7723]1294            self.flash(_("You are in the wrong registration state."))
[6992]1295            self.redirect(self.url(self.context))
1296            return
[7642]1297        if student['studycourse'].current_session != acc_details[
1298            'booking_session']:
[7061]1299            self.flash(
[7723]1300                _('Your current session does not match accommodation session.'))
[7061]1301            self.redirect(self.url(self.context))
1302            return
1303        if str(acc_details['booking_session']) in self.context.keys():
[7642]1304            self.flash(
[7723]1305                _('You already booked a bed space in current ' \
1306                    + 'accommodation session.'))
[7004]1307            self.redirect(self.url(self.context))
1308            return
[6992]1309        self.ac_series = self.request.form.get('ac_series', None)
1310        self.ac_number = self.request.form.get('ac_number', None)
1311        if SUBMIT is None:
1312            return
1313        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1314        code = get_access_code(pin)
1315        if not code:
[7723]1316            self.flash(_('Activation code is invalid.'))
[6992]1317            return
[7060]1318        # Search and book bed
[6997]1319        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1320        entries = cat.searchResults(
[7003]1321            owner=(student.student_id,student.student_id))
1322        if len(entries):
[7060]1323            # If bed space has bee manually allocated use this bed
[7003]1324            bed = [entry for entry in entries][0]
[7060]1325        else:
1326            # else search for other available beds
1327            entries = cat.searchResults(
1328                bed_type=(acc_details['bt'],acc_details['bt']))
1329            available_beds = [
1330                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1331            if available_beds:
[7150]1332                students_utils = getUtility(IStudentsUtils)
[7186]1333                bed = students_utils.selectBed(available_beds)
[7060]1334                bed.bookBed(student.student_id)
1335            else:
[7723]1336                self.flash(_('There is no free bed in your category ${a}.',
1337                    mapping = {'a':acc_details['bt']}))
[7060]1338                return
[6992]1339        # Mark pin as used (this also fires a pin related transition)
1340        if code.state == USED:
[7723]1341            self.flash(_('Activation code has already been used.'))
[6992]1342            return
1343        else:
[7723]1344            comment = _(u'invalidated')
[6992]1345            # Here we know that the ac is in state initialized so we do not
1346            # expect an exception, but the owner might be different
1347            if not invalidate_accesscode(
[8736]1348                pin,comment,self.context.student.student_id):
[7723]1349                self.flash(_('You are not the owner of this access code.'))
[6992]1350                return
[7060]1351        # Create bed ticket
[6992]1352        bedticket = createObject(u'waeup.BedTicket')
1353        bedticket.booking_code = pin
[6994]1354        bedticket.booking_session = acc_details['booking_session']
[6996]1355        bedticket.bed_type = acc_details['bt']
[7006]1356        bedticket.bed = bed
[6996]1357        hall_title = bed.__parent__.hostel_name
1358        coordinates = bed.getBedCoordinates()[1:]
1359        block, room_nr, bed_nr = coordinates
[7723]1360        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1361            'a':hall_title, 'b':block,
1362            'c':room_nr, 'd':bed_nr,
1363            'e':bed.bed_type})
[7819]1364        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7723]1365        bedticket.bed_coordinates = translate(
[7811]1366            bc, 'waeup.kofa',target_language=portal_language)
[6996]1367        key = str(acc_details['booking_session'])
1368        self.context[key] = bedticket
[7723]1369        self.flash(_('Bed ticket created and bed booked: ${a}',
1370            mapping = {'a':bedticket.bed_coordinates}))
[6992]1371        self.redirect(self.url(self.context))
1372        return
1373
[7819]1374class BedTicketDisplayFormPage(KofaDisplayFormPage):
[6994]1375    """ Page to display bed tickets
1376    """
1377    grok.context(IBedTicket)
1378    grok.name('index')
[7181]1379    grok.require('waeup.handleAccommodation')
[6994]1380    form_fields = grok.AutoFields(IBedTicket)
1381    pnav = 4
1382
1383    @property
1384    def label(self):
[7723]1385        return _('Bed Ticket for Session ${a}',
1386            mapping = {'a':self.context.getSessionString()})
[6994]1387
[7459]1388class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
[7027]1389    """Deliver a PDF slip of the context.
1390    """
1391    grok.context(IBedTicket)
1392    grok.name('bed_allocation.pdf')
[7181]1393    grok.require('waeup.handleAccommodation')
[7027]1394    form_fields = grok.AutoFields(IBedTicket)
[8173]1395    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[7027]1396    prefix = 'form'
1397
1398    @property
[7723]1399    def title(self):
[7819]1400        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7811]1401        return translate(_('Bed Allocation Data'), 'waeup.kofa',
[7723]1402            target_language=portal_language)
1403
1404    @property
[7027]1405    def label(self):
[7819]1406        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7723]1407        return translate(_('Bed Allocation: '),
[7811]1408            'waeup.kofa', target_language=portal_language) \
[7723]1409            + ' %s' % self.context.bed_coordinates
[7027]1410
1411    def render(self):
[8736]1412        studentview = StudentBaseDisplayFormPage(self.context.student,
[7027]1413            self.request)
[7150]1414        students_utils = getUtility(IStudentsUtils)
[7186]1415        return students_utils.renderPDF(
[7318]1416            self, 'bed_allocation.pdf',
[8736]1417            self.context.student, studentview)
[7027]1418
[7459]1419class BedTicketRelocationPage(UtilityView, grok.View):
[7015]1420    """ Callback view
1421    """
1422    grok.context(IBedTicket)
1423    grok.name('relocate')
1424    grok.require('waeup.manageHostels')
1425
[7059]1426    # Relocate student if student parameters have changed or the bed_type
1427    # of the bed has changed
[7015]1428    def update(self):
[8736]1429        student = self.context.student
[7150]1430        students_utils = getUtility(IStudentsUtils)
[7186]1431        acc_details  = students_utils.getAccommodationDetails(student)
[7068]1432        if self.context.bed != None and \
1433              'reserved' in self.context.bed.bed_type:
[7723]1434            self.flash(_("Students in reserved beds can't be relocated."))
[7068]1435            self.redirect(self.url(self.context))
1436            return
[7059]1437        if acc_details['bt'] == self.context.bed_type and \
[7068]1438                self.context.bed != None and \
[7059]1439                self.context.bed.bed_type == self.context.bed_type:
[7723]1440            self.flash(_("Student can't be relocated."))
[7068]1441            self.redirect(self.url(self.context))
[7015]1442            return
[7068]1443        # Search a bed
[7015]1444        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1445        entries = cat.searchResults(
[7068]1446            owner=(student.student_id,student.student_id))
1447        if len(entries) and self.context.bed == None:
1448            # If booking has been cancelled but other bed space has been
1449            # manually allocated after cancellation use this bed
1450            new_bed = [entry for entry in entries][0]
1451        else:
1452            # Search for other available beds
1453            entries = cat.searchResults(
1454                bed_type=(acc_details['bt'],acc_details['bt']))
1455            available_beds = [
1456                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1457            if available_beds:
[7150]1458                students_utils = getUtility(IStudentsUtils)
[7186]1459                new_bed = students_utils.selectBed(available_beds)
[7068]1460                new_bed.bookBed(student.student_id)
1461            else:
[7723]1462                self.flash(_('There is no free bed in your category ${a}.',
1463                    mapping = {'a':acc_details['bt']}))
[7068]1464                self.redirect(self.url(self.context))
1465                return
[7642]1466        # Release old bed if exists
[7068]1467        if self.context.bed != None:
1468            self.context.bed.owner = NOT_OCCUPIED
1469            notify(grok.ObjectModifiedEvent(self.context.bed))
[7015]1470        # Alocate new bed
1471        self.context.bed_type = acc_details['bt']
[7068]1472        self.context.bed = new_bed
1473        hall_title = new_bed.__parent__.hostel_name
1474        coordinates = new_bed.getBedCoordinates()[1:]
[7015]1475        block, room_nr, bed_nr = coordinates
[7723]1476        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1477            'a':hall_title, 'b':block,
1478            'c':room_nr, 'd':bed_nr,
1479            'e':new_bed.bed_type})
[7819]1480        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
[7723]1481        self.context.bed_coordinates = translate(
[7811]1482            bc, 'waeup.kofa',target_language=portal_language)
[7723]1483        self.flash(_('Student relocated: ${a}',
1484            mapping = {'a':self.context.bed_coordinates}))
[7015]1485        self.redirect(self.url(self.context))
1486        return
1487
1488    def render(self):
1489        return
1490
[7819]1491class StudentHistoryPage(KofaPage):
[6637]1492    """ Page to display student clearance data
1493    """
1494    grok.context(IStudent)
1495    grok.name('history')
[6660]1496    grok.require('waeup.viewStudent')
[6637]1497    grok.template('studenthistory')
[6642]1498    pnav = 4
[6637]1499
1500    @property
1501    def label(self):
[7723]1502        return _('${a}: History', mapping = {'a':self.context.display_fullname})
[6694]1503
1504# Pages for students only
1505
[7819]1506class StudentBaseEditFormPage(KofaEditFormPage):
[7133]1507    """ View to edit student base data
1508    """
1509    grok.context(IStudent)
1510    grok.name('edit_base')
1511    grok.require('waeup.handleStudent')
1512    form_fields = grok.AutoFields(IStudentBase).select(
1513        'email', 'phone')
[7723]1514    label = _('Edit base data')
[7133]1515    pnav = 4
1516
[7723]1517    @action(_('Save'), style='primary')
[7133]1518    def save(self, **data):
1519        msave(self, **data)
1520        return
1521
[7819]1522class StudentChangePasswordPage(KofaEditFormPage):
[7144]1523    """ View to manage student base data
[6756]1524    """
1525    grok.context(IStudent)
[7114]1526    grok.name('change_password')
[6694]1527    grok.require('waeup.handleStudent')
[7144]1528    grok.template('change_password')
[7723]1529    label = _('Change password')
[6694]1530    pnav = 4
1531
[7723]1532    @action(_('Save'), style='primary')
[7144]1533    def save(self, **data):
1534        form = self.request.form
1535        password = form.get('change_password', None)
1536        password_ctl = form.get('change_password_repeat', None)
1537        if password:
[7147]1538            validator = getUtility(IPasswordValidator)
1539            errors = validator.validate_password(password, password_ctl)
1540            if not errors:
1541                IUserAccount(self.context).setPassword(password)
[8735]1542                self.context.writeLogMessage(self, 'saved: password')
[7723]1543                self.flash(_('Password changed.'))
[6756]1544            else:
[7147]1545                self.flash( ' '.join(errors))
[6756]1546        return
1547
[7819]1548class StudentFilesUploadPage(KofaPage):
[7114]1549    """ View to upload files by student
1550    """
1551    grok.context(IStudent)
1552    grok.name('change_portrait')
[7127]1553    grok.require('waeup.uploadStudentFile')
[7114]1554    grok.template('filesuploadpage')
[7723]1555    label = _('Upload portrait')
[7114]1556    pnav = 4
1557
[7133]1558    def update(self):
[8736]1559        if self.context.student.state != ADMITTED:
[7145]1560            emit_lock_message(self)
[7133]1561            return
1562        super(StudentFilesUploadPage, self).update()
1563        return
1564
[7819]1565class StartClearancePage(KofaPage):
[6770]1566    grok.context(IStudent)
1567    grok.name('start_clearance')
1568    grok.require('waeup.handleStudent')
1569    grok.template('enterpin')
[7723]1570    label = _('Start clearance')
[6770]1571    ac_prefix = 'CLR'
1572    notice = ''
1573    pnav = 4
[7723]1574    buttonname = _('Start clearance now')
[6770]1575
[7133]1576    @property
1577    def all_required_fields_filled(self):
1578        if self.context.email and self.context.phone:
1579            return True
1580        return False
1581
1582    @property
1583    def portrait_uploaded(self):
1584        store = getUtility(IExtFileStore)
1585        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1586            return True
1587        return False
1588
[6770]1589    def update(self, SUBMIT=None):
[7671]1590        if not self.context.state == ADMITTED:
[7745]1591            self.flash(_("Wrong state"))
[6936]1592            self.redirect(self.url(self.context))
1593            return
[7133]1594        if not self.portrait_uploaded:
[7723]1595            self.flash(_("No portrait uploaded."))
[7133]1596            self.redirect(self.url(self.context, 'change_portrait'))
1597            return
1598        if not self.all_required_fields_filled:
[7723]1599            self.flash(_("Not all required fields filled."))
[7133]1600            self.redirect(self.url(self.context, 'edit_base'))
1601            return
[6770]1602        self.ac_series = self.request.form.get('ac_series', None)
1603        self.ac_number = self.request.form.get('ac_number', None)
1604
1605        if SUBMIT is None:
1606            return
1607        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1608        code = get_access_code(pin)
1609        if not code:
[7723]1610            self.flash(_('Activation code is invalid.'))
[6770]1611            return
1612        if code.state == USED:
[7723]1613            self.flash(_('Activation code has already been used.'))
[6770]1614            return
[8974]1615        # Mark pin as used (this also fires a pin related transition)
1616        # and fire transition start_clearance
1617        comment = _(u"invalidated")
1618        # Here we know that the ac is in state initialized so we do not
1619        # expect an exception, but the owner might be different
1620        if not invalidate_accesscode(pin, comment, self.context.student_id):
1621            self.flash(_('You are not the owner of this access code.'))
1622            return
1623        self.context.clr_code = pin
[6770]1624        IWorkflowInfo(self.context).fireTransition('start_clearance')
[7723]1625        self.flash(_('Clearance process has been started.'))
[6770]1626        self.redirect(self.url(self.context,'cedit'))
1627        return
1628
[6695]1629class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1630    """ View to edit student clearance data by student
1631    """
1632    grok.context(IStudent)
1633    grok.name('cedit')
1634    grok.require('waeup.handleStudent')
[7723]1635    label = _('Edit clearance data')
[6718]1636
[7993]1637    @property
1638    def form_fields(self):
[8472]1639        if self.context.is_postgrad:
[8974]1640            form_fields = grok.AutoFields(IPGStudentClearance).omit(
[8977]1641                'clearance_locked', 'clr_code')
[7993]1642        else:
[8974]1643            form_fields = grok.AutoFields(IUGStudentClearance).omit(
[8977]1644                'clearance_locked', 'clr_code')
[7993]1645        return form_fields
1646
[6718]1647    def update(self):
1648        if self.context.clearance_locked:
[7145]1649            emit_lock_message(self)
[6718]1650            return
1651        return super(StudentClearanceEditFormPage, self).update()
[6719]1652
[7723]1653    @action(_('Save'), style='primary')
[6722]1654    def save(self, **data):
1655        self.applyData(self.context, **data)
[7723]1656        self.flash(_('Clearance form has been saved.'))
[6722]1657        return
1658
[7253]1659    def dataNotComplete(self):
[7642]1660        """To be implemented in the customization package.
1661        """
[7253]1662        return False
1663
[7723]1664    @action(_('Save and request clearance'), style='primary')
[7186]1665    def requestClearance(self, **data):
[6722]1666        self.applyData(self.context, **data)
[7253]1667        if self.dataNotComplete():
1668            self.flash(self.dataNotComplete())
1669            return
[7723]1670        self.flash(_('Clearance form has been saved.'))
[9021]1671        if self.context.clr_code:
1672            self.redirect(self.url(self.context, 'request_clearance'))
1673        else:
1674            # We bypass the request_clearance page if student
1675            # has been imported in state 'clearance started' and
1676            # no clr_code was entered before.
1677            state = IWorkflowState(self.context).getState()
1678            if state != CLEARANCE:
1679                # This shouldn't happen, but the application officer
1680                # might have forgotten to lock the form after changing the state
1681                self.flash(_('This form cannot be submitted. Wrong state!'))
1682                return
1683            IWorkflowInfo(self.context).fireTransition('request_clearance')
1684            self.flash(_('Clearance has been requested.'))
1685            self.redirect(self.url(self.context))
[6722]1686        return
1687
[7819]1688class RequestClearancePage(KofaPage):
[6769]1689    grok.context(IStudent)
1690    grok.name('request_clearance')
1691    grok.require('waeup.handleStudent')
1692    grok.template('enterpin')
[7723]1693    label = _('Request clearance')
1694    notice = _('Enter the CLR access code used for starting clearance.')
[6769]1695    ac_prefix = 'CLR'
1696    pnav = 4
[7723]1697    buttonname = _('Request clearance now')
[6769]1698
1699    def update(self, SUBMIT=None):
1700        self.ac_series = self.request.form.get('ac_series', None)
1701        self.ac_number = self.request.form.get('ac_number', None)
1702        if SUBMIT is None:
1703            return
1704        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
[9021]1705        if self.context.clr_code and self.context.clr_code != pin:
[7723]1706            self.flash(_("This isn't your CLR access code."))
[6769]1707            return
1708        state = IWorkflowState(self.context).getState()
1709        if state != CLEARANCE:
[9021]1710            # This shouldn't happen, but the application officer
1711            # might have forgotten to lock the form after changing the state
[7723]1712            self.flash(_('This form cannot be submitted. Wrong state!'))
[6769]1713            return
1714        IWorkflowInfo(self.context).fireTransition('request_clearance')
[7723]1715        self.flash(_('Clearance has been requested.'))
[6769]1716        self.redirect(self.url(self.context))
[6789]1717        return
[6806]1718
[8471]1719class StartSessionPage(KofaPage):
[6944]1720    grok.context(IStudentStudyCourse)
[8471]1721    grok.name('start_session')
[6944]1722    grok.require('waeup.handleStudent')
1723    grok.template('enterpin')
[8471]1724    label = _('Start session')
[6944]1725    ac_prefix = 'SFE'
1726    notice = ''
1727    pnav = 4
[8471]1728    buttonname = _('Start now')
[6944]1729
1730    def update(self, SUBMIT=None):
[8471]1731        if not self.context.next_session_allowed:
1732            self.flash(_("You are not entitled to start session."))
[6944]1733            self.redirect(self.url(self.context))
1734            return
1735        self.ac_series = self.request.form.get('ac_series', None)
1736        self.ac_number = self.request.form.get('ac_number', None)
1737
1738        if SUBMIT is None:
1739            return
1740        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1741        code = get_access_code(pin)
1742        if not code:
[7723]1743            self.flash(_('Activation code is invalid.'))
[6944]1744            return
1745        # Mark pin as used (this also fires a pin related transition)
1746        if code.state == USED:
[7723]1747            self.flash(_('Activation code has already been used.'))
[6944]1748            return
1749        else:
[7723]1750            comment = _(u"invalidated")
[6944]1751            # Here we know that the ac is in state initialized so we do not
[8471]1752            # expect an error, but the owner might be different
[6944]1753            if not invalidate_accesscode(
[8736]1754                pin,comment,self.context.student.student_id):
[7723]1755                self.flash(_('You are not the owner of this access code.'))
[6944]1756                return
[8736]1757        if self.context.student.state == CLEARED:
1758            IWorkflowInfo(self.context.student).fireTransition(
[6944]1759                'pay_first_school_fee')
[8736]1760        elif self.context.student.state == RETURNING:
1761            IWorkflowInfo(self.context.student).fireTransition(
[6944]1762                'pay_school_fee')
[8736]1763        elif self.context.student.state == PAID:
1764            IWorkflowInfo(self.context.student).fireTransition(
[8471]1765                'pay_pg_fee')
1766        self.flash(_('Session started.'))
[6944]1767        self.redirect(self.url(self.context))
1768        return
1769
[7819]1770class AddStudyLevelFormPage(KofaEditFormPage):
[6806]1771    """ Page for students to add current study levels
1772    """
1773    grok.context(IStudentStudyCourse)
1774    grok.name('add')
1775    grok.require('waeup.handleStudent')
1776    grok.template('studyleveladdpage')
1777    form_fields = grok.AutoFields(IStudentStudyCourse)
1778    pnav = 4
1779
1780    @property
1781    def label(self):
1782        studylevelsource = StudyLevelSource().factory
1783        code = self.context.current_level
1784        title = studylevelsource.getTitle(self.context, code)
[7723]1785        return _('Add current level ${a}', mapping = {'a':title})
[6806]1786
1787    def update(self):
[8736]1788        if self.context.student.state != PAID:
[7145]1789            emit_lock_message(self)
[6806]1790            return
1791        super(AddStudyLevelFormPage, self).update()
1792        return
1793
[7723]1794    @action(_('Create course list now'), style='primary')
[6806]1795    def addStudyLevel(self, **data):
[8323]1796        studylevel = createObject(u'waeup.StudentStudyLevel')
[6806]1797        studylevel.level = self.context.current_level
1798        studylevel.level_session = self.context.current_session
1799        try:
1800            self.context.addStudentStudyLevel(
1801                self.context.certificate,studylevel)
1802        except KeyError:
[7723]1803            self.flash(_('This level exists.'))
[6806]1804        self.redirect(self.url(self.context))
1805        return
[6808]1806
[7819]1807class StudyLevelEditFormPage(KofaEditFormPage):
[6808]1808    """ Page to edit the student study level data by students
1809    """
1810    grok.context(IStudentStudyLevel)
1811    grok.name('edit')
1812    grok.require('waeup.handleStudent')
1813    grok.template('studyleveleditpage')
1814    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1815        'level_session', 'level_verdict')
1816    pnav = 4
[8642]1817    max_credits = 50
[6808]1818
1819    def update(self):
[8736]1820        if self.context.student.state != PAID:
[7539]1821            emit_lock_message(self)
1822            return
[6808]1823        super(StudyLevelEditFormPage, self).update()
1824        datatable.need()
[7329]1825        warning.need()
[6808]1826        return
1827
1828    @property
1829    def label(self):
[7833]1830        # Here we know that the cookie has been set
1831        lang = self.request.cookies.get('kofa.language')
[7811]1832        level_title = translate(self.context.level_title, 'waeup.kofa',
[7723]1833            target_language=lang)
[8920]1834        return _('Edit course list of ${a}',
[7723]1835            mapping = {'a':level_title})
[6808]1836
1837    @property
1838    def total_credits(self):
1839        total_credits = 0
1840        for key, val in self.context.items():
1841            total_credits += val.credits
1842        return total_credits
1843
[8921]1844    @property
1845    def translated_values(self):
1846        return translated_values(self)
1847
[7723]1848    @action(_('Add course ticket'))
[6808]1849    def addCourseTicket(self, **data):
1850        self.redirect(self.url(self.context, 'ctadd'))
1851
[7723]1852    @jsaction(_('Remove selected tickets'))
[6808]1853    def delCourseTicket(self, **data):
1854        form = self.request.form
1855        if form.has_key('val_id'):
1856            child_id = form['val_id']
1857        else:
[7723]1858            self.flash(_('No ticket selected.'))
[6808]1859            self.redirect(self.url(self.context, '@@edit'))
1860            return
1861        if not isinstance(child_id, list):
1862            child_id = [child_id]
1863        deleted = []
1864        for id in child_id:
[6940]1865            # Students are not allowed to remove core tickets
[7665]1866            if not self.context[id].mandatory:
[7723]1867                del self.context[id]
1868                deleted.append(id)
[6808]1869        if len(deleted):
[7723]1870            self.flash(_('Successfully removed: ${a}',
1871                mapping = {'a':', '.join(deleted)}))
[6808]1872        self.redirect(self.url(self.context, u'@@edit'))
1873        return
1874
[7723]1875    @action(_('Register course list'), style='primary')
[8481]1876    def registerCourses(self, **data):
[8642]1877        if self.total_credits > self.max_credits:
1878            self.flash(_('Maximum credits of ${a} exceeded.',
1879                mapping = {'a':self.max_credits}))
1880            return
[8736]1881        IWorkflowInfo(self.context.student).fireTransition(
[7642]1882            'register_courses')
[7723]1883        self.flash(_('Course list has been registered.'))
[6810]1884        self.redirect(self.url(self.context))
1885        return
1886
[6808]1887class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1888    """Add a course ticket by student.
1889    """
1890    grok.name('ctadd')
1891    grok.require('waeup.handleStudent')
1892    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
[8141]1893        'score', 'mandatory', 'automatic', 'carry_over')
[6808]1894
[7539]1895    def update(self):
[8736]1896        if self.context.student.state != PAID:
[7539]1897            emit_lock_message(self)
1898            return
1899        super(CourseTicketAddFormPage2, self).update()
1900        return
1901
[7723]1902    @action(_('Add course ticket'))
[6808]1903    def addCourseTicket(self, **data):
[7642]1904        # Safety belt
[8736]1905        if self.context.student.state != PAID:
[7539]1906            return
[8325]1907        ticket = createObject(u'waeup.CourseTicket')
[6808]1908        course = data['course']
1909        ticket.automatic = False
[8920]1910        ticket.carry_over = False
[6808]1911        try:
[8920]1912            self.context.addCourseTicket(ticket, course)
[6808]1913        except KeyError:
[7723]1914            self.flash(_('The ticket exists.'))
[6808]1915            return
[7723]1916        self.flash(_('Successfully added ${a}.',
1917            mapping = {'a':ticket.code}))
[6808]1918        self.redirect(self.url(self.context, u'@@edit'))
1919        return
[7369]1920
1921
[7819]1922class SetPasswordPage(KofaPage):
1923    grok.context(IKofaObject)
[7660]1924    grok.name('setpassword')
1925    grok.require('waeup.Anonymous')
1926    grok.template('setpassword')
[7723]1927    label = _('Set password for first-time login')
[7660]1928    ac_prefix = 'PWD'
1929    pnav = 0
[7738]1930    set_button = _('Set')
[7660]1931
1932    def update(self, SUBMIT=None):
1933        self.reg_number = self.request.form.get('reg_number', None)
1934        self.ac_series = self.request.form.get('ac_series', None)
1935        self.ac_number = self.request.form.get('ac_number', None)
1936
1937        if SUBMIT is None:
1938            return
1939        hitlist = search(query=self.reg_number,
1940            searchtype='reg_number', view=self)
1941        if not hitlist:
[7723]1942            self.flash(_('No student found.'))
[7660]1943            return
1944        if len(hitlist) != 1:   # Cannot happen but anyway
[7723]1945            self.flash(_('More than one student found.'))
[7660]1946            return
1947        student = hitlist[0].context
1948        self.student_id = student.student_id
1949        student_pw = student.password
1950        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1951        code = get_access_code(pin)
1952        if not code:
[7723]1953            self.flash(_('Access code is invalid.'))
[7660]1954            return
1955        if student_pw and pin == student.adm_code:
[7723]1956            self.flash(_(
1957                'Password has already been set. Your Student Id is ${a}',
1958                mapping = {'a':self.student_id}))
[7660]1959            return
1960        elif student_pw:
1961            self.flash(
[7723]1962                _('Password has already been set. You are using the ' +
1963                'wrong Access Code.'))
[7660]1964            return
1965        # Mark pin as used (this also fires a pin related transition)
1966        # and set student password
1967        if code.state == USED:
[7723]1968            self.flash(_('Access code has already been used.'))
[7660]1969            return
1970        else:
[7723]1971            comment = _(u"invalidated")
[7660]1972            # Here we know that the ac is in state initialized so we do not
1973            # expect an exception
1974            invalidate_accesscode(pin,comment)
1975            IUserAccount(student).setPassword(self.ac_number)
1976            student.adm_code = pin
[7723]1977        self.flash(_('Password has been set. Your Student Id is ${a}',
1978            mapping = {'a':self.student_id}))
[7811]1979        return
[8779]1980
1981class StudentRequestPasswordPage(KofaAddFormPage):
1982    """Captcha'd registration page for applicants.
1983    """
1984    grok.name('requestpw')
1985    grok.require('waeup.Anonymous')
1986    grok.template('requestpw')
1987    form_fields = grok.AutoFields(IStudentRequestPW).select(
[8854]1988        'firstname','number','email')
[8779]1989    label = _('Request password for first-time login')
1990
1991    def update(self):
1992        # Handle captcha
1993        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1994        self.captcha_result = self.captcha.verify(self.request)
1995        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1996        return
1997
1998    def _redirect(self, email, password, student_id):
1999        # Forward only email to landing page in base package.
2000        self.redirect(self.url(self.context, 'requestpw_complete',
2001            data = dict(email=email)))
2002        return
2003
2004    def _pw_used(self):
[8780]2005        # XXX: False if password has not been used. We need an extra
2006        #      attribute which remembers if student logged in.
[8779]2007        return True
2008
[8854]2009    @action(_('Send login credentials to email address'), style='primary')
[8779]2010    def get_credentials(self, **data):
2011        if not self.captcha_result.is_valid:
2012            # Captcha will display error messages automatically.
2013            # No need to flash something.
2014            return
[8854]2015        number = data.get('number','')
[8779]2016        firstname = data.get('firstname','')
2017        cat = getUtility(ICatalog, name='students_catalog')
2018        results = list(
[8854]2019            cat.searchResults(reg_number=(number, number)))
2020        if not results:
2021            results = list(
2022                cat.searchResults(matric_number=(number, number)))
[8779]2023        if results:
2024            student = results[0]
2025            if getattr(student,'firstname',None) is None:
2026                self.flash(_('An error occurred.'))
2027                return
2028            elif student.firstname.lower() != firstname.lower():
2029                # Don't tell the truth here. Anonymous must not
2030                # know that a record was found and only the firstname
2031                # verification failed.
2032                self.flash(_('No student record found.'))
2033                return
2034            elif student.password is not None and self._pw_used:
2035                self.flash(_('Your password has already been set and used. '
2036                             'Please proceed to the login page.'))
2037                return
2038            # Store email address but nothing else.
2039            student.email = data['email']
2040            notify(grok.ObjectModifiedEvent(student))
2041        else:
2042            # No record found, this is the truth.
2043            self.flash(_('No student record found.'))
2044            return
2045
2046        kofa_utils = getUtility(IKofaUtils)
2047        password = kofa_utils.genPassword()
[8857]2048        mandate = PasswordMandate()
[8853]2049        mandate.params['password'] = password
[8858]2050        mandate.params['user'] = student
[8853]2051        site = grok.getSite()
2052        site['mandates'].addMandate(mandate)
[8779]2053        # Send email with credentials
[8853]2054        args = {'mandate_id':mandate.mandate_id}
2055        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2056        url_info = u'Confirmation link: %s' % mandate_url
[8779]2057        msg = _('You have successfully requested a password for the')
2058        if kofa_utils.sendCredentials(IUserAccount(student),
[8853]2059            password, url_info, msg):
[8779]2060            email_sent = student.email
2061        else:
2062            email_sent = None
2063        self._redirect(email=email_sent, password=password,
2064            student_id=student.student_id)
[8856]2065        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2066        self.context.logger.info(
2067            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
[8779]2068        return
2069
2070class StudentRequestPasswordEmailSent(KofaPage):
2071    """Landing page after successful password request.
2072
2073    """
2074    grok.name('requestpw_complete')
2075    grok.require('waeup.Public')
2076    grok.template('requestpwmailsent')
2077    label = _('Your password request was successful.')
2078
2079    def update(self, email=None, student_id=None, password=None):
2080        self.email = email
2081        self.password = password
2082        self.student_id = student_id
[8974]2083        return
Note: See TracBrowser for help on using the repository browser.