source: main/waeup.ikoba/trunk/src/waeup/ikoba/browser/pages.py @ 13803

Last change on this file since 13803 was 13803, checked in by Henrik Bettermann, 9 years ago

Enable temporary suspension of officer accounts. Plugins must be
updated after restart.

See r12926.

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