source: main/waeup.kofa/branches/uli-zc-async/src/waeup/kofa/browser/pages.py @ 8786

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

Fix datecenter import step 3. Store header number in first column of preview table instead of original header name. Fix pagetemplate.

We have to improve the browser tests. Setting headerfields is not yet properly tested in datacenter.txt.

  • Property svn:keywords set to Id
File size: 71.4 KB
Line 
1## $Id: pages.py 8783 2012-06-24 08:17:57Z henrik $
2##
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""" Viewing components for Kofa objects.
19"""
20import copy
21import csv
22import grok
23import os
24import re
25import sys
26import time
27import re
28from zope import schema
29from zope.authentication.interfaces import (
30    IAuthentication, IUnauthenticatedPrincipal, ILogout)
31from zope.catalog.interfaces import ICatalog
32from zope.component import (
33    getUtility, queryUtility, createObject, getAllUtilitiesRegisteredFor,
34    getUtilitiesFor,
35    )
36#from zope.component.interfaces import Invalid
37from zope.event import notify
38from zope.securitypolicy.interfaces import (
39    IPrincipalRoleManager, IPrincipalRoleMap)
40from zope.session.interfaces import ISession
41from waeup.kofa.browser import (
42    KofaPage, KofaForm, KofaEditFormPage, KofaAddFormPage,
43    KofaDisplayFormPage, NullValidator)
44from waeup.kofa.browser.interfaces import (
45    IUniversity, IFacultiesContainer, IFaculty, IFacultyAdd,
46    IDepartment, IDepartmentAdd, ICourse, ICourseAdd, ICertificate,
47    ICertificateAdd, ICertificateCourse, ICertificateCourseAdd,
48    ICaptchaManager)
49from waeup.kofa.browser.layout import jsaction, action, UtilityView
50from waeup.kofa.browser.resources import warning, datepicker, tabs, datatable
51from waeup.kofa.interfaces import MessageFactory as _
52from waeup.kofa.interfaces import(
53    IKofaObject, IUsersContainer, IUserAccount, IDataCenter,
54    IKofaXMLImporter, IKofaXMLExporter, IBatchProcessor,
55    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
56    ISessionConfiguration, ISessionConfigurationAdd,
57    IPasswordValidator, IContactForm, IKofaUtils, ICSVExporter,
58    IChangePassword)
59from waeup.kofa.permissions import (
60    get_users_with_local_roles, get_all_roles, get_all_users)
61from waeup.kofa.students.catalog import search as searchstudents
62from waeup.kofa.university.catalog import search
63from waeup.kofa.university.vocabularies import course_levels
64from waeup.kofa.authentication import LocalRoleSetEvent
65#from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
66from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
67from waeup.kofa.authentication import get_principal_role_manager
68from waeup.kofa.utils.helpers import get_user_account, msave
69
70grok.context(IKofaObject)
71grok.templatedir('templates')
72
73def add_local_role(view, tab, **data):
74    localrole = view.request.form.get('local_role', None)
75    user = view.request.form.get('user', None)
76    if user is None or localrole is None:
77        view.flash('No user selected.')
78        view.redirect(view.url(view.context, '@@manage')+'?tab%s' % tab)
79        return
80    role_manager = IPrincipalRoleManager(view.context)
81    role_manager.assignRoleToPrincipal(localrole, user)
82    notify(LocalRoleSetEvent(view.context, localrole, user, granted=True))
83    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
84    grok.getSite().logger.info(
85        '%s - added: %s|%s' % (ob_class, user, localrole))
86    view.redirect(view.url(view.context, u'@@manage')+'?tab%s' % tab)
87    return
88
89def del_local_roles(view, tab, **data):
90    child_ids = view.request.form.get('role_id', None)
91    if child_ids is None:
92        view.flash(_('No local role selected.'))
93        view.redirect(view.url(view.context, '@@manage')+'?tab%s' % tab)
94        return
95    if not isinstance(child_ids, list):
96        child_ids = [child_ids]
97    deleted = []
98    role_manager = IPrincipalRoleManager(view.context)
99    for child_id in child_ids:
100        localrole = child_id.split('|')[1]
101        user_name = child_id.split('|')[0]
102        try:
103            role_manager.unsetRoleForPrincipal(localrole, user_name)
104            notify(LocalRoleSetEvent(
105                    view.context, localrole, user_name, granted=False))
106            deleted.append(child_id)
107        except:
108            view.flash('Could not remove %s: %s: %s' % (
109                    child_id, sys.exc_info()[0], sys.exc_info()[1]))
110    if len(deleted):
111        view.flash(
112            _('Local role successfully removed: ${a}',
113            mapping = {'a':', '.join(deleted)}))
114        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
115        grok.getSite().logger.info(
116            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
117    view.redirect(view.url(view.context, u'@@manage')+'?tab%s' % tab)
118    return
119
120def delSubobjects(view, redirect, tab=None, subcontainer=None):
121    form = view.request.form
122    if form.has_key('val_id'):
123        child_id = form['val_id']
124    else:
125        view.flash(_('No item selected.'))
126        if tab:
127            view.redirect(view.url(view.context, redirect)+'?tab%s' % tab)
128        else:
129            view.redirect(view.url(view.context, redirect))
130        return
131    if not isinstance(child_id, list):
132        child_id = [child_id]
133    deleted = []
134    for id in child_id:
135        try:
136            if subcontainer:
137                container = getattr(view.context, subcontainer, None)
138                del container[id]
139            else:
140                del view.context[id]
141            deleted.append(id)
142        except:
143            view.flash('Could not delete %s: %s: %s' % (
144                    id, sys.exc_info()[0], sys.exc_info()[1]))
145    if len(deleted):
146        view.flash(_('Successfully removed: ${a}',
147            mapping = {'a': ', '.join(deleted)}))
148        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
149        grok.getSite().logger.info(
150            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
151    if tab:
152        view.redirect(view.url(view.context, redirect)+'?tab%s' % tab)
153    else:
154        view.redirect(view.url(view.context, redirect))
155    return
156
157#
158# Login/logout and language switch pages...
159#
160
161class LoginPage(KofaPage):
162    """A login page, available for all objects.
163    """
164    grok.name('login')
165    grok.context(IKofaObject)
166    grok.require('waeup.Public')
167    label = _(u'Login')
168    camefrom = None
169    login_button = label
170
171    def update(self, SUBMIT=None, camefrom=None):
172        self.camefrom = camefrom
173        if SUBMIT is not None:
174            if self.request.principal.id != 'zope.anybody':
175                self.flash(_('You logged in.'))
176                if self.request.principal.user_type == 'student':
177                    rel_link = '/students/%s' % self.request.principal.id
178                    self.redirect(self.application_url() + rel_link)
179                    return
180                elif self.request.principal.user_type == 'applicant':
181                    container, application_number = self.request.principal.id.split('_')
182                    rel_link = '/applicants/%s/%s' % (
183                        container, application_number)
184                    self.redirect(self.application_url() + rel_link)
185                    return
186                if not self.camefrom:
187                    # User might have entered the URL directly. Let's beam
188                    # him back to our context.
189                    self.redirect(self.url(self.context))
190                    return
191                self.redirect(self.camefrom)
192                return
193            self.flash(_('You entered wrong credentials.'))
194
195
196class LogoutPage(KofaPage):
197    """A logout page. Calling this page will log the current user out.
198    """
199    grok.context(IKofaObject)
200    grok.require('waeup.Public')
201    grok.name('logout')
202
203    def update(self):
204        if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
205            auth = getUtility(IAuthentication)
206            ILogout(auth).logout(self.request)
207            self.flash(_("You have been logged out. Thanks for using WAeUP Kofa!"))
208        self.redirect(self.application_url())
209
210
211class LanguageChangePage(KofaPage):
212    """ Language switch
213    """
214    grok.context(IKofaObject)
215    grok.name('change_language')
216    grok.require('waeup.Public')
217
218    def update(self, lang='en', view_name='@@index'):
219        self.response.setCookie('kofa.language', lang, path='/')
220        self.redirect(self.url(self.context, view_name))
221        return
222
223    def render(self):
224        return
225
226#
227# Contact form...
228#
229
230class ContactAdminForm(KofaForm):
231    grok.name('contactadmin')
232    #grok.context(IUniversity)
233    grok.template('contactform')
234    grok.require('waeup.Authenticated')
235    pnav = 2
236    form_fields = grok.AutoFields(IContactForm).select('body')
237
238    @property
239    def config(self):
240        return grok.getSite()['configuration']
241
242    def label(self):
243        return _(u'Contact ${a}', mapping = {'a': self.config.name_admin})
244
245    @property
246    def get_user_account(self):
247        return get_user_account(self.request)
248
249    @action(_('Send message now'), style='primary')
250    def send(self, *args, **data):
251        fullname = self.request.principal.title
252        try:
253            email = self.request.principal.email
254        except AttributeError:
255            email = self.config.email_admin
256        username = self.request.principal.id
257        usertype = getattr(self.request.principal,
258                           'user_type', 'system').title()
259        kofa_utils = getUtility(IKofaUtils)
260        success = kofa_utils.sendContactForm(
261                fullname,email,
262                self.config.name_admin,self.config.email_admin,
263                username,usertype,self.config.name,
264                data['body'],self.config.email_subject)
265        # Success is always True if sendContactForm didn't fail.
266        # TODO: Catch exceptions.
267        if success:
268            self.flash(_('Your message has been sent.'))
269        return
270
271class EnquiriesForm(ContactAdminForm):
272    """Captcha'd page to let anonymous send emails to the administrator.
273    """
274    grok.name('enquiries')
275    grok.require('waeup.Public')
276    pnav = 2
277    form_fields = grok.AutoFields(IContactForm).select(
278                          'fullname', 'email_from', 'body')
279
280    def update(self):
281        # Handle captcha
282        self.captcha = getUtility(ICaptchaManager).getCaptcha()
283        self.captcha_result = self.captcha.verify(self.request)
284        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
285        return super(EnquiriesForm, self).update()
286
287    @action(_('Send now'), style='primary')
288    def send(self, *args, **data):
289        if not self.captcha_result.is_valid:
290            # Captcha will display error messages automatically.
291            # No need to flash something.
292            return
293        kofa_utils = getUtility(IKofaUtils)
294        success = kofa_utils.sendContactForm(
295                data['fullname'],data['email_from'],
296                self.config.name_admin,self.config.email_admin,
297                u'None',u'Anonymous',self.config.name,
298                data['body'],self.config.email_subject)
299        if success:
300            self.flash(_('Your message has been sent.'))
301        else:
302            self.flash(_('A smtp server error occurred.'))
303        return
304
305#
306# University related pages...
307#
308
309class UniversityPage(KofaDisplayFormPage):
310    """ The main university page.
311    """
312    grok.require('waeup.Public')
313    grok.name('index')
314    grok.context(IUniversity)
315    pnav = 0
316    label = ''
317
318    @property
319    def frontpage(self):
320        lang = self.request.cookies.get('kofa.language')
321        html = self.context['configuration'].frontpage_dict.get(lang,'')
322        if html =='':
323            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
324            html = self.context[
325                'configuration'].frontpage_dict.get(portal_language,'')
326        if html =='':
327            return _(u'<h1>Welcome to WAeUP.Kofa</h1>')
328        else:
329            return html
330
331class AdministrationPage(KofaPage):
332    """ The administration overview page.
333    """
334    grok.name('administration')
335    grok.context(IUniversity)
336    grok.require('waeup.managePortal')
337    label = _(u'Administration')
338    pnav = 0
339
340class RSS20Feed(grok.View):
341    """An RSS 2.0 feed.
342    """
343    grok.name('feed.rss')
344    grok.context(IUniversity)
345    grok.require('waeup.Public')
346    grok.template('universityrss20feed')
347
348    name = 'General news feed'
349    description = 'waeup.kofa now supports RSS 2.0 feeds :-)'
350    language = None
351    date = None
352    buildDate = None
353    editor = None
354    webmaster = None
355
356    @property
357    def title(self):
358        return getattr(grok.getSite(), 'name', u'Sample University')
359
360    @property
361    def contexttitle(self):
362        return self.name
363
364    @property
365    def link(self):
366        return self.url(grok.getSite())
367
368    def update(self):
369        self.response.setHeader('Content-Type', 'text/xml; charset=UTF-8')
370
371    def entries(self):
372        return ()
373
374class ReindexPage(UtilityView, grok.View):
375    """ Reindex view.
376
377    Reindexes a catalog. For managers only.
378    """
379    grok.context(IUniversity)
380    grok.name('reindex')
381    grok.require('waeup.managePortal')
382
383    def update(self,ctlg=None):
384        if ctlg is None:
385            self.flash('No catalog name provided.')
386            return
387        cat = queryUtility(ICatalog, name='%s_catalog' % ctlg)
388        if cat is None:
389            self.flash('%s_catalog does not exist' % ctlg)
390            return
391        self.context.logger.info(
392            'Catalog `%s_catalog` re-indexing started.' % ctlg)
393        cat.updateIndexes()
394        no_of_entries = cat.values()[0].documentCount()
395        self.flash('%d %s re-indexed.' % (no_of_entries,ctlg))
396        self.context.logger.info(
397            'Re-indexing of %d objects finished.' % no_of_entries)
398        return
399
400    def render(self):
401        self.redirect(self.url(self.context, '@@index'))
402        return
403
404#
405# User container pages...
406#
407
408class UsersContainerPage(KofaPage):
409    """Overview page for all local users.
410    """
411    grok.require('waeup.manageUsers')
412    grok.context(IUsersContainer)
413    grok.name('index')
414    label = _('Portal Users')
415    manage_button = _(u'Manage')
416    delete_button = _(u'Remove')
417
418    def update(self, userid=None, adduser=None, manage=None, delete=None):
419        datatable.need()
420        if manage is not None and userid is not None:
421            self.redirect(self.url(userid) + '/@@manage')
422        if delete is not None and userid is not None:
423            self.context.delUser(userid)
424            self.flash(_('User account ${a} successfully deleted.',
425                mapping = {'a':  userid}))
426            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
427            self.context.__parent__.logger.info(
428                '%s - removed: %s' % (ob_class, userid))
429
430    def getLocalRoles(self, account):
431        local_roles = account.getLocalRoles()
432        local_roles_string = ''
433        site_url = self.url(grok.getSite())
434        for local_role in local_roles.keys():
435            role_title = dict(get_all_roles())[local_role].title
436            objects_string = ''
437            for object in local_roles[local_role]:
438                objects_string += '<a href="%s">%s</a>, ' %(self.url(object),
439                    self.url(object).replace(site_url,''))
440            local_roles_string += '%s: <br />%s <br />' %(role_title,
441                objects_string.rstrip(', '))
442        return local_roles_string
443
444    def getSiteRoles(self, account):
445        site_roles = account.roles
446        site_roles_string = ''
447        for site_role in site_roles:
448            role_title = dict(get_all_roles())[site_role].title
449            site_roles_string += '%s <br />' % role_title
450        return site_roles_string
451
452class AddUserFormPage(KofaAddFormPage):
453    """Add a user account.
454    """
455    grok.require('waeup.manageUsers')
456    grok.context(IUsersContainer)
457    grok.name('add')
458    grok.template('usereditformpage')
459    form_fields = grok.AutoFields(IUserAccount)
460    label = _('Add user')
461
462    @action(_('Add user'), style='primary')
463    def addUser(self, **data):
464        name = data['name']
465        title = data['title']
466        email = data['email']
467        phone = data['phone']
468        description = data['description']
469        #password = data['password']
470        roles = data['roles']
471        form = self.request.form
472        password = form.get('password', None)
473        password_ctl = form.get('control_password', None)
474        if password:
475            validator = getUtility(IPasswordValidator)
476            errors = validator.validate_password(password, password_ctl)
477            if errors:
478                self.flash( ' '.join(errors))
479                return
480        try:
481            self.context.addUser(name, password, title=title, email=email,
482                                 phone=phone, description=description,
483                                 roles=roles)
484            self.flash(_('User account ${a} successfully added.',
485                mapping = {'a': name}))
486            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
487            self.context.__parent__.logger.info(
488                '%s - added: %s' % (ob_class, name))
489        except KeyError:
490            self.status = self.flash('The userid chosen already exists '
491                                  'in the database.')
492            return
493        self.redirect(self.url(self.context))
494
495class UserManageFormPage(KofaEditFormPage):
496    """Manage a user account.
497    """
498    grok.context(IUserAccount)
499    grok.name('manage')
500    grok.template('usereditformpage')
501    grok.require('waeup.manageUsers')
502    form_fields = grok.AutoFields(IUserAccount).omit('name')
503
504    def label(self):
505        return _("Edit user ${a}", mapping = {'a':self.context.__name__})
506
507    def setUpWidgets(self, ignore_request=False):
508        super(UserManageFormPage,self).setUpWidgets(ignore_request)
509        self.widgets['title'].displayWidth = 30
510        self.widgets['description'].height = 3
511        return
512
513    @action(_('Save'), style='primary')
514    def save(self, **data):
515        form = self.request.form
516        password = form.get('password', None)
517        password_ctl = form.get('control_password', None)
518        if password:
519            validator = getUtility(IPasswordValidator)
520            errors = validator.validate_password(password, password_ctl)
521            if errors:
522                self.flash( ' '.join(errors))
523                return
524        changed_fields = self.applyData(self.context, **data)
525        if changed_fields:
526            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
527        else:
528            changed_fields = []
529        if password:
530            # Now we know that the form has no errors and can set password ...
531            self.context.setPassword(password)
532            changed_fields.append('password')
533        fields_string = ' + '.join(changed_fields)
534        if fields_string:
535            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
536            self.context.__parent__.logger.info(
537                '%s - %s edited: %s' % (
538                ob_class, self.context.name, fields_string))
539        self.flash(_('User settings have been saved.'))
540        return
541
542    @action(_('Cancel'), validator=NullValidator)
543    def cancel(self, **data):
544        self.redirect(self.url(self.context.__parent__))
545        return
546
547class ContactUserForm(ContactAdminForm):
548    grok.name('contactuser')
549    grok.context(IUserAccount)
550    grok.template('contactform')
551    grok.require('waeup.manageUsers')
552    pnav = 0
553    form_fields = grok.AutoFields(IContactForm).select('body')
554
555    def label(self):
556        return _(u'Send message to ${a}', mapping = {'a':self.context.title})
557
558    @action(_('Send message now'), style='primary')
559    def send(self, *args, **data):
560        try:
561            email = self.request.principal.email
562        except AttributeError:
563            email = self.config.email_admin
564        usertype = getattr(self.request.principal,
565                           'user_type', 'system').title()
566        kofa_utils = getUtility(IKofaUtils)
567        success = kofa_utils.sendContactForm(
568                self.request.principal.title,email,
569                self.context.title,self.context.email,
570                self.request.principal.id,usertype,self.config.name,
571                data['body'],self.config.email_subject)
572        # Success is always True if sendContactForm didn't fail.
573        # TODO: Catch exceptions.
574        if success:
575            self.flash(_('Your message has been sent.'))
576        return
577
578class UserEditFormPage(UserManageFormPage):
579    """Edit a user account by user
580    """
581    grok.name('index')
582    grok.require('waeup.editUser')
583    form_fields = grok.AutoFields(IUserAccount).omit(
584        'name', 'description', 'roles')
585    label = _(u"My Preferences")
586
587    def setUpWidgets(self, ignore_request=False):
588        super(UserManageFormPage,self).setUpWidgets(ignore_request)
589        self.widgets['title'].displayWidth = 30
590
591class MyRolesPage(KofaPage):
592    """Display site roles and local roles assigned to officers.
593    """
594    grok.name('my_roles')
595    grok.require('waeup.editUser')
596    grok.context(IUserAccount)
597    grok.template('myrolespage')
598    label = _(u"My Roles")
599
600    @property
601    def getLocalRoles(self):
602        local_roles = get_user_account(self.request).getLocalRoles()
603        local_roles_userfriendly = {}
604        for local_role in local_roles:
605            role_title = dict(get_all_roles())[local_role].title
606            local_roles_userfriendly[role_title] = local_roles[local_role]
607        return local_roles_userfriendly
608
609    @property
610    def getSiteRoles(self):
611        site_roles = get_user_account(self.request).roles
612        site_roles_userfriendly = []
613        for site_role in site_roles:
614            role_title = dict(get_all_roles())[site_role].title
615            site_roles_userfriendly.append(role_title)
616        return site_roles_userfriendly
617
618#
619# Search pages...
620#
621
622class SearchPage(KofaPage):
623    """General search page for the academics section.
624    """
625    grok.context(IFacultiesContainer)
626    grok.name('search')
627    grok.template('searchpage')
628    grok.require('waeup.viewAcademics')
629    label = _(u"Search Academic Section")
630    pnav = 1
631    search_button = _(u'Search')
632
633    def update(self, *args, **kw):
634        datatable.need()
635        form = self.request.form
636        self.hitlist = []
637        self.query = ''
638        if not 'query' in form:
639            return
640        query = form['query']
641        self.query = query
642        self.hitlist = search(query=self.query, view=self)
643        return
644
645#
646# Configuration pages...
647#
648
649class ConfigurationContainerDisplayFormPage(KofaDisplayFormPage):
650    """View page of the configuration container.
651    """
652    grok.require('waeup.managePortalConfiguration')
653    grok.name('view')
654    grok.context(IConfigurationContainer)
655    pnav = 0
656    label = _(u'View portal configuration')
657    form_fields = grok.AutoFields(IConfigurationContainer)
658    form_fields['frontpage'].custom_widget = HTMLDisplayWidget
659
660class ConfigurationContainerManageFormPage(KofaEditFormPage):
661    """Manage page of the configuration container. We always use the
662    manage page in the UI not the view page, thus we use the index name here.
663    """
664    grok.require('waeup.managePortalConfiguration')
665    grok.name('index')
666    grok.context(IConfigurationContainer)
667    grok.template('configurationmanagepage')
668    pnav = 0
669    label = _(u'Edit portal configuration')
670    taboneactions = [_('Save'), _('Update plugins')]
671    tabtwoactions = [
672        _('Add session configuration'),
673        _('Remove selected')]
674    form_fields = grok.AutoFields(IConfigurationContainer).omit('frontpage_dict')
675
676    def update(self):
677        tabs.need()
678        self.tab1 = self.tab2 = ''
679        qs = self.request.get('QUERY_STRING', '')
680        if not qs:
681            qs = 'tab1'
682        setattr(self, qs, 'active')
683        datatable.need()
684        warning.need()
685        return super(ConfigurationContainerManageFormPage, self).update()
686
687    def _frontpage(self):
688        view = ConfigurationContainerDisplayFormPage(
689            self.context,self.request)
690        view.setUpWidgets()
691        return view.widgets['frontpage']()
692
693    @action(_('Save'), style='primary')
694    def save(self, **data):
695        msave(self, **data)
696        self.context.frontpage_dict = self._frontpage()
697        return
698
699    @action(_('Add session configuration'), validator=NullValidator,
700            style='primary')
701    def addSubunit(self, **data):
702        self.redirect(self.url(self.context, '@@add'))
703        return
704
705    def getSessionConfigurations(self):
706        """Get a list of all stored session configuration objects.
707        """
708        for key, val in self.context.items():
709            url = self.url(val)
710            session_string = val.getSessionString()
711            title = _('Session ${a} Configuration',
712                      mapping = {'a':session_string})
713            yield(dict(url=url, name=key, title=title))
714
715    @jsaction(_('Remove selected'))
716    def delSessonConfigurations(self, **data):
717        delSubobjects(self, redirect='@@index', tab='2')
718        return
719
720    @action(_('Update plugins'), validator=NullValidator)
721    def updatePlugins(self, **data):
722        grok.getSite().updatePlugins()
723        self.flash(_('Plugins were updated. See log file for details.'))
724        return
725
726class SessionConfigurationAddFormPage(KofaAddFormPage):
727    """Add a session configuration object to configuration container.
728    """
729    grok.context(IConfigurationContainer)
730    grok.name('add')
731    grok.require('waeup.managePortalConfiguration')
732    label = _('Add session configuration')
733    form_fields = grok.AutoFields(ISessionConfigurationAdd)
734    pnav = 0
735
736    @action(_('Add Session Configuration'), style='primary')
737    def addSessionConfiguration(self, **data):
738        sessionconfiguration = createObject(u'waeup.SessionConfiguration')
739        self.applyData(sessionconfiguration, **data)
740        try:
741            self.context.addSessionConfiguration(sessionconfiguration)
742            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
743            self.context.__parent__.logger.info(
744                '%s - added: %s' % (
745                ob_class, sessionconfiguration.academic_session))
746        except KeyError:
747            self.flash(_('The session chosen already exists.'))
748            return
749        self.redirect(self.url(self.context, '@@index')+'?tab2')
750        return
751
752    @action(_('Cancel'), validator=NullValidator)
753    def cancel(self):
754        self.redirect(self.url(self.context, '@@index')+'?tab2')
755        return
756
757class SessionConfigurationManageFormPage(KofaEditFormPage):
758    """Manage session configuration object.
759    """
760    grok.context(ISessionConfiguration)
761    grok.name('index')
762    grok.require('waeup.managePortalConfiguration')
763    form_fields = grok.AutoFields(ISessionConfiguration)
764    pnav = 0
765
766    @property
767    def label(self):
768        session_string = self.context.getSessionString()
769        return _('Edit academic session ${a} configuration',
770            mapping = {'a':session_string})
771
772    @action(_('Save'), style='primary')
773    def save(self, **data):
774        msave(self, **data)
775        self.redirect(self.url(self.context.__parent__, '@@index')+'?tab2')
776        return
777
778    @action(_('Cancel'), validator=NullValidator)
779    def cancel(self):
780        self.redirect(self.url(self.context.__parent__, '@@index')+'?tab2')
781        return
782
783#
784# Datacenter pages...
785#
786
787class DatacenterPage(KofaEditFormPage):
788    grok.context(IDataCenter)
789    grok.name('index')
790    grok.require('waeup.manageDataCenter')
791    label = _(u'Data Center')
792    pnav = 0
793
794    def update(self):
795        datatable.need()
796        warning.need()
797        return super(DatacenterPage, self).update()
798
799    @jsaction(_('Remove selected'))
800    def delFiles(self, **data):
801        form = self.request.form
802        logger = self.context.logger
803        if form.has_key('val_id'):
804            child_id = form['val_id']
805        else:
806            self.flash(_('No item selected.'))
807            return
808        if not isinstance(child_id, list):
809            child_id = [child_id]
810        deleted = []
811        for id in child_id:
812            fullpath = os.path.join(self.context.storage, id)
813            try:
814                os.remove(fullpath)
815                deleted.append(id)
816            except OSError:
817                self.flash(_('OSError: The file could not be deleted.'))
818                return
819        if len(deleted):
820            self.flash(_('Successfully deleted: ${a}',
821                mapping = {'a': ', '.join(deleted)}))
822            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
823            self.context.logger.info(
824                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
825        return
826
827class DatacenterUploadPage(KofaPage):
828    grok.context(IDataCenter)
829    grok.name('upload')
830    grok.require('waeup.manageDataCenter')
831    label = _(u'Upload file')
832    pnav = 0
833    upload_button =_(u'Upload')
834    cancel_button =_(u'Cancel')
835
836    def update(self, uploadfile=None, CANCEL=None, SUBMIT=None):
837        if CANCEL is not None:
838            self.redirect(self.url(self.context))
839            return
840        if not uploadfile:
841            return
842        try:
843            filename = uploadfile.filename
844            #if 'pending' in filename:
845            #    self.flash(_("You can't re-upload pending data files."))
846            #    return
847            if not filename.endswith('.csv'):
848                self.flash(_("Only csv files are allowed."))
849                return
850            target = os.path.join(self.context.storage,
851                                  self.getNormalizedFileName(filename))
852            open(target, 'wb').write(uploadfile.read())
853            os.chmod(target, 0664)
854            logger = self.context.logger
855            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
856            logger.info('%s - uploaded: %s' % (ob_class, target))
857        except IOError:
858            self.flash('Error while uploading file. Please retry.')
859            self.flash('I/O error: %s' % sys.exc_info()[1])
860            return
861        self.redirect(self.url(self.context))
862
863    def getNormalizedFileName(self, filename):
864        """Build sane filename.
865
866        An uploaded file foo.csv will be stored as foo_USERNAME.csv
867        where username is the principal id of the currently logged in
868        user.
869
870        Spaces in filename are replaced by underscore.
871        Pending data filenames remain unchanged.
872        """
873        if filename.endswith('.pending.csv'):
874            return filename
875        username = self.request.principal.id
876        filename = filename.replace(' ', '_')
877        # Only accept typical filname chars...
878        filtered_username = ''.join(re.findall('[a-zA-Z0-9_\.\-]', username))
879        base, ext = os.path.splitext(filename)
880        return '%s_%s%s' % (base, filtered_username, ext.lower())
881
882class FileDownloadView(UtilityView, grok.View):
883    grok.context(IDataCenter)
884    grok.name('download')
885    grok.require('waeup.manageDataCenter')
886
887    def update(self, filename=None):
888        self.filename = self.request.form['filename']
889        return
890
891    def render(self):
892        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
893        self.context.logger.info(
894            '%s - downloaded: %s' % (ob_class, self.filename))
895        self.response.setHeader(
896            'Content-Type', 'text/csv; charset=UTF-8')
897        self.response.setHeader(
898            'Content-Disposition:', 'attachment; filename="%s.csv' %
899            self.filename.replace('.csv',''))
900        fullpath = os.path.join(self.context.storage, self.filename)
901        return open(fullpath, 'rb').read()
902
903class DatacenterImportStep1(KofaPage):
904    """Manual import step 1: choose file
905    """
906    grok.context(IDataCenter)
907    grok.name('import1')
908    grok.template('datacenterimport1page')
909    grok.require('waeup.importData')
910    label = _(u'Process CSV file')
911    pnav = 0
912    cancel_button =_(u'Cancel')
913
914    def getFiles(self):
915        files = self.context.getFiles(sort='date')
916        for file in files:
917            name = file.name
918            if not name.endswith('.csv') and not name.endswith('.pending'):
919                continue
920            yield file
921
922    def update(self, filename=None, select=None, cancel=None):
923        if cancel is not None:
924            self.flash(_('Import aborted.'))
925            self.redirect(self.url(self.context))
926            return
927        if select is not None:
928            # A filename was selected
929            session = ISession(self.request)['waeup.kofa']
930            session['import_filename'] = select
931            self.redirect(self.url(self.context, '@@import2'))
932
933class DatacenterImportStep2(KofaPage):
934    """Manual import step 2: choose processor
935    """
936    grok.context(IDataCenter)
937    grok.name('import2')
938    grok.template('datacenterimport2page')
939    grok.require('waeup.importData')
940    label = _(u'Process CSV file')
941    pnav = 0
942    cancel_button =_(u'Cancel')
943    back_button =_(u'Back to step 1')
944    proceed_button =_(u'Proceed to step 3')
945
946    filename = None
947    mode = 'create'
948    importer = None
949    mode_locked = False
950
951    def getPreviewHeader(self):
952        """Get the header fields of attached CSV file.
953        """
954        reader = csv.reader(open(self.fullpath, 'rb'))
955        return reader.next()
956
957    def getPreviewTable(self):
958        """Get transposed table with 3 sample records.
959
960        The first column contains the headers.
961        """
962        if not self.reader:
963            return
964        header = self.getPreviewHeader()
965        num = 0
966        data = []
967        for line in self.reader:
968            if num > 2:
969                break
970            num += 1
971            data.append(line)
972        result = []
973        for name in header:
974            result_line = []
975            result_line.append(name)
976            for d in data:
977                result_line.append(d[name])
978            result.append(result_line)
979        return result
980
981    def getImporters(self):
982        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
983        importers = sorted(
984            [dict(title=x.name, name=x.util_name) for x in importers])
985        return importers
986
987    def getModeFromFilename(self, filename):
988        """Lookup filename or path and return included mode name or None.
989        """
990        if not filename.endswith('.pending.csv'):
991            return None
992        base = os.path.basename(filename)
993        parts = base.rsplit('.', 3)
994        if len(parts) != 4:
995            return None
996        if parts[1] not in ['create', 'update', 'remove']:
997            return None
998        return parts[1]
999
1000    def getWarnings(self):
1001        import sys
1002        result = []
1003        try:
1004            headerfields = self.getPreviewHeader()
1005            headerfields_clean = list(set(headerfields))
1006            if len(headerfields) > len(headerfields_clean):
1007                result.append(
1008                    _("Double headers: each column name may only appear once. "))
1009        except:
1010            fatal = '%s' % sys.exc_info()[1]
1011            result.append(fatal)
1012        if result:
1013            warnings = ""
1014            for line in result:
1015                warnings += line + '<br />'
1016            warnings += _('Replace imported file!')
1017            return warnings
1018        return False
1019
1020    def update(self, mode=None, importer=None,
1021               back1=None, cancel=None, proceed=None):
1022        session = ISession(self.request)['waeup.kofa']
1023        self.filename = session.get('import_filename', None)
1024
1025        if self.filename is None or back1 is not None:
1026            self.redirect(self.url(self.context, '@@import1'))
1027            return
1028        if cancel is not None:
1029            self.flash(_('Import aborted.'))
1030            self.redirect(self.url(self.context))
1031            return
1032        self.mode = mode or session.get('import_mode', self.mode)
1033        filename_mode = self.getModeFromFilename(self.filename)
1034        if filename_mode is not None:
1035            self.mode = filename_mode
1036            self.mode_locked = True
1037        self.importer = importer or session.get('import_importer', None)
1038        session['import_importer'] = self.importer
1039        if self.importer and 'update' in self.importer:
1040            if self.mode != 'update':
1041                self.flash(_('Update mode only!'))
1042                self.mode_locked = True
1043                self.mode = 'update'
1044                proceed = None
1045        session['import_mode'] = self.mode
1046        if proceed is not None:
1047            self.redirect(self.url(self.context, '@@import3'))
1048            return
1049        self.fullpath = os.path.join(self.context.storage, self.filename)
1050        warnings = self.getWarnings()
1051        if not warnings:
1052            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1053        else:
1054            self.reader = ()
1055            self.flash(warnings)
1056
1057class DatacenterImportStep3(KofaPage):
1058    """Manual import step 3: modify header
1059    """
1060    grok.context(IDataCenter)
1061    grok.name('import3')
1062    grok.template('datacenterimport3page')
1063    grok.require('waeup.importData')
1064    label = _(u'Process CSV file')
1065    pnav = 0
1066    cancel_button =_(u'Cancel')
1067    reset_button =_(u'Reset')
1068    update_button =_(u'Set headerfields')
1069    back_button =_(u'Back to step 2')
1070    proceed_button =_(u'Perform import')
1071
1072    filename = None
1073    mode = None
1074    importername = None
1075
1076    @property
1077    def nextstep(self):
1078        return self.url(self.context, '@@import4')
1079
1080    def getPreviewHeader(self):
1081        """Get the header fields of attached CSV file.
1082        """
1083        reader = csv.reader(open(self.fullpath, 'rb'))
1084        return reader.next()
1085
1086    def getPreviewTable(self):
1087        """Get transposed table with 1 sample record.
1088
1089        The first column contains the headers.
1090        """
1091        if not self.reader:
1092            return
1093        headers = self.getPreviewHeader()
1094        num = 0
1095        data = []
1096        for line in self.reader:
1097            if num > 0:
1098                break
1099            num += 1
1100            data.append(line)
1101        result = []
1102        field_num = 0
1103        for name in headers:
1104            result_line = []
1105            result_line.append(field_num)
1106            field_num += 1
1107            for d in data:
1108                result_line.append(d[name])
1109            result.append(result_line)
1110        return result
1111
1112    def getPossibleHeaders(self):
1113        """Get the possible headers.
1114
1115        The headers are described as dicts {value:internal_name,
1116        title:displayed_name}
1117        """
1118        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
1119        headers = self.importer.getHeaders()
1120        result.extend([dict(title=x, value=x) for x in headers])
1121        return result
1122
1123    def getWarnings(self):
1124        import sys
1125        result = []
1126        try:
1127            self.importer.checkHeaders(self.headerfields, mode=self.mode)
1128        except:
1129            fatal = '%s' % sys.exc_info()[1]
1130            result.append(fatal)
1131        if result:
1132            warnings = ""
1133            for line in result:
1134                warnings += line + '<br />'
1135            warnings += _('Edit headers or replace imported file!')
1136            return warnings
1137        return False
1138
1139    @property
1140    def nextstep(self):
1141        return self.url(self.context, '@@import4')
1142
1143    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
1144        datatable.need()
1145        session = ISession(self.request)['waeup.kofa']
1146        self.filename = session.get('import_filename', None)
1147        self.mode = session.get('import_mode', None)
1148        self.importername = session.get('import_importer', None)
1149
1150        if None in (self.filename, self.mode, self.importername):
1151            self.redirect(self.url(self.context, '@@import2'))
1152            return
1153        if back2 is not None:
1154            self.redirect(self.url(self.context ,'@@import2'))
1155            return
1156        if cancel is not None:
1157            self.flash(_('Import aborted.'))
1158            self.redirect(self.url(self.context))
1159            return
1160
1161        self.fullpath = os.path.join(self.context.storage, self.filename)
1162        self.headerfields = headerfield or self.getPreviewHeader()
1163        session['import_headerfields'] = self.headerfields
1164
1165        if proceed is not None:
1166            self.redirect(self.url(self.context, '@@import4'))
1167            return
1168        self.importer = getUtility(IBatchProcessor, name=self.importername)
1169        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1170        warnings = self.getWarnings()
1171        if warnings:
1172            self.flash(warnings)
1173
1174class DatacenterImportStep4(KofaPage):
1175    """Manual import step 4: do actual import
1176    """
1177    grok.context(IDataCenter)
1178    grok.name('import4')
1179    grok.template('datacenterimport4page')
1180    grok.require('waeup.importData')
1181    label = _(u'Process CSV file')
1182    pnav = 0
1183    show_button =_(u'View processing log')
1184    back_button =_(u'Back to data center')
1185
1186    filename = None
1187    mode = None
1188    importername = None
1189    headerfields = None
1190    warnnum = None
1191
1192    def update(self, back=None, finish=None, showlog=None):
1193        if finish is not None:
1194            self.redirect(self.url(self.context))
1195            return
1196        session = ISession(self.request)['waeup.kofa']
1197        self.filename = session.get('import_filename', None)
1198        self.mode = session.get('import_mode', None)
1199        self.importername = session.get('import_importer', None)
1200        self.headerfields = session.get('import_headerfields', None)
1201
1202        if None in (self.filename, self.mode, self.importername,
1203                    self.headerfields):
1204            self.redirect(self.url(self.context, '@@import3'))
1205            return
1206
1207        if showlog is not None:
1208            logfilename = "datacenter.log"
1209            session['logname'] = logfilename
1210            self.redirect(self.url(self.context, '@@show'))
1211            return
1212
1213        self.fullpath = os.path.join(self.context.storage, self.filename)
1214        self.importer = getUtility(IBatchProcessor, name=self.importername)
1215
1216        # Perform batch processing...
1217        # XXX: This might be better placed in datacenter module.
1218        (linenum, self.warn_num,
1219         fin_path, pending_path) = self.importer.doImport(
1220            self.fullpath, self.headerfields, self.mode,
1221            self.request.principal.id, logger=self.context.logger)
1222        # Put result files in desired locations...
1223        self.context.distProcessedFiles(
1224            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1225            self.mode)
1226
1227        if self.warn_num:
1228            self.flash(_('Processing of ${a} rows failed.',
1229                mapping = {'a':self.warn_num}))
1230        self.flash(_('Successfully processed ${a} rows.',
1231            mapping = {'a':linenum - self.warn_num}))
1232
1233class DatacenterLogsOverview(KofaPage):
1234    grok.context(IDataCenter)
1235    grok.name('logs')
1236    grok.template('datacenterlogspage')
1237    grok.require('waeup.manageDataCenter')
1238    label = _(u'Show logfiles')
1239    pnav = 0
1240    back_button = _(u'Back to Data Center')
1241    show_button = _(u'Show')
1242
1243    def update(self, back=None):
1244        if back is not None:
1245            self.redirect(self.url(self.context))
1246            return
1247        self.files = self.context.getLogFiles()
1248
1249class DatacenterLogsFileview(KofaPage):
1250    grok.context(IDataCenter)
1251    grok.name('show')
1252    grok.template('datacenterlogsshowfilepage')
1253    grok.require('waeup.manageDataCenter')
1254    title = _(u'Data Center')
1255    pnav = 0
1256    search_button = _('Search')
1257    back_button = _('Back')
1258    placeholder = _('Enter a regular expression here...')
1259
1260    def label(self):
1261        return "Logfile %s" % self.filename
1262
1263    def update(self, back=None, query=None, logname=None):
1264        if os.name != 'posix':
1265            self.flash(
1266                _('Log files can only be searched ' +
1267                  'on Unix-based operating systems.'))
1268            self.redirect(self.url(self.context, '@@logs'))
1269            return
1270        if back is not None or logname is None:
1271            self.redirect(self.url(self.context, '@@logs'))
1272            return
1273        self.filename = logname
1274        self.query = query
1275        if search is None or not query:
1276            return
1277        try:
1278            self.result = ''.join(
1279                self.context.queryLogfiles(logname, query))
1280        except ValueError:
1281            self.flash(_('Invalid search expression.'))
1282            return
1283        if not self.result:
1284            self.flash(_('No search results found.'))
1285        return
1286
1287class DatacenterSettings(KofaPage):
1288    grok.context(IDataCenter)
1289    grok.name('manage')
1290    grok.template('datacentermanagepage')
1291    grok.require('waeup.managePortal')
1292    label = _('Edit data center settings')
1293    pnav = 0
1294    save_button =_(u'Save')
1295    reset_button =_(u'Reset')
1296    cancel_button =_(u'Cancel')
1297
1298    def update(self, newpath=None, move=False, overwrite=False,
1299               save=None, cancel=None):
1300        if move:
1301            move = True
1302        if overwrite:
1303            overwrite = True
1304        if newpath is None:
1305            return
1306        if cancel is not None:
1307            self.redirect(self.url(self.context))
1308            return
1309        try:
1310            not_copied = self.context.setStoragePath(newpath, move=move)
1311            for name in not_copied:
1312                self.flash(_('File already existed (not copied): ${a}',
1313                    mapping = {'a':name}))
1314        except:
1315            self.flash(_('Given storage path cannot be used.'))
1316            self.flash(_('Error: ${a}', mapping = {'a':sys.exc_info()[1]}))
1317            return
1318        if newpath:
1319            self.flash(_('New storage path succefully set.'))
1320            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1321            self.context.logger.info(
1322                '%s - storage path set: %s' % (ob_class, newpath))
1323            self.redirect(self.url(self.context))
1324        return
1325
1326class ExportCSVPage(KofaPage):
1327    grok.context(IDataCenter)
1328    grok.name('export')
1329    grok.template('datacenterexportpage')
1330    grok.require('waeup.manageDataCenter')
1331    label = _('Download portal data as CSV file')
1332    pnav = 0
1333    export_button = _(u'Download')
1334
1335    def getExporters(self):
1336        utils = getUtilitiesFor(ICSVExporter)
1337        title_name_tuples = [
1338            (util.title, name) for name, util in utils]
1339        return sorted(title_name_tuples)
1340
1341    def update(self, export=None, exporter=None):
1342        if None in (export, exporter):
1343            return
1344        self.redirect(
1345            self.url(self.context, 'export.csv') + '?exporter=%s' % exporter)
1346        return
1347
1348class ExportCSVView(grok.View):
1349    grok.context(IDataCenter)
1350    grok.name('export.csv')
1351    grok.require('waeup.manageDataCenter')
1352
1353    def render(self, exporter=None):
1354        if exporter is None:
1355            return
1356        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1357        self.context.logger.info(
1358            '%s - exported: %s' % (ob_class, exporter))
1359        exporter = getUtility(ICSVExporter, name=exporter)
1360        csv_data = exporter.export_all(grok.getSite())
1361        #csv_data.seek(0)
1362        self.response.setHeader(
1363            'Content-Type', 'text/csv; charset=UTF-8')
1364        return csv_data
1365
1366class ExportXMLPage(grok.View):
1367    """Deliver an XML representation of the context.
1368    """
1369    grok.name('export.xml')
1370    grok.require('waeup.managePortal')
1371
1372    def render(self):
1373        exporter = IKofaXMLExporter(self.context)
1374        xml = exporter.export().read()
1375        self.response.setHeader(
1376            'Content-Type', 'text/xml; charset=UTF-8')
1377        return xml
1378
1379class ImportXMLPage(KofaPage):
1380    """Replace the context object by an object created from an XML
1381       representation.
1382
1383       XXX: This does not work for ISite objects, i.e. for instance
1384            for complete University objects.
1385    """
1386    grok.name('importxml')
1387    grok.require('waeup.managePortal')
1388
1389    def update(self, xmlfile=None, CANCEL=None, SUBMIT=None):
1390        if CANCEL is not None:
1391            self.redirect(self.url(self.context))
1392            return
1393        if not xmlfile:
1394            return
1395        importer = IKofaXMLImporter(self.context)
1396        obj = importer.doImport(xmlfile)
1397        if type(obj) != type(self.context):
1398            return
1399        parent = self.context.__parent__
1400        name = self.context.__name__
1401        self.context = obj
1402        if hasattr(parent, name):
1403            setattr(parent, name, obj)
1404        else:
1405            del parent[name]
1406            parent[name] = obj
1407            pass
1408        return
1409
1410
1411#
1412# FacultiesContainer pages...
1413#
1414
1415class FacultiesContainerPage(KofaPage):
1416    """ Index page for faculty containers.
1417    """
1418    grok.context(IFacultiesContainer)
1419    grok.require('waeup.viewAcademics')
1420    grok.name('index')
1421    label = _('Academic Section')
1422    pnav = 1
1423    grok.template('facultypage')
1424
1425class FacultiesContainerManageFormPage(KofaEditFormPage):
1426    """Manage the basic properties of a `Faculty` instance.
1427    """
1428    grok.context(IFacultiesContainer)
1429    grok.name('manage')
1430    grok.require('waeup.manageAcademics')
1431    grok.template('facultiescontainermanagepage')
1432    pnav = 1
1433    taboneactions = [_('Add faculty'), _('Remove selected'),_('Cancel')]
1434    subunits = _('Faculties')
1435
1436    @property
1437    def label(self):
1438        return _('Manage academic section')
1439
1440    def update(self):
1441        warning.need()
1442        return super(FacultiesContainerManageFormPage, self).update()
1443
1444    @jsaction(_('Remove selected'))
1445    def delFaculties(self, **data):
1446        delSubobjects(self, redirect='@@manage', tab='1')
1447        return
1448
1449    @action(_('Add faculty'), validator=NullValidator)
1450    def addFaculty(self, **data):
1451        self.redirect(self.url(self.context, '@@add'))
1452        return
1453
1454    @action(_('Cancel'), validator=NullValidator)
1455    def cancel(self, **data):
1456        self.redirect(self.url(self.context))
1457        return
1458
1459
1460class FacultyAddFormPage(KofaAddFormPage):
1461    """ Page form to add a new faculty to a faculty container.
1462    """
1463    grok.context(IFacultiesContainer)
1464    grok.require('waeup.manageAcademics')
1465    grok.name('add')
1466    label = _('Add faculty')
1467    form_fields = grok.AutoFields(IFacultyAdd)
1468    pnav = 1
1469
1470    @action(_('Add faculty'), style='primary')
1471    def addFaculty(self, **data):
1472        faculty = createObject(u'waeup.Faculty')
1473        self.applyData(faculty, **data)
1474        try:
1475            self.context.addFaculty(faculty)
1476        except KeyError:
1477            self.flash(_('The faculty code chosen already exists.'))
1478            return
1479        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1480        self.context.__parent__.logger.info(
1481            '%s - added: %s' % (ob_class, faculty.code))
1482        self.redirect(self.url(self.context, u'@@manage')+'?tab1')
1483        return
1484
1485    @action(_('Cancel'), validator=NullValidator)
1486    def cancel(self, **data):
1487        self.redirect(self.url(self.context))
1488
1489#
1490# Faculty pages
1491#
1492class FacultyPage(KofaPage):
1493    """Index page of faculties.
1494    """
1495    grok.context(IFaculty)
1496    grok.require('waeup.viewAcademics')
1497    grok.name('index')
1498    pnav = 1
1499
1500    @property
1501    def label(self):
1502        return _('Departments')
1503
1504class FacultyManageFormPage(KofaEditFormPage):
1505    """Manage the basic properties of a `Faculty` instance.
1506    """
1507    grok.context(IFaculty)
1508    grok.name('manage')
1509    grok.require('waeup.manageAcademics')
1510    grok.template('facultymanagepage')
1511    pnav = 1
1512    subunits = _('Departments')
1513    taboneactions = [_('Save'),_('Cancel')]
1514    tabtwoactions = [_('Add department'), _('Remove selected'),_('Cancel')]
1515    tabthreeactions1 = [_('Remove selected local roles')]
1516    tabthreeactions2 = [_('Add local role')]
1517
1518    form_fields = grok.AutoFields(IFaculty)
1519
1520    @property
1521    def label(self):
1522        return _('Manage faculty')
1523
1524    def update(self):
1525        tabs.need()
1526        self.tab1 = self.tab2 = self.tab3 = ''
1527        qs = self.request.get('QUERY_STRING', '')
1528        if not qs:
1529            qs = 'tab1'
1530        setattr(self, qs, 'active')
1531        warning.need()
1532        datatable.need()
1533        return super(FacultyManageFormPage, self).update()
1534
1535    def getLocalRoles(self):
1536        roles = ILocalRolesAssignable(self.context)
1537        return roles()
1538
1539    def getUsers(self):
1540        return get_all_users()
1541
1542    def getUsersWithLocalRoles(self):
1543        return get_users_with_local_roles(self.context)
1544
1545    @jsaction(_('Remove selected'))
1546    def delDepartments(self, **data):
1547        delSubobjects(self, redirect='@@manage', tab='2')
1548        return
1549
1550    @action(_('Save'), style='primary')
1551    def save(self, **data):
1552        return msave(self, **data)
1553
1554    @action(_('Cancel'), validator=NullValidator)
1555    def cancel(self, **data):
1556        self.redirect(self.url(self.context))
1557        return
1558
1559    @action(_('Add department'), validator=NullValidator)
1560    def addSubunit(self, **data):
1561        self.redirect(self.url(self.context, '@@add'))
1562        return
1563
1564    @action(_('Add local role'), validator=NullValidator)
1565    def addLocalRole(self, **data):
1566        return add_local_role(self, '3', **data)
1567
1568    @action(_('Remove selected local roles'))
1569    def delLocalRoles(self, **data):
1570        return del_local_roles(self,3,**data)
1571
1572class DepartmentAddFormPage(KofaAddFormPage):
1573    """Add a department to a faculty.
1574    """
1575    grok.context(IFaculty)
1576    grok.name('add')
1577    grok.require('waeup.manageAcademics')
1578    label = _('Add department')
1579    form_fields = grok.AutoFields(IDepartmentAdd)
1580    pnav = 1
1581
1582    @action(_('Add department'), style='primary')
1583    def addDepartment(self, **data):
1584        department = createObject(u'waeup.Department')
1585        self.applyData(department, **data)
1586        try:
1587            self.context.addDepartment(department)
1588        except KeyError:
1589            self.flash(_('The code chosen already exists in this faculty.'))
1590            return
1591        self.status = self.flash(
1592            _("Department ${a} added.", mapping = {'a':data['code']}))
1593        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1594        self.context.__parent__.__parent__.logger.info(
1595            '%s - added: %s' % (ob_class, data['code']))
1596        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1597        return
1598
1599    @action(_('Cancel'), validator=NullValidator)
1600    def cancel(self, **data):
1601        self.redirect(self.url(self.context))
1602        return
1603
1604#
1605# Department pages
1606#
1607class DepartmentPage(KofaPage):
1608    """Department index page.
1609    """
1610    grok.context(IDepartment)
1611    grok.require('waeup.viewAcademics')
1612    grok.name('index')
1613    pnav = 1
1614    label = _('Courses and Certificates')
1615
1616    def update(self):
1617        tabs.need()
1618        datatable.need()
1619        super(DepartmentPage, self).update()
1620        return
1621
1622    def getCourses(self):
1623        """Get a list of all stored courses.
1624        """
1625        for key, val in self.context.courses.items():
1626            url = self.url(val)
1627            yield(dict(url=url, name=key, container=val))
1628
1629    def getCertificates(self):
1630        """Get a list of all stored certificates.
1631        """
1632        for key, val in self.context.certificates.items():
1633            url = self.url(val)
1634            yield(dict(url=url, name=key, container=val))
1635
1636class ShowStudentsInDepartmentPage(KofaPage):
1637    """Page that lists all students in the department.
1638    """
1639    grok.context(IDepartment)
1640    grok.require('waeup.showStudents')
1641    grok.name('showdepstudents')
1642    grok.template('showstudentspage')
1643    pnav = 1
1644    label = _('Students')
1645
1646    @property
1647    def getStudents(self):
1648        hitlist = searchstudents(query=self.context.code,
1649            searchtype='depcode', view=self)
1650        return hitlist
1651
1652    def update(self, *args, **kw):
1653        datatable.need()
1654        return
1655
1656class ShowStudentsInCertificatePage(ShowStudentsInDepartmentPage):
1657    """Page that lists all students studying a certificate.
1658    """
1659    grok.context(ICertificate)
1660    grok.require('waeup.showStudents')
1661    grok.name('showcertstudents')
1662    pnav = 1
1663    label = _('Students')
1664
1665    @property
1666    def getStudents(self):
1667        hitlist = searchstudents(query=self.context.code,
1668            searchtype='certcode', view=self)
1669        return hitlist
1670
1671class DepartmentManageFormPage(KofaEditFormPage):
1672    """Manage the basic properties of a `Department` instance.
1673    """
1674    grok.context(IDepartment)
1675    grok.name('manage')
1676    grok.require('waeup.manageAcademics')
1677    pnav = 1
1678    grok.template('departmentmanagepage')
1679    taboneactions = [_('Save'),_('Cancel')]
1680    tabtwoactions = [_('Add course'), _('Remove selected courses'),_('Cancel')]
1681    tabthreeactions = [_('Add certificate'), _('Remove selected certificates'),
1682                       _('Cancel')]
1683    tabfouractions1 = [_('Remove selected local roles')]
1684    tabfouractions2 = [_('Add local role')]
1685
1686    form_fields = grok.AutoFields(IDepartment)
1687
1688    @property
1689    def label(self):
1690        return _('Manage department')
1691
1692    def getCourses(self):
1693        """Get a list of all stored courses.
1694        """
1695        for key, val in self.context.courses.items():
1696            url = self.url(val)
1697            yield(dict(url=url, name=key, container=val))
1698
1699    def getCertificates(self):
1700        """Get a list of all stored certificates.
1701        """
1702        for key, val in self.context.certificates.items():
1703            url = self.url(val)
1704            yield(dict(url=url, name=key, container=val))
1705
1706    def update(self):
1707        tabs.need()
1708        self.tab1 = self.tab2 = self.tab3 = self.tab4 = ''
1709        qs = self.request.get('QUERY_STRING', '')
1710        if not qs:
1711            qs = 'tab1'
1712        setattr(self, qs, 'active')
1713        warning.need()
1714        datatable.need()
1715        super(DepartmentManageFormPage, self).update()
1716        return
1717
1718    def getLocalRoles(self):
1719        roles = ILocalRolesAssignable(self.context)
1720        return roles()
1721
1722    def getUsers(self):
1723        return get_all_users()
1724
1725    def getUsersWithLocalRoles(self):
1726        return get_users_with_local_roles(self.context)
1727
1728    @action(_('Save'), style='primary')
1729    def save(self, **data):
1730        return msave(self, **data)
1731
1732    @jsaction(_('Remove selected courses'))
1733    def delCourses(self, **data):
1734        delSubobjects(
1735            self, redirect='@@manage', tab='2', subcontainer='courses')
1736        return
1737
1738    @jsaction(_('Remove selected certificates'))
1739    def delCertificates(self, **data):
1740        delSubobjects(
1741            self, redirect='@@manage', tab='3', subcontainer='certificates')
1742        return
1743
1744    @action(_('Add course'), validator=NullValidator)
1745    def addCourse(self, **data):
1746        self.redirect(self.url(self.context, 'addcourse'))
1747        return
1748
1749    @action(_('Add certificate'), validator=NullValidator)
1750    def addCertificate(self, **data):
1751        self.redirect(self.url(self.context, 'addcertificate'))
1752        return
1753
1754    @action(_('Cancel'), validator=NullValidator)
1755    def cancel(self, **data):
1756        self.redirect(self.url(self.context))
1757        return
1758
1759    @action(_('Add local role'), validator=NullValidator)
1760    def addLocalRole(self, **data):
1761        return add_local_role(self, 4, **data)
1762
1763    @action(_('Remove selected local roles'))
1764    def delLocalRoles(self, **data):
1765        return del_local_roles(self,4,**data)
1766
1767class CourseAddFormPage(KofaAddFormPage):
1768    """Add-form to add course to a department.
1769    """
1770    grok.context(IDepartment)
1771    grok.name('addcourse')
1772    grok.require('waeup.manageAcademics')
1773    label = _(u'Add course')
1774    form_fields = grok.AutoFields(ICourseAdd)
1775    pnav = 1
1776
1777    @action(_('Add course'))
1778    def addCourse(self, **data):
1779        course = createObject(u'waeup.Course')
1780        self.applyData(course, **data)
1781        try:
1782            self.context.courses.addCourse(course)
1783        except DuplicationError, error:
1784            # signals duplication error
1785            entries = error.entries
1786            for entry in entries:
1787                # do not use error.msg but render a more detailed message instead
1788                # and show links to all certs with same code
1789                message = _('A course with same code already exists: ')
1790                message += '<a href="%s">%s</a>' % (
1791                    self.url(entry), getattr(entry, '__name__', u'Unnamed'))
1792                self.flash(message)
1793            self.redirect(self.url(self.context, u'@@addcourse'))
1794            return
1795        message = _(u'Course ${a} successfully created.', mapping = {'a':course.code})
1796        self.flash(message)
1797        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1798        self.context.__parent__.__parent__.__parent__.logger.info(
1799            '%s - added: %s' % (ob_class, data['code']))
1800        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1801        return
1802
1803    @action(_('Cancel'), validator=NullValidator)
1804    def cancel(self, **data):
1805        self.redirect(self.url(self.context))
1806        return
1807
1808class CertificateAddFormPage(KofaAddFormPage):
1809    """Add-form to add certificate to a department.
1810    """
1811    grok.context(IDepartment)
1812    grok.name('addcertificate')
1813    grok.require('waeup.manageAcademics')
1814    label = _(u'Add certificate')
1815    form_fields = grok.AutoFields(ICertificateAdd)
1816    pnav = 1
1817
1818    @action(_('Add certificate'))
1819    def addCertificate(self, **data):
1820        certificate = createObject(u'waeup.Certificate')
1821        self.applyData(certificate, **data)
1822        try:
1823            self.context.certificates.addCertificate(certificate)
1824        except DuplicationError, error:
1825            # signals duplication error
1826            entries = error.entries
1827            for entry in entries:
1828                # do not use error.msg but render a more detailed message instead
1829                # and show links to all certs with same code
1830                message = _('A certificate with same code already exists: ')
1831                message += '<a href="%s">%s</a>' % (
1832                    self.url(entry), getattr(entry, '__name__', u'Unnamed'))
1833                self.flash(message)
1834            self.redirect(self.url(self.context, u'@@addcertificate'))
1835            return
1836        message = _(u'Certificate ${a} successfully created.', mapping = {'a':certificate.code})
1837        self.flash(message)
1838        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1839        self.context.__parent__.__parent__.__parent__.logger.info(
1840            '%s - added: %s' % (ob_class, data['code']))
1841        self.redirect(self.url(self.context, u'@@manage')+'?tab3')
1842        return
1843
1844    @action(_('Cancel'), validator=NullValidator)
1845    def cancel(self): #, **data):
1846        self.redirect(self.url(self.context))
1847        return
1848
1849#
1850# Courses pages
1851#
1852class CoursePage(KofaDisplayFormPage):
1853    """Course index page.
1854    """
1855    grok.context(ICourse)
1856    grok.name('index')
1857    grok.require('waeup.viewAcademics')
1858    pnav = 1
1859    form_fields = grok.AutoFields(ICourse)
1860
1861    @property
1862    def label(self):
1863        return '%s (%s)' % (self.context.title, self.context.code)
1864
1865class CourseManageFormPage(KofaEditFormPage):
1866    """Edit form page for courses.
1867    """
1868    grok.context(ICourse)
1869    grok.name('manage')
1870    grok.require('waeup.manageAcademics')
1871    label = _(u'Edit course')
1872    pnav = 1
1873
1874    form_fields = grok.AutoFields(ICourse)
1875
1876    @action(_('Save'), style='primary')
1877    def save(self, **data):
1878        return msave(self, **data)
1879
1880    @action(_('Cancel'), validator=NullValidator)
1881    def cancel(self, **data):
1882        self.redirect(self.url(self.context))
1883        return
1884
1885#
1886# Certificate pages
1887#
1888class CertificatePage(KofaDisplayFormPage):
1889    """Index page for certificates.
1890    """
1891    grok.context(ICertificate)
1892    grok.name('index')
1893    grok.require('waeup.viewAcademics')
1894    pnav = 1
1895    form_fields = grok.AutoFields(ICertificate)
1896    grok.template('certificatepage')
1897
1898    @property
1899    def label(self):
1900        return "%s (%s)" % (self.context.title, self.context.code)
1901
1902    def update(self):
1903        datatable.need()
1904        return super(CertificatePage, self).update()
1905
1906class CertificateManageFormPage(KofaEditFormPage):
1907    """Manage the properties of a `Certificate` instance.
1908    """
1909    grok.context(ICertificate)
1910    grok.name('manage')
1911    grok.require('waeup.manageAcademics')
1912    pnav = 1
1913    label = _('Edit certificate')
1914
1915    form_fields = grok.AutoFields(ICertificate)
1916
1917    pnav = 1
1918    grok.template('certificatemanagepage')
1919    taboneactions = [_('Save'),_('Cancel')]
1920    tabtwoactions = [_('Add course referrer'),
1921                     _('Remove selected course referrers'),_('Cancel')]
1922    tabthreeactions1 = [_('Remove selected local roles')]
1923    tabthreeactions2 = [_('Add local role')]
1924
1925    @property
1926    def label(self):
1927        return _('Manage certificate')
1928
1929    def update(self):
1930        tabs.need()
1931        self.tab1 = self.tab2 = self.tab3 = ''
1932        qs = self.request.get('QUERY_STRING', '')
1933        if not qs:
1934            qs = 'tab1'
1935        setattr(self, qs, 'active')
1936        warning.need()
1937        datatable.need()
1938        return super(CertificateManageFormPage, self).update()
1939
1940    @action(_('Save'), style='primary')
1941    def save(self, **data):
1942        return msave(self, **data)
1943
1944    @jsaction(_('Remove selected course referrers'))
1945    def delCertificateCourses(self, **data):
1946        delSubobjects(self, redirect='@@manage', tab='2')
1947        return
1948
1949    @action(_('Add course referrer'), validator=NullValidator)
1950    def addCertificateCourse(self, **data):
1951        self.redirect(self.url(self.context, 'addcertificatecourse'))
1952        return
1953
1954    @action(_('Cancel'), validator=NullValidator)
1955    def cancel(self, **data):
1956        self.redirect(self.url(self.context))
1957        return
1958
1959    def getLocalRoles(self):
1960        roles = ILocalRolesAssignable(self.context)
1961        return roles()
1962
1963    def getUsers(self):
1964        return get_all_users()
1965
1966    def getUsersWithLocalRoles(self):
1967        return get_users_with_local_roles(self.context)
1968
1969    @action(_('Add local role'), validator=NullValidator)
1970    def addLocalRole(self, **data):
1971        return add_local_role(self, 3, **data)
1972
1973    @action(_('Remove selected local roles'))
1974    def delLocalRoles(self, **data):
1975        return del_local_roles(self,3,**data)
1976
1977
1978class CertificateCourseAddFormPage(KofaAddFormPage):
1979    """Add-page to add a course ref to a certificate
1980    """
1981    grok.context(ICertificate)
1982    grok.name('addcertificatecourse')
1983    grok.require('waeup.manageAcademics')
1984    form_fields = grok.AutoFields(ICertificateCourseAdd)
1985    pnav = 1
1986    label = _('Add course referrer')
1987
1988    @action(_('Add course referrer'))
1989    def addCertcourse(self, **data):
1990        try:
1991            self.context.addCourseRef(**data)
1992        except KeyError:
1993            self.status = self.flash(_('The chosen course referrer is already '
1994                                  'part of this certificate.'))
1995            return
1996        self.status = self.flash(
1997            _("Course referrer ${a}_${b} added.",
1998            mapping = {'a': data['course'].code, 'b': data['level']}))
1999        code = "%s_%s" % (data['course'].code, data['level'])
2000        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2001        grok.getSite().logger.info('%s - added: %s' % (ob_class, code))
2002        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
2003        return
2004
2005    @action(_('Cancel'), validator=NullValidator)
2006    def cancel(self, **data):
2007        self.redirect(self.url(self.context))
2008        return
2009
2010
2011#
2012# Certificate course pages...
2013#
2014class CertificateCoursePage(KofaPage):
2015    """CertificateCourse index page.
2016    """
2017    grok.context(ICertificateCourse)
2018    grok.name('index')
2019    grok.require('waeup.viewAcademics')
2020    pnav = 1
2021    #form_fields = grok.AutoFields(ICertificateCourse)
2022
2023    @property
2024    def label(self):
2025        return '%s' % (self.context.longtitle())
2026
2027    @property
2028    def leveltitle(self):
2029        return course_levels.getTerm(self.context.level).title
2030
2031class CertificateCourseManageFormPage(KofaEditFormPage):
2032    """Manage the basic properties of a `CertificateCourse` instance.
2033    """
2034    grok.context(ICertificateCourse)
2035    grok.name('manage')
2036    grok.require('waeup.manageAcademics')
2037    form_fields = grok.AutoFields(ICertificateCourse)
2038    label = _('Edit course referrer')
2039    pnav = 1
2040
2041    @action(_('Save and return'), style='primary')
2042    def saveAndReturn(self, **data):
2043        parent = self.context.__parent__
2044        if self.context.level == data['level']:
2045            msave(self, **data)
2046        else:
2047            # The level changed. We have to create a new certcourse as
2048            # the key of the entry will change as well...
2049            old_level = self.context.level
2050            data['course'] = self.context.course
2051            parent.addCourseRef(**data)
2052            parent.delCourseRef(data['course'].code, level=old_level)
2053            self.flash(_('Form has been saved.'))
2054            old_code = "%s_%s" % (data['course'].code, old_level)
2055            new_code = "%s_%s" % (data['course'].code, data['level'])
2056            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2057            grok.getSite().logger.info(
2058                '%s - renamed: %s to %s' % (ob_class, old_code, new_code))
2059        self.redirect(self.url(parent))
2060        return
2061
2062    @action(_('Cancel'), validator=NullValidator)
2063    def cancel(self, **data):
2064        self.redirect(self.url(self.context))
2065        return
2066
2067class ChangePasswordRequestPage(KofaForm):
2068    """Captcha'd page for all kind of users to request a password change.
2069    """
2070    grok.context(IUniversity)
2071    grok.name('changepw')
2072    grok.require('waeup.Anonymous')
2073    grok.template('changepw')
2074    label = _('Send me a new password')
2075    form_fields = grok.AutoFields(IChangePassword)
2076
2077    def update(self):
2078        # Handle captcha
2079        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2080        self.captcha_result = self.captcha.verify(self.request)
2081        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2082        return
2083
2084    def _searchUser(self, identifier, email):
2085        # Search student
2086        cat = queryUtility(ICatalog, name='students_catalog')
2087        results = cat.searchResults(
2088            #reg_number=(identifier, identifier),
2089            email=(email,email))
2090        for result in results:
2091            if result.student_id == identifier or \
2092                result.reg_number == identifier or \
2093                result.matric_number == identifier:
2094                return result
2095        # Search applicant
2096        cat = queryUtility(ICatalog, name='applicants_catalog')
2097        if cat is not None:
2098            results = cat.searchResults(
2099                #reg_number=(identifier, identifier),
2100                email=(email,email))
2101            for result in results:
2102                if result.applicant_id == identifier or \
2103                    result.reg_number == identifier:
2104                    return result
2105        # Search portal user
2106        user = grok.getSite()['users'].get(identifier, None)
2107        if user is not None and user.email == email:
2108            return user
2109        return None
2110
2111    @action(_('Get login credentials'), style='primary')
2112    def request(self, **data):
2113        if not self.captcha_result.is_valid:
2114            # Captcha will display error messages automatically.
2115            # No need to flash something.
2116            return
2117        # Search student or applicant
2118        identifier = data['identifier']
2119        email = data['email']
2120        user = self._searchUser(identifier, email)
2121        if user is None:
2122            self.flash(_('No record found.'))
2123            return
2124        # Change password
2125        kofa_utils = getUtility(IKofaUtils)
2126        pwd = kofa_utils.genPassword()
2127        IUserAccount(user).setPassword(pwd)
2128        # Send email with new redentials
2129        msg = _('You have successfully changed your password for the')
2130        login_url = self.url(grok.getSite(), 'login')
2131        success = kofa_utils.sendCredentials(
2132            IUserAccount(user),pwd,login_url,msg)
2133        if success:
2134            self.flash(_('An email with your user name and password ' +
2135                'has been sent to ${a}.', mapping = {'a':email}))
2136        else:
2137            self.flash(_('An smtp server error occurred.'))
2138        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2139        self.context.logger.info(
2140            '%s - %s - %s' % (ob_class, data['identifier'], data['email']))
2141        return
Note: See TracBrowser for help on using the repository browser.