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

Last change on this file since 9011 was 9010, checked in by Henrik Bettermann, 13 years ago

Show history on clearance slip.

Render signatures.

Remove unused function.

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