source: main/waeup.kofa/branches/henrik-regista/src/waeup/ikoba/browser/pages.py @ 11952

Last change on this file since 11952 was 11952, checked in by Henrik Bettermann, 10 years ago

More renaming: University -> Institution, Student -> Customer
Change portal title.

  • Property svn:keywords set to Id
File size: 59.0 KB
Line 
1## $Id: pages.py 11952 2014-11-13 16:03:08Z 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 Ikoba objects.
19"""
20# XXX: All csv ops should move to a dedicated module soon
21import unicodecsv as csv
22import grok
23import os
24import re
25import sys
26from datetime import datetime, timedelta
27from urllib import urlencode
28from hurry.query import Eq, Text
29from hurry.query.query import Query
30from zope import schema
31from zope.i18n import translate
32from zope.authentication.interfaces import (
33    IAuthentication, IUnauthenticatedPrincipal, ILogout)
34from zope.catalog.interfaces import ICatalog
35from zope.component import (
36    getUtility, queryUtility, createObject, getAllUtilitiesRegisteredFor,
37    getUtilitiesFor,
38    )
39from zope.event import notify
40from zope.security import checkPermission
41from zope.securitypolicy.interfaces import IPrincipalRoleManager
42from zope.session.interfaces import ISession
43from zope.password.interfaces import IPasswordManager
44from waeup.ikoba.browser.layout import (
45    IkobaPage, IkobaForm, IkobaEditFormPage, IkobaAddFormPage,
46    IkobaDisplayFormPage, NullValidator)
47from waeup.ikoba.browser.interfaces import (
48    IInstitution, ICaptchaManager, IChangePassword)
49from waeup.ikoba.browser.layout import jsaction, action, UtilityView
50from waeup.ikoba.interfaces import MessageFactory as _
51from waeup.ikoba.interfaces import(
52    IIkobaObject, IUsersContainer, IUserAccount, IDataCenter,
53    IIkobaXMLImporter, IIkobaXMLExporter, IBatchProcessor,
54    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
55    ISessionConfiguration, ISessionConfigurationAdd, IJobManager,
56    IPasswordValidator, IContactForm, IIkobaUtils, ICSVExporter,
57    application_sessions_vocab)
58from waeup.ikoba.permissions import (
59    get_users_with_local_roles, get_all_roles, get_all_users,
60    get_users_with_role)
61
62from waeup.ikoba.authentication import LocalRoleSetEvent
63from waeup.ikoba.widgets.htmlwidget import HTMLDisplayWidget
64from waeup.ikoba.utils.helpers import get_user_account, check_csv_charset
65from waeup.ikoba.mandates.mandate import PasswordMandate
66from waeup.ikoba.datacenter import DataCenterFile
67
68FORBIDDEN_CHARACTERS = (160,)
69
70grok.context(IIkobaObject)
71grok.templatedir('templates')
72
73def add_local_role(view, tab, **data):
74    localrole = view.request.form.get('local_role', None)
75    user = view.request.form.get('user', None)
76    if user is None or localrole is None:
77        view.flash('No user selected.', type='danger')
78        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
79        return
80    role_manager = IPrincipalRoleManager(view.context)
81    role_manager.assignRoleToPrincipal(localrole, user)
82    notify(LocalRoleSetEvent(view.context, localrole, user, granted=True))
83    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
84    grok.getSite().logger.info(
85        '%s - added: %s|%s' % (ob_class, user, localrole))
86    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
87    return
88
89def del_local_roles(view, tab, **data):
90    child_ids = view.request.form.get('role_id', None)
91    if child_ids is None:
92        view.flash(_('No local role selected.'), type='danger')
93        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
94        return
95    if not isinstance(child_ids, list):
96        child_ids = [child_ids]
97    deleted = []
98    role_manager = IPrincipalRoleManager(view.context)
99    for child_id in child_ids:
100        localrole = child_id.split('|')[1]
101        user_name = child_id.split('|')[0]
102        try:
103            role_manager.unsetRoleForPrincipal(localrole, user_name)
104            notify(LocalRoleSetEvent(
105                    view.context, localrole, user_name, granted=False))
106            deleted.append(child_id)
107        except:
108            view.flash('Could not remove %s: %s: %s' % (
109                    child_id, sys.exc_info()[0], sys.exc_info()[1]),
110                    type='danger')
111    if len(deleted):
112        view.flash(
113            _('Local role successfully removed: ${a}',
114            mapping = {'a':', '.join(deleted)}))
115        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
116        grok.getSite().logger.info(
117            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
118    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
119    return
120
121def delSubobjects(view, redirect, tab=None, subcontainer=None):
122    form = view.request.form
123    if 'val_id' in form:
124        child_id = form['val_id']
125    else:
126        view.flash(_('No item selected.'), type='danger')
127        if tab:
128            view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
129        else:
130            view.redirect(view.url(view.context, redirect))
131        return
132    if not isinstance(child_id, list):
133        child_id = [child_id]
134    deleted = []
135    for id in child_id:
136        try:
137            if subcontainer:
138                container = getattr(view.context, subcontainer, None)
139                del container[id]
140            else:
141                del view.context[id]
142            deleted.append(id)
143        except:
144            view.flash('Could not delete %s: %s: %s' % (
145                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
146    if len(deleted):
147        view.flash(_('Successfully removed: ${a}',
148            mapping = {'a': ', '.join(deleted)}))
149        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
150        grok.getSite().logger.info(
151            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
152    if tab:
153        view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
154    else:
155        view.redirect(view.url(view.context, redirect))
156    return
157
158def getPreviewTable(view, n):
159    """Get transposed table with n sample record.
160
161    The first column contains the headers.
162    """
163    if not view.reader:
164        return
165    header = view.getPreviewHeader()
166    num = 0
167    data = []
168    for line in view.reader:
169        if num > n-1:
170            break
171        num += 1
172        data.append(line)
173    result = []
174    for name in header:
175        result_line = []
176        result_line.append(name)
177        for d in data:
178            result_line.append(d[name])
179        result.append(result_line)
180    return result
181
182# Save function used for save methods in pages
183def msave(view, **data):
184    changed_fields = view.applyData(view.context, **data)
185    # Turn list of lists into single list
186    if changed_fields:
187        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
188    fields_string = ' + '.join(changed_fields)
189    view.flash(_('Form has been saved.'))
190    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
191    if fields_string:
192        grok.getSite().logger.info('%s - %s - saved: %s' % (ob_class, view.context.__name__, fields_string))
193    return
194
195def doll_up(view, user=None):
196    """Doll up export jobs for displaying in table.
197    """
198    job_entries = view.context.get_running_export_jobs(user)
199    job_manager = getUtility(IJobManager)
200    entries = []
201    for job_id, exporter_name, user_id in job_entries:
202        job = job_manager.get(job_id)
203        exporter = getUtility(ICSVExporter, name=exporter_name)
204        exporter_title = getattr(exporter, 'title', 'Unknown')
205        args = ', '.join(['%s=%s' % (item[0], item[1])
206            for item in job.kwargs.items()])
207        status = job.finished and 'ready' or 'running'
208        status = job.failed and 'FAILED' or status
209        start_time = getattr(job, 'begin_after', None)
210        time_delta = None
211        if start_time:
212            tz = getUtility(IIkobaUtils).tzinfo
213            time_delta = datetime.now(tz) - start_time
214            start_time = start_time.astimezone(tz).strftime(
215                "%Y-%m-%d %H:%M:%S %Z")
216        download_url = view.url(view.context, 'download_export',
217                                data=dict(job_id=job_id))
218        show_download_button = job.finished and not \
219                               job.failed and time_delta and \
220                               time_delta.days < 1
221        new_entry = dict(
222            job=job_id,
223            creator=user_id,
224            args=args,
225            exporter=exporter_title,
226            status=status,
227            start_time=start_time,
228            download_url=download_url,
229            show_download_button = show_download_button,
230            show_refresh_button = not job.finished,
231            show_discard_button = job.finished,)
232        entries.append(new_entry)
233    return entries
234
235class LocalRoleAssignmentUtilityView(object):
236    """A view mixin with useful methods for local role assignment.
237
238    """
239
240    def getLocalRoles(self):
241        roles = ILocalRolesAssignable(self.context)
242        return roles()
243
244    def getUsers(self):
245        return get_all_users()
246
247    def getUsersWithLocalRoles(self):
248        return get_users_with_local_roles(self.context)
249
250#
251# Login/logout and language switch pages...
252#
253
254class LoginPage(IkobaPage):
255    """A login page, available for all objects.
256    """
257    grok.name('login')
258    grok.context(IIkobaObject)
259    grok.require('waeup.Public')
260    label = _(u'Login')
261    camefrom = None
262    login_button = label
263
264    def update(self, SUBMIT=None, camefrom=None):
265        self.camefrom = camefrom
266        if SUBMIT is not None:
267            if self.request.principal.id != 'zope.anybody':
268                self.flash(_('You logged in.'))
269                if self.request.principal.user_type == 'customer':
270                    customer = grok.getSite()['customers'][
271                        self.request.principal.id]
272                    rel_link = '/customers/%s' % self.request.principal.id
273                    if customer.personal_data_expired:
274                        rel_link = '/customerss/%s/edit_personal' % (
275                            self.request.principal.id)
276                        self.flash(
277                          _('Your personal data record is outdated. Please update.'),
278                          type='warning')
279                    self.redirect(self.application_url() + rel_link)
280                    return
281                if not self.camefrom:
282                    self.redirect(self.application_url() + '/index')
283                    return
284                self.redirect(self.camefrom)
285                return
286            self.flash(_('You entered invalid credentials.'), type='danger')
287            return
288
289
290class LogoutPage(IkobaPage):
291    """A logout page. Calling this page will log the current user out.
292    """
293    grok.context(IIkobaObject)
294    grok.require('waeup.Public')
295    grok.name('logout')
296
297    def update(self):
298        if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
299            auth = getUtility(IAuthentication)
300            ILogout(auth).logout(self.request)
301            self.flash(_("You have been logged out. Thanks for using WAeUP Ikoba!"))
302        self.redirect(self.application_url() + '/index')
303        return
304
305
306class LanguageChangePage(IkobaPage):
307    """ Language switch
308    """
309    grok.context(IIkobaObject)
310    grok.name('change_language')
311    grok.require('waeup.Public')
312
313    def update(self, lang='en', view_name='@@index'):
314        self.response.setCookie('ikoba.language', lang, path='/')
315        self.redirect(self.url(self.context, view_name))
316        return
317
318    def render(self):
319        return
320
321#
322# Contact form...
323#
324
325class ContactAdminForm(IkobaForm):
326    grok.name('contactadmin')
327    #grok.context(IInstitution)
328    grok.template('contactform')
329    grok.require('waeup.Authenticated')
330    pnav = 2
331    form_fields = grok.AutoFields(IContactForm).select('body')
332
333    def update(self):
334        super(ContactAdminForm, self).update()
335        self.form_fields.get('body').field.default = None
336        return
337
338    @property
339    def config(self):
340        return grok.getSite()['configuration']
341
342    def label(self):
343        return _(u'Contact ${a}', mapping = {'a': self.config.name_admin})
344
345    @property
346    def get_user_account(self):
347        return get_user_account(self.request)
348
349    @action(_('Send message now'), style='primary')
350    def send(self, *args, **data):
351        fullname = self.request.principal.title
352        try:
353            email = self.request.principal.email
354        except AttributeError:
355            email = self.config.email_admin
356        username = self.request.principal.id
357        usertype = getattr(self.request.principal,
358                           'user_type', 'system').title()
359        ikoba_utils = getUtility(IIkobaUtils)
360        success = ikoba_utils.sendContactForm(
361                fullname,email,
362                self.config.name_admin,self.config.email_admin,
363                username,usertype,self.config.name,
364                data['body'],self.config.email_subject)
365        # Success is always True if sendContactForm didn't fail.
366        # TODO: Catch exceptions.
367        if success:
368            self.flash(_('Your message has been sent.'))
369        return
370
371class EnquiriesForm(ContactAdminForm):
372    """Captcha'd page to let anonymous send emails to the administrator.
373    """
374    grok.name('enquiries')
375    grok.require('waeup.Public')
376    pnav = 2
377    form_fields = grok.AutoFields(IContactForm).select(
378                          'fullname', 'email_from', 'body')
379
380    def update(self):
381        super(EnquiriesForm, self).update()
382        # Handle captcha
383        self.captcha = getUtility(ICaptchaManager).getCaptcha()
384        self.captcha_result = self.captcha.verify(self.request)
385        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
386        return
387
388    @action(_('Send now'), style='primary')
389    def send(self, *args, **data):
390        if not self.captcha_result.is_valid:
391            # Captcha will display error messages automatically.
392            # No need to flash something.
393            return
394        ikoba_utils = getUtility(IIkobaUtils)
395        success = ikoba_utils.sendContactForm(
396                data['fullname'],data['email_from'],
397                self.config.name_admin,self.config.email_admin,
398                u'None',u'Anonymous',self.config.name,
399                data['body'],self.config.email_subject)
400        if success:
401            self.flash(_('Your message has been sent.'))
402        else:
403            self.flash(_('A smtp server error occurred.'), type='danger')
404        return
405
406#
407# Institution related pages...
408#
409
410class InstitutionPage(IkobaDisplayFormPage):
411    """ The main institution page.
412    """
413    grok.require('waeup.Public')
414    grok.name('index')
415    grok.context(IInstitution)
416    pnav = 0
417    label = ''
418
419    @property
420    def frontpage(self):
421        lang = self.request.cookies.get('ikoba.language')
422        html = self.context['configuration'].frontpage_dict.get(lang,'')
423        if html =='':
424            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
425            html = self.context[
426                'configuration'].frontpage_dict.get(portal_language,'')
427        if html =='':
428            return _(u'<h1>Welcome to WAeUP.Ikoba</h1>')
429        else:
430            return html
431
432class AdministrationPage(IkobaPage):
433    """ The administration overview page.
434    """
435    grok.name('administration')
436    grok.context(IInstitution)
437    grok.require('waeup.managePortal')
438    label = _(u'Administration')
439    pnav = 0
440
441class RSS20Feed(grok.View):
442    """An RSS 2.0 feed.
443    """
444    grok.name('feed.rss')
445    grok.context(IInstitution)
446    grok.require('waeup.Public')
447    grok.template('institutionrss20feed')
448
449    name = 'General news feed'
450    description = 'waeup.ikoba now supports RSS 2.0 feeds :-)'
451    language = None
452    date = None
453    buildDate = None
454    editor = None
455    webmaster = None
456
457    @property
458    def title(self):
459        return getattr(grok.getSite(), 'name', u'Sample Institution')
460
461    @property
462    def contexttitle(self):
463        return self.name
464
465    @property
466    def link(self):
467        return self.url(grok.getSite())
468
469    def update(self):
470        self.response.setHeader('Content-Type', 'text/xml; charset=UTF-8')
471
472    def entries(self):
473        return ()
474
475#
476# User container pages...
477#
478
479class UsersContainerPage(IkobaPage):
480    """Overview page for all local users.
481    """
482    grok.require('waeup.manageUsers')
483    grok.context(IUsersContainer)
484    grok.name('index')
485    label = _('Portal Users')
486    manage_button = _(u'Manage')
487    delete_button = _(u'Remove')
488
489    def update(self, userid=None, adduser=None, manage=None, delete=None):
490        if manage is not None and userid is not None:
491            self.redirect(self.url(userid) + '/@@manage')
492        if delete is not None and userid is not None:
493            self.context.delUser(userid)
494            self.flash(_('User account ${a} successfully deleted.',
495                mapping = {'a':  userid}))
496            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
497            self.context.__parent__.logger.info(
498                '%s - removed: %s' % (ob_class, userid))
499
500    def getLocalRoles(self, account):
501        local_roles = account.getLocalRoles()
502        local_roles_string = ''
503        site_url = self.url(grok.getSite())
504        for local_role in local_roles.keys():
505            role_title = getattr(
506                dict(get_all_roles()).get(local_role, None), 'title', None)
507            objects_string = ''
508            for object in local_roles[local_role]:
509                objects_string += '<a href="%s">%s</a>, ' %(self.url(object),
510                    self.url(object).replace(site_url,''))
511            local_roles_string += '%s: <br />%s <br />' %(role_title,
512                objects_string.rstrip(', '))
513        return local_roles_string
514
515    def getSiteRoles(self, account):
516        site_roles = account.roles
517        site_roles_string = ''
518        for site_role in site_roles:
519            role_title = dict(get_all_roles())[site_role].title
520            site_roles_string += '%s <br />' % role_title
521        return site_roles_string
522
523class AddUserFormPage(IkobaAddFormPage):
524    """Add a user account.
525    """
526    grok.require('waeup.manageUsers')
527    grok.context(IUsersContainer)
528    grok.name('add')
529    grok.template('usereditformpage')
530    form_fields = grok.AutoFields(IUserAccount)
531    label = _('Add user')
532
533    @action(_('Add user'), style='primary')
534    def addUser(self, **data):
535        name = data['name']
536        title = data['title']
537        email = data['email']
538        phone = data['phone']
539        description = data['description']
540        #password = data['password']
541        roles = data['roles']
542        form = self.request.form
543        password = form.get('password', None)
544        password_ctl = form.get('control_password', None)
545        if password:
546            validator = getUtility(IPasswordValidator)
547            errors = validator.validate_password(password, password_ctl)
548            if errors:
549                self.flash( ' '.join(errors), type='danger')
550                return
551        try:
552            self.context.addUser(name, password, title=title, email=email,
553                                 phone=phone, description=description,
554                                 roles=roles)
555            self.flash(_('User account ${a} successfully added.',
556                mapping = {'a': name}))
557            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
558            self.context.__parent__.logger.info(
559                '%s - added: %s' % (ob_class, name))
560        except KeyError:
561            self.status = self.flash('The userid chosen already exists '
562                                  'in the database.', type='danger')
563            return
564        self.redirect(self.url(self.context))
565
566class UserManageFormPage(IkobaEditFormPage):
567    """Manage a user account.
568    """
569    grok.context(IUserAccount)
570    grok.name('manage')
571    grok.template('usereditformpage')
572    grok.require('waeup.manageUsers')
573    form_fields = grok.AutoFields(IUserAccount).omit('name')
574
575    def label(self):
576        return _("Edit user ${a}", mapping = {'a':self.context.__name__})
577
578    def setUpWidgets(self, ignore_request=False):
579        super(UserManageFormPage,self).setUpWidgets(ignore_request)
580        self.widgets['title'].displayWidth = 30
581        self.widgets['description'].height = 3
582        return
583
584    @action(_('Save'), style='primary')
585    def save(self, **data):
586        form = self.request.form
587        password = form.get('password', None)
588        password_ctl = form.get('control_password', None)
589        if password:
590            validator = getUtility(IPasswordValidator)
591            errors = validator.validate_password(password, password_ctl)
592            if errors:
593                self.flash( ' '.join(errors), type='danger')
594                return
595        changed_fields = self.applyData(self.context, **data)
596        if changed_fields:
597            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
598        else:
599            changed_fields = []
600        if password:
601            # Now we know that the form has no errors and can set password ...
602            self.context.setPassword(password)
603            changed_fields.append('password')
604        fields_string = ' + '.join(changed_fields)
605        if fields_string:
606            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
607            self.context.__parent__.logger.info(
608                '%s - %s edited: %s' % (
609                ob_class, self.context.name, fields_string))
610        self.flash(_('User settings have been saved.'))
611        return
612
613    @action(_('Cancel'), validator=NullValidator)
614    def cancel(self, **data):
615        self.redirect(self.url(self.context.__parent__))
616        return
617
618class ContactUserForm(ContactAdminForm):
619    grok.name('contactuser')
620    grok.context(IUserAccount)
621    grok.template('contactform')
622    grok.require('waeup.manageUsers')
623    pnav = 0
624    form_fields = grok.AutoFields(IContactForm).select('body')
625
626    def label(self):
627        return _(u'Send message to ${a}', mapping = {'a':self.context.title})
628
629    @action(_('Send message now'), style='primary')
630    def send(self, *args, **data):
631        try:
632            email = self.request.principal.email
633        except AttributeError:
634            email = self.config.email_admin
635        usertype = getattr(self.request.principal,
636                           'user_type', 'system').title()
637        ikoba_utils = getUtility(IIkobaUtils)
638        success = ikoba_utils.sendContactForm(
639                self.request.principal.title,email,
640                self.context.title,self.context.email,
641                self.request.principal.id,usertype,self.config.name,
642                data['body'],self.config.email_subject)
643        # Success is always True if sendContactForm didn't fail.
644        # TODO: Catch exceptions.
645        if success:
646            self.flash(_('Your message has been sent.'))
647        return
648
649class UserEditFormPage(UserManageFormPage):
650    """Edit a user account by user
651    """
652    grok.name('index')
653    grok.require('waeup.editUser')
654    form_fields = grok.AutoFields(IUserAccount).omit(
655        'name', 'description', 'roles')
656    label = _(u"My Preferences")
657
658    def setUpWidgets(self, ignore_request=False):
659        super(UserManageFormPage,self).setUpWidgets(ignore_request)
660        self.widgets['title'].displayWidth = 30
661
662class MyRolesPage(IkobaPage):
663    """Display site roles and local roles assigned to officers.
664    """
665    grok.name('my_roles')
666    grok.require('waeup.editUser')
667    grok.context(IUserAccount)
668    grok.template('myrolespage')
669    label = _(u"My Roles")
670
671    @property
672    def getLocalRoles(self):
673        local_roles = get_user_account(self.request).getLocalRoles()
674        local_roles_userfriendly = {}
675        for local_role in local_roles:
676            role_title = dict(get_all_roles())[local_role].title
677            local_roles_userfriendly[role_title] = local_roles[local_role]
678        return local_roles_userfriendly
679
680    @property
681    def getSiteRoles(self):
682        site_roles = get_user_account(self.request).roles
683        site_roles_userfriendly = []
684        for site_role in site_roles:
685            role_title = dict(get_all_roles())[site_role].title
686            site_roles_userfriendly.append(role_title)
687        return site_roles_userfriendly
688
689#
690# Configuration pages...
691#
692
693class ConfigurationContainerDisplayFormPage(IkobaDisplayFormPage):
694    """View page of the configuration container.
695    """
696    grok.require('waeup.managePortalConfiguration')
697    grok.name('view')
698    grok.context(IConfigurationContainer)
699    pnav = 0
700    label = _(u'View portal configuration')
701    form_fields = grok.AutoFields(IConfigurationContainer)
702    form_fields['frontpage'].custom_widget = HTMLDisplayWidget
703
704
705class ConfigurationContainerManageFormPage(IkobaEditFormPage):
706    """Manage page of the configuration container. We always use the
707    manage page in the UI not the view page, thus we use the index name here.
708    """
709    grok.require('waeup.managePortalConfiguration')
710    grok.name('index')
711    grok.context(IConfigurationContainer)
712    grok.template('configurationmanagepage')
713    pnav = 0
714    label = _(u'Edit portal configuration')
715    taboneactions = [_('Save'), _('Update plugins')]
716    tabtwoactions = [
717        _('Add session configuration'),
718        _('Remove selected')]
719    form_fields = grok.AutoFields(IConfigurationContainer).omit(
720        'frontpage_dict')
721
722    def _frontpage(self):
723        view = ConfigurationContainerDisplayFormPage(
724            self.context,self.request)
725        view.setUpWidgets()
726        return view.widgets['frontpage']()
727
728    @action(_('Save'), style='primary')
729    def save(self, **data):
730        msave(self, **data)
731        self.context.frontpage_dict = self._frontpage()
732        return
733
734    @action(_('Add session configuration'), validator=NullValidator,
735            style='primary')
736    def addSubunit(self, **data):
737        self.redirect(self.url(self.context, '@@add'))
738        return
739
740    def getSessionConfigurations(self):
741        """Get a list of all stored session configuration objects.
742        """
743        for key, val in self.context.items():
744            url = self.url(val)
745            session_string = val.getSessionString()
746            title = _('Session ${a} Configuration',
747                      mapping = {'a':session_string})
748            yield(dict(url=url, name=key, title=title))
749
750    @jsaction(_('Remove selected'))
751    def delSessonConfigurations(self, **data):
752        delSubobjects(self, redirect='@@index', tab='2')
753        return
754
755    @action(_('Update plugins'),
756              tooltip=_('For experts only!'),
757              warning=_('Plugins may only be updated after software upgrades. '
758                        'Are you really sure?'),
759              validator=NullValidator)
760    def updatePlugins(self, **data):
761        grok.getSite().updatePlugins()
762        self.flash(_('Plugins were updated. See log file for details.'))
763        return
764
765class SessionConfigurationAddFormPage(IkobaAddFormPage):
766    """Add a session configuration object to configuration container.
767    """
768    grok.context(IConfigurationContainer)
769    grok.name('add')
770    grok.require('waeup.managePortalConfiguration')
771    label = _('Add session configuration')
772    form_fields = grok.AutoFields(ISessionConfigurationAdd)
773    pnav = 0
774
775    @action(_('Add Session Configuration'), style='primary')
776    def addSessionConfiguration(self, **data):
777        sessionconfiguration = createObject(u'waeup.SessionConfiguration')
778        self.applyData(sessionconfiguration, **data)
779        try:
780            self.context.addSessionConfiguration(sessionconfiguration)
781            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
782            self.context.__parent__.logger.info(
783                '%s - added: %s' % (
784                ob_class, sessionconfiguration.application_session))
785        except KeyError:
786            self.flash(_('The session chosen already exists.'), type='danger')
787            return
788        self.redirect(self.url(self.context, '@@index')+'#tab2')
789        return
790
791    @action(_('Cancel'), validator=NullValidator)
792    def cancel(self):
793        self.redirect(self.url(self.context, '@@index')+'#tab2')
794        return
795
796class SessionConfigurationManageFormPage(IkobaEditFormPage):
797    """Manage session configuration object.
798    """
799    grok.context(ISessionConfiguration)
800    grok.name('index')
801    grok.require('waeup.managePortalConfiguration')
802    form_fields = grok.AutoFields(ISessionConfiguration)
803    pnav = 0
804
805    @property
806    def label(self):
807        session_string = self.context.getSessionString()
808        return _('Edit academic session ${a} configuration',
809            mapping = {'a':session_string})
810
811    @action(_('Save'), style='primary')
812    def save(self, **data):
813        msave(self, **data)
814        self.redirect(self.url(self.context.__parent__, '@@index')+'#tab2')
815        return
816
817    @action(_('Cancel'), validator=NullValidator)
818    def cancel(self):
819        self.redirect(self.url(self.context.__parent__, '@@index')+'#tab2')
820        return
821
822#
823# Datacenter pages...
824#
825
826class DatacenterPage(IkobaEditFormPage):
827    grok.context(IDataCenter)
828    grok.name('index')
829    grok.require('waeup.manageDataCenter')
830    label = _(u'Data Center')
831    pnav = 0
832
833    @jsaction(_('Remove selected'))
834    def delFiles(self, **data):
835        form = self.request.form
836        if 'val_id' in form:
837            child_id = form['val_id']
838        else:
839            self.flash(_('No item selected.'), type='danger')
840            return
841        if not isinstance(child_id, list):
842            child_id = [child_id]
843        deleted = []
844        for id in child_id:
845            fullpath = os.path.join(self.context.storage, id)
846            try:
847                os.remove(fullpath)
848                deleted.append(id)
849            except OSError:
850                self.flash(_('OSError: The file could not be deleted.'),
851                           type='danger')
852                return
853        if len(deleted):
854            self.flash(_('Successfully deleted: ${a}',
855                mapping = {'a': ', '.join(deleted)}))
856            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
857            self.context.logger.info(
858                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
859        return
860
861class DatacenterFinishedPage(IkobaEditFormPage):
862    grok.context(IDataCenter)
863    grok.name('processed')
864    grok.require('waeup.manageDataCenter')
865    label = _(u'Processed Files')
866    pnav = 0
867    cancel_button =_('Back to Data Center')
868
869    def update(self, CANCEL=None):
870        if CANCEL is not None:
871            self.redirect(self.url(self.context))
872            return
873        return super(DatacenterFinishedPage, self).update()
874
875class DatacenterUploadPage(IkobaPage):
876    grok.context(IDataCenter)
877    grok.name('upload')
878    grok.require('waeup.manageDataCenter')
879    label = _(u'Upload portal data as CSV file')
880    pnav = 0
881    max_files = 20
882    upload_button =_(u'Upload')
883    cancel_button =_(u'Back to Data Center')
884
885    def getPreviewHeader(self):
886        """Get the header fields of uploaded CSV file.
887        """
888        reader = csv.reader(open(self.fullpath, 'rb'))
889        return reader.next()
890
891    def _notifyImportManagers(self, filename,
892        normalized_filename, importer, import_mode):
893        """Send email to Import Managers
894        """
895        # Get information about file
896        self.fullpath = os.path.join(self.context.storage, normalized_filename)
897        uploadfile = DataCenterFile(self.fullpath)
898        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
899        table = getPreviewTable(self, 3)
900        mail_table = ''
901        for line in table:
902            header = line[0]
903            data = str(line[1:]).strip('[').strip(']')
904            mail_table += '%s: %s ...\n' % (line[0], data)
905        # Collect all recipient addresses
906        ikoba_utils = getUtility(IIkobaUtils)
907        import_managers = get_users_with_role(
908            'waeup.ImportManager', grok.getSite())
909        rcpt_addrs = ','.join(
910            [user['user_email'] for user in import_managers if
911                user['user_email'] is not None])
912        if rcpt_addrs:
913            config = grok.getSite()['configuration']
914            fullname = self.request.principal.title
915            try:
916                email = self.request.principal.email
917            except AttributeError:
918                email = config.email_admin
919            username = self.request.principal.id
920            usertype = getattr(self.request.principal,
921                               'user_type', 'system').title()
922            rcpt_name = _('Import Manager')
923            subject = translate(
924                      _('${a}: ${b} uploaded',
925                      mapping = {'a':config.acronym, 'b':filename}),
926                      'waeup.ikoba',
927                      target_language=ikoba_utils.PORTAL_LANGUAGE)
928            text = _("""File: ${a}
929Importer: ${b}
930Import Mode: ${c}
931Datasets: ${d}
932
933${e}
934
935Comment by Import Manager:""", mapping = {'a':normalized_filename,
936                'b':importer,
937                'c':import_mode,
938                'd':uploadfile.lines - 1,
939                'e':mail_table})
940            success = ikoba_utils.sendContactForm(
941                    fullname,email,
942                    rcpt_name,rcpt_addrs,
943                    username,usertype,config.name,
944                    text,subject)
945            if success:
946                self.flash(
947                    _('All import managers have been notified by email.'))
948            else:
949                self.flash(_('An smtp server error occurred.'), type='danger')
950            return
951
952    def update(self, uploadfile=None, import_mode=None,
953               importer=None, CANCEL=None, SUBMIT=None):
954        number_of_pendings = len(self.context.getPendingFiles())
955        if number_of_pendings > self.max_files:
956            self.flash(
957                _('Maximum number of files in the data center exceeded.'),
958                  type='danger')
959            self.redirect(self.url(self.context))
960            return
961        if CANCEL is not None:
962            self.redirect(self.url(self.context))
963            return
964        if not uploadfile:
965            return
966        try:
967            filename = uploadfile.filename
968            #if 'pending' in filename:
969            #    self.flash(_("You can't re-upload pending data files."), type='danger')
970            #    return
971            if not filename.endswith('.csv'):
972                self.flash(_("Only csv files are allowed."), type='danger')
973                return
974            normalized_filename = self.getNormalizedFileName(filename)
975            finished_file = os.path.join(
976                self.context.storage, 'finished', normalized_filename)
977            unfinished_file = os.path.join(
978                self.context.storage, 'unfinished', normalized_filename)
979            if os.path.exists(finished_file) or os.path.exists(unfinished_file):
980                self.flash(_("File with same name was uploaded earlier."),
981                           type='danger')
982                return
983            target = os.path.join(self.context.storage, normalized_filename)
984            filecontent = uploadfile.read()
985            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
986            logger = self.context.logger
987
988            # Forbid certain characters in import files.
989            invalid_line = check_csv_charset(filecontent.splitlines())
990            if invalid_line:
991                self.flash(_(
992                    "Your file contains forbidden characters or "
993                    "has invalid CSV format. "
994                    "First problematic line detected: line %s. "
995                    "Please replace." % invalid_line), type='danger')
996                logger.info('%s - invalid file uploaded: %s' %
997                            (ob_class, target))
998                return
999
1000            open(target, 'wb').write(filecontent)
1001            os.chmod(target, 0664)
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.', type='danger')
1008            self.flash('I/O error: %s' % sys.exc_info()[1], type='danger')
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.ikoba.','')
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.ikoba.','')
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(IkobaPage):
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'Back to Data Center')
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.redirect(self.url(self.context))
1118            return
1119        if select is not None:
1120            # A filename was selected
1121            session = ISession(self.request)['waeup.ikoba']
1122            session['import_filename'] = select
1123            self.redirect(self.url(self.context, '@@import2'))
1124
1125class DatacenterImportStep2(IkobaPage):
1126    """Manual import step 2: choose processor
1127    """
1128    grok.context(IDataCenter)
1129    grok.name('import2')
1130    grok.template('datacenterimport2page')
1131    grok.require('waeup.manageDataCenter')
1132    label = _(u'Process CSV file')
1133    pnav = 0
1134    cancel_button =_(u'Cancel')
1135    back_button =_(u'Back to step 1')
1136    proceed_button =_(u'Proceed to step 3')
1137
1138    filename = None
1139    mode = 'create'
1140    importer = None
1141    mode_locked = False
1142
1143    def getPreviewHeader(self):
1144        """Get the header fields of attached CSV file.
1145        """
1146        reader = csv.reader(open(self.fullpath, 'rb'))
1147        return reader.next()
1148
1149    def getPreviewTable(self):
1150        return getPreviewTable(self, 3)
1151
1152    def getImporters(self):
1153        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
1154        importers = sorted(
1155            [dict(title=x.name, name=x.util_name) for x in importers])
1156        return importers
1157
1158    def getModeFromFilename(self, filename):
1159        """Lookup filename or path and return included mode name or None.
1160        """
1161        if not filename.endswith('.pending.csv'):
1162            return None
1163        base = os.path.basename(filename)
1164        parts = base.rsplit('.', 3)
1165        if len(parts) != 4:
1166            return None
1167        if parts[1] not in ['create', 'update', 'remove']:
1168            return None
1169        return parts[1]
1170
1171    def getWarnings(self):
1172        import sys
1173        result = []
1174        try:
1175            headerfields = self.getPreviewHeader()
1176            headerfields_clean = list(set(headerfields))
1177            if len(headerfields) > len(headerfields_clean):
1178                result.append(
1179                    _("Double headers: each column name may only appear once. "))
1180        except:
1181            fatal = '%s' % sys.exc_info()[1]
1182            result.append(fatal)
1183        if result:
1184            warnings = ""
1185            for line in result:
1186                warnings += line + '<br />'
1187            warnings += _('Replace imported file!')
1188            return warnings
1189        return False
1190
1191    def update(self, mode=None, importer=None,
1192               back1=None, cancel=None, proceed=None):
1193        session = ISession(self.request)['waeup.ikoba']
1194        self.filename = session.get('import_filename', None)
1195
1196        if self.filename is None or back1 is not None:
1197            self.redirect(self.url(self.context, '@@import1'))
1198            return
1199        if cancel is not None:
1200            self.flash(_('Import aborted.'), type='warning')
1201            self.redirect(self.url(self.context))
1202            return
1203        self.mode = mode or session.get('import_mode', self.mode)
1204        filename_mode = self.getModeFromFilename(self.filename)
1205        if filename_mode is not None:
1206            self.mode = filename_mode
1207            self.mode_locked = True
1208        self.importer = importer or session.get('import_importer', None)
1209        session['import_importer'] = self.importer
1210        if self.importer and 'update' in self.importer:
1211            if self.mode != 'update':
1212                self.flash(_('Update mode only!'), type='warning')
1213                self.mode_locked = True
1214                self.mode = 'update'
1215                proceed = None
1216        session['import_mode'] = self.mode
1217        if proceed is not None:
1218            self.redirect(self.url(self.context, '@@import3'))
1219            return
1220        self.fullpath = os.path.join(self.context.storage, self.filename)
1221        warnings = self.getWarnings()
1222        if not warnings:
1223            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1224        else:
1225            self.reader = ()
1226            self.flash(warnings, type='warning')
1227
1228class DatacenterImportStep3(IkobaPage):
1229    """Manual import step 3: modify header
1230    """
1231    grok.context(IDataCenter)
1232    grok.name('import3')
1233    grok.template('datacenterimport3page')
1234    grok.require('waeup.manageDataCenter')
1235    label = _(u'Process CSV file')
1236    pnav = 0
1237    cancel_button =_(u'Cancel')
1238    reset_button =_(u'Reset')
1239    update_button =_(u'Set headerfields')
1240    back_button =_(u'Back to step 2')
1241    proceed_button =_(u'Perform import')
1242
1243    filename = None
1244    mode = None
1245    importername = None
1246
1247    @property
1248    def nextstep(self):
1249        return self.url(self.context, '@@import4')
1250
1251    def getPreviewHeader(self):
1252        """Get the header fields of attached CSV file.
1253        """
1254        reader = csv.reader(open(self.fullpath, 'rb'))
1255        return reader.next()
1256
1257    def getPreviewTable(self):
1258        """Get transposed table with 1 sample record.
1259
1260        The first column contains the headers.
1261        """
1262        if not self.reader:
1263            return
1264        headers = self.getPreviewHeader()
1265        num = 0
1266        data = []
1267        for line in self.reader:
1268            if num > 0:
1269                break
1270            num += 1
1271            data.append(line)
1272        result = []
1273        field_num = 0
1274        for name in headers:
1275            result_line = []
1276            result_line.append(field_num)
1277            field_num += 1
1278            for d in data:
1279                result_line.append(d[name])
1280            result.append(result_line)
1281        return result
1282
1283    def getPossibleHeaders(self):
1284        """Get the possible headers.
1285
1286        The headers are described as dicts {value:internal_name,
1287        title:displayed_name}
1288        """
1289        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
1290        headers = self.importer.getHeaders()
1291        result.extend([dict(title=x, value=x) for x in headers])
1292        return result
1293
1294    def getWarnings(self):
1295        import sys
1296        result = []
1297        try:
1298            self.importer.checkHeaders(self.headerfields, mode=self.mode)
1299        except:
1300            fatal = '%s' % sys.exc_info()[1]
1301            result.append(fatal)
1302        if result:
1303            warnings = ""
1304            for line in result:
1305                warnings += line + '<br />'
1306            warnings += _('Edit headers or replace imported file!')
1307            return warnings
1308        return False
1309
1310    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
1311        session = ISession(self.request)['waeup.ikoba']
1312        self.filename = session.get('import_filename', None)
1313        self.mode = session.get('import_mode', None)
1314        self.importername = session.get('import_importer', None)
1315
1316        if None in (self.filename, self.mode, self.importername):
1317            self.redirect(self.url(self.context, '@@import2'))
1318            return
1319        if back2 is not None:
1320            self.redirect(self.url(self.context ,'@@import2'))
1321            return
1322        if cancel is not None:
1323            self.flash(_('Import aborted.'), type='warning')
1324            self.redirect(self.url(self.context))
1325            return
1326
1327        self.fullpath = os.path.join(self.context.storage, self.filename)
1328        self.headerfields = headerfield or self.getPreviewHeader()
1329        session['import_headerfields'] = self.headerfields
1330
1331        if proceed is not None:
1332            self.redirect(self.url(self.context, '@@import4'))
1333            return
1334        self.importer = getUtility(IBatchProcessor, name=self.importername)
1335        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1336        warnings = self.getWarnings()
1337        if warnings:
1338            self.flash(warnings, type='warning')
1339
1340class DatacenterImportStep4(IkobaPage):
1341    """Manual import step 4: do actual import
1342    """
1343    grok.context(IDataCenter)
1344    grok.name('import4')
1345    grok.template('datacenterimport4page')
1346    grok.require('waeup.importData')
1347    label = _(u'Process CSV file')
1348    pnav = 0
1349    back_button =_(u'Process next')
1350
1351    filename = None
1352    mode = None
1353    importername = None
1354    headerfields = None
1355    warnnum = None
1356
1357    def update(self, back=None, finish=None, showlog=None):
1358        if finish is not None:
1359            self.redirect(self.url(self.context, '@@import1'))
1360            return
1361        session = ISession(self.request)['waeup.ikoba']
1362        self.filename = session.get('import_filename', None)
1363        self.mode = session.get('import_mode', None)
1364        self.importername = session.get('import_importer', None)
1365        # If the import file contains only one column
1366        # the import_headerfields attribute is a string.
1367        ihf = session.get('import_headerfields', None)
1368        if not isinstance(ihf, list):
1369            self.headerfields = ihf.split()
1370        else:
1371            self.headerfields = ihf
1372
1373        if None in (self.filename, self.mode, self.importername,
1374                    self.headerfields):
1375            self.redirect(self.url(self.context, '@@import3'))
1376            return
1377
1378        if showlog is not None:
1379            logfilename = "datacenter.log"
1380            session['logname'] = logfilename
1381            self.redirect(self.url(self.context, '@@show'))
1382            return
1383
1384        self.fullpath = os.path.join(self.context.storage, self.filename)
1385        self.importer = getUtility(IBatchProcessor, name=self.importername)
1386
1387        # Perform batch processing...
1388        # XXX: This might be better placed in datacenter module.
1389        (linenum, self.warn_num,
1390         fin_path, pending_path) = self.importer.doImport(
1391            self.fullpath, self.headerfields, self.mode,
1392            self.request.principal.id, logger=self.context.logger)
1393        # Put result files in desired locations...
1394        self.context.distProcessedFiles(
1395            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1396            self.mode)
1397
1398        if self.warn_num:
1399            self.flash(_('Processing of ${a} rows failed.',
1400                mapping = {'a':self.warn_num}), type='warning')
1401        self.flash(_('Successfully processed ${a} rows.',
1402            mapping = {'a':linenum - self.warn_num}))
1403
1404class DatacenterLogsOverview(IkobaPage):
1405    grok.context(IDataCenter)
1406    grok.name('logs')
1407    grok.template('datacenterlogspage')
1408    grok.require('waeup.manageDataCenter')
1409    label = _(u'Show logfiles')
1410    pnav = 0
1411    back_button = _(u'Back to Data Center')
1412    show_button = _(u'Show')
1413
1414    def update(self, back=None):
1415        if back is not None:
1416            self.redirect(self.url(self.context))
1417            return
1418        self.files = self.context.getLogFiles()
1419
1420class DatacenterLogsFileview(IkobaPage):
1421    grok.context(IDataCenter)
1422    grok.name('show')
1423    grok.template('datacenterlogsshowfilepage')
1424    grok.require('waeup.manageDataCenter')
1425    title = _(u'Data Center')
1426    pnav = 0
1427    search_button = _('Search')
1428    back_button = _('Back to Data Center')
1429    placeholder = _('Enter a regular expression here...')
1430
1431    def label(self):
1432        return "Logfile %s" % self.filename
1433
1434    def update(self, back=None, query=None, logname=None):
1435        if os.name != 'posix':
1436            self.flash(
1437                _('Log files can only be searched ' +
1438                  'on Unix-based operating systems.'), type='danger')
1439            self.redirect(self.url(self.context, '@@logs'))
1440            return
1441        if back is not None or logname is None:
1442            self.redirect(self.url(self.context, '@@logs'))
1443            return
1444        self.filename = logname
1445        self.query = query
1446        if not query:
1447            return
1448        try:
1449            self.result = ''.join(
1450                self.context.queryLogfiles(logname, query))
1451        except ValueError:
1452            self.flash(_('Invalid search expression.'), type='danger')
1453            return
1454        if not self.result:
1455            self.flash(_('No search results found.'), type='warning')
1456        return
1457
1458class DatacenterSettings(IkobaPage):
1459    grok.context(IDataCenter)
1460    grok.name('manage')
1461    grok.template('datacentermanagepage')
1462    grok.require('waeup.managePortal')
1463    label = _('Edit data center settings')
1464    pnav = 0
1465    save_button =_(u'Save')
1466    reset_button =_(u'Reset')
1467    cancel_button =_(u'Back to Data Center')
1468
1469    def update(self, newpath=None, move=False, overwrite=False,
1470               save=None, cancel=None):
1471        if move:
1472            move = True
1473        if overwrite:
1474            overwrite = True
1475        if newpath is None:
1476            return
1477        if cancel is not None:
1478            self.redirect(self.url(self.context))
1479            return
1480        try:
1481            not_copied = self.context.setStoragePath(newpath, move=move)
1482            for name in not_copied:
1483                self.flash(_('File already existed (not copied): ${a}',
1484                    mapping = {'a':name}), type='danger')
1485        except:
1486            self.flash(_('Given storage path cannot be used. ${a}',
1487                        mapping = {'a':sys.exc_info()[1]}), type='danger')
1488            return
1489        if newpath:
1490            self.flash(_('New storage path succefully set.'))
1491            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1492            self.context.logger.info(
1493                '%s - storage path set: %s' % (ob_class, newpath))
1494            self.redirect(self.url(self.context))
1495        return
1496
1497class ExportCSVPage(IkobaPage):
1498    grok.context(IDataCenter)
1499    grok.name('export')
1500    grok.template('datacenterexportpage')
1501    grok.require('waeup.exportData')
1502    label = _('Download portal data as CSV file')
1503    pnav = 0
1504    export_button = _(u'Create CSV file')
1505    cancel_button =_(u'Back to Data Center')
1506
1507    def getExporters(self):
1508        utils = getUtilitiesFor(ICSVExporter)
1509        title_name_tuples = [
1510            (util.title, name) for name, util in utils]
1511        return sorted(title_name_tuples)
1512
1513    def update(self, CREATE=None, DISCARD=None, exporter=None,
1514               job_id=None, CANCEL=None):
1515        if CANCEL is not None:
1516            self.redirect(self.url(self.context))
1517            return
1518        if CREATE:
1519            job_id = self.context.start_export_job(
1520                exporter, self.request.principal.id)
1521            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1522            self.context.logger.info(
1523                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
1524        if DISCARD and job_id:
1525            entry = self.context.entry_from_job_id(job_id)
1526            self.context.delete_export_entry(entry)
1527            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1528            self.context.logger.info(
1529                '%s - discarded: job_id=%s' % (ob_class, job_id))
1530            self.flash(_('Discarded export') + ' %s' % job_id)
1531        self.entries = doll_up(self, user=None)
1532        return
1533
1534class ExportCSVView(grok.View):
1535    grok.context(IDataCenter)
1536    grok.name('download_export')
1537    grok.require('waeup.exportData')
1538
1539    def render(self, job_id=None):
1540        manager = getUtility(IJobManager)
1541        job = manager.get(job_id)
1542        if job is None:
1543            return
1544        if hasattr(job.result, 'traceback'):
1545            # XXX: Some error happened. Do something more approriate here...
1546            return
1547        path = job.result
1548        if not os.path.exists(path):
1549            # XXX: Do something more appropriate here...
1550            return
1551        result = open(path, 'rb').read()
1552        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
1553        filename = "%s_%s" % (acronym, os.path.basename(path))
1554        filename = filename.replace('.csv', '_%s.csv' % job_id)
1555        self.response.setHeader(
1556            'Content-Type', 'text/csv; charset=UTF-8')
1557        self.response.setHeader(
1558            'Content-Disposition', 'attachment; filename="%s' % filename)
1559        # remove job and running_exports entry from context
1560        #self.context.delete_export_entry(
1561        #    self.context.entry_from_job_id(job_id))
1562        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1563        self.context.logger.info(
1564            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
1565        return result
1566
1567class ChangePasswordRequestPage(IkobaForm):
1568    """Captcha'd page for all kind of users to request a password change.
1569    """
1570    grok.context(IInstitution)
1571    grok.name('changepw')
1572    grok.require('waeup.Anonymous')
1573    grok.template('changepw')
1574    label = _('Send me a new password')
1575    form_fields = grok.AutoFields(IChangePassword)
1576
1577    def update(self):
1578        # Handle captcha
1579        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1580        self.captcha_result = self.captcha.verify(self.request)
1581        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1582        return
1583
1584    def _searchUser(self, identifier, email):
1585        # Search customer
1586        #cat = queryUtility(ICatalog, name='customers_catalog')
1587        #results = cat.searchResults(
1588            #reg_number=(identifier, identifier),
1589        #    email=(email,email))
1590        #for result in results:
1591        #    if result.customer_id == identifier or \
1592        #        result.reg_number == identifier or \
1593        #        result.matric_number == identifier:
1594        #        return result
1595        # Search portal user
1596        user = grok.getSite()['users'].get(identifier, None)
1597        if user is not None and user.email == email:
1598            return user
1599        return None
1600
1601    @action(_('Send login credentials to email address'), style='primary')
1602    def request(self, **data):
1603        if not self.captcha_result.is_valid:
1604            # Captcha will display error messages automatically.
1605            # No need to flash something.
1606            return
1607        # Search customer
1608        identifier = data['identifier']
1609        email = data['email']
1610        user = self._searchUser(identifier, email)
1611        if user is None:
1612            self.flash(_('No record found.'), type='warning')
1613            return
1614        # Change password
1615        ikoba_utils = getUtility(IIkobaUtils)
1616        password = ikoba_utils.genPassword()
1617        mandate = PasswordMandate()
1618        mandate.params['password'] = password
1619        mandate.params['user'] = user
1620        site = grok.getSite()
1621        site['mandates'].addMandate(mandate)
1622        # Send email with credentials
1623        args = {'mandate_id':mandate.mandate_id}
1624        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1625        url_info = u'Confirmation link: %s' % mandate_url
1626        msg = _('You have successfully requested a password for the')
1627        success = ikoba_utils.sendCredentials(
1628            IUserAccount(user),password,url_info,msg)
1629        if success:
1630            self.flash(_('An email with your user name and password ' +
1631                'has been sent to ${a}.', mapping = {'a':email}))
1632        else:
1633            self.flash(_('An smtp server error occurred.'), type='danger')
1634        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1635        self.context.logger.info(
1636            '%s - %s - %s' % (ob_class, data['identifier'], data['email']))
1637        return
Note: See TracBrowser for help on using the repository browser.