source: main/waeup.kofa/trunk/src/waeup/kofa/browser/pages.py @ 9707

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

Ease customization of displaying suspended_comment.

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