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

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

Do only allow one running export job.
Add switch to disable all exports.

See r13198 - r13201 and r13211.

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