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
RevLine 
[7195]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##
[11949]18""" Viewing components for Ikoba objects.
[4584]19"""
[10027]20# XXX: All csv ops should move to a dedicated module soon
21import unicodecsv as csv
[4584]22import grok
[4679]23import os
[4858]24import re
[4679]25import sys
[9334]26from datetime import datetime, timedelta
[8858]27from urllib import urlencode
[10646]28from hurry.query import Eq, Text
29from hurry.query.query import Query
[5047]30from zope import schema
[9311]31from zope.i18n import translate
[6154]32from zope.authentication.interfaces import (
33    IAuthentication, IUnauthenticatedPrincipal, ILogout)
[6228]34from zope.catalog.interfaces import ICatalog
[5047]35from zope.component import (
[7908]36    getUtility, queryUtility, createObject, getAllUtilitiesRegisteredFor,
37    getUtilitiesFor,
38    )
[6234]39from zope.event import notify
[9261]40from zope.security import checkPermission
[9217]41from zope.securitypolicy.interfaces import IPrincipalRoleManager
[5047]42from zope.session.interfaces import ISession
[9123]43from zope.password.interfaces import IPasswordManager
[12229]44from waeup.ikoba.utils.helpers import html2dict
[11949]45from waeup.ikoba.browser.layout import (
[13066]46    IkobaPage, IkobaFormPage, IkobaEditFormPage, IkobaAddFormPage,
[11949]47    IkobaDisplayFormPage, NullValidator)
48from waeup.ikoba.browser.interfaces import (
[11954]49    ICompany, ICaptchaManager, IChangePassword)
[11949]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,
[12995]54    IBatchProcessor,
[6916]55    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
[12186]56    IJobManager,
57    IPasswordValidator, IContactForm, IIkobaUtils, ICSVExporter)
[11949]58from waeup.ikoba.permissions import (
[9311]59    get_users_with_local_roles, get_all_roles, get_all_users,
60    get_users_with_role)
[9217]61
[11949]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
[4584]66
[10029]67FORBIDDEN_CHARACTERS = (160,)
68
[11949]69grok.context(IIkobaObject)
[4584]70grok.templatedir('templates')
71
[6154]72def add_local_role(view, tab, **data):
[7104]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:
[11254]76        view.flash('No user selected.', type='danger')
77        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
[6589]78        return
[6152]79    role_manager = IPrincipalRoleManager(view.context)
80    role_manager.assignRoleToPrincipal(localrole, user)
[6181]81    notify(LocalRoleSetEvent(view.context, localrole, user, granted=True))
[11949]82    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
[8740]83    grok.getSite().logger.info(
84        '%s - added: %s|%s' % (ob_class, user, localrole))
[11254]85    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
[6152]86    return
87
[6161]88def del_local_roles(view, tab, **data):
[7104]89    child_ids = view.request.form.get('role_id', None)
90    if child_ids is None:
[11254]91        view.flash(_('No local role selected.'), type='danger')
92        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
[6161]93        return
[7104]94    if not isinstance(child_ids, list):
95        child_ids = [child_ids]
[6161]96    deleted = []
97    role_manager = IPrincipalRoleManager(view.context)
[7104]98    for child_id in child_ids:
99        localrole = child_id.split('|')[1]
100        user_name = child_id.split('|')[0]
[6161]101        try:
102            role_manager.unsetRoleForPrincipal(localrole, user_name)
[7104]103            notify(LocalRoleSetEvent(
104                    view.context, localrole, user_name, granted=False))
105            deleted.append(child_id)
[6161]106        except:
107            view.flash('Could not remove %s: %s: %s' % (
[11254]108                    child_id, sys.exc_info()[0], sys.exc_info()[1]),
109                    type='danger')
[6161]110    if len(deleted):
[7700]111        view.flash(
112            _('Local role successfully removed: ${a}',
113            mapping = {'a':', '.join(deleted)}))
[11949]114        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]115        grok.getSite().logger.info(
116            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
[11254]117    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
[6161]118    return
119
[6917]120def delSubobjects(view, redirect, tab=None, subcontainer=None):
121    form = view.request.form
[9701]122    if 'val_id' in form:
[6917]123        child_id = form['val_id']
124    else:
[11254]125        view.flash(_('No item selected.'), type='danger')
[6917]126        if tab:
[11254]127            view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
[6917]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' % (
[11254]144                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
[6917]145    if len(deleted):
[7700]146        view.flash(_('Successfully removed: ${a}',
147            mapping = {'a': ', '.join(deleted)}))
[11949]148        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]149        grok.getSite().logger.info(
150            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
[6917]151    if tab:
[11254]152        view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
[6917]153    else:
154        view.redirect(view.url(view.context, redirect))
155    return
156
[9322]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
[9011]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.'))
[11949]189    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
[9011]190    if fields_string:
[9087]191        grok.getSite().logger.info('%s - %s - saved: %s' % (ob_class, view.context.__name__, fields_string))
[9011]192    return
193
[9822]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')
[9845]204        args = ', '.join(['%s=%s' % (item[0], item[1])
205            for item in job.kwargs.items()])
[9822]206        status = job.finished and 'ready' or 'running'
207        status = job.failed and 'FAILED' or status
208        start_time = getattr(job, 'begin_after', None)
[11826]209        time_delta = None
[9822]210        if start_time:
[11949]211            tz = getUtility(IIkobaUtils).tzinfo
[11826]212            time_delta = datetime.now(tz) - start_time
213            start_time = start_time.astimezone(tz).strftime(
214                "%Y-%m-%d %H:%M:%S %Z")
[9822]215        download_url = view.url(view.context, 'download_export',
216                                data=dict(job_id=job_id))
[11826]217        show_download_button = job.finished and not \
218                               job.failed and time_delta and \
219                               time_delta.days < 1
[9822]220        new_entry = dict(
221            job=job_id,
222            creator=user_id,
[9839]223            args=args,
224            exporter=exporter_title,
[9822]225            status=status,
226            start_time=start_time,
227            download_url=download_url,
[11826]228            show_download_button = show_download_button,
[9822]229            show_refresh_button = not job.finished,
230            show_discard_button = job.finished,)
231        entries.append(new_entry)
232    return entries
233
[13802]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
[10541]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
[4609]263#
[7674]264# Login/logout and language switch pages...
[4609]265#
266
[11949]267class LoginPage(IkobaPage):
[4594]268    """A login page, available for all objects.
269    """
270    grok.name('login')
[11949]271    grok.context(IIkobaObject)
[5498]272    grok.require('waeup.Public')
[7700]273    label = _(u'Login')
[4594]274    camefrom = None
[7707]275    login_button = label
[4594]276
[11977]277    def _comment(self, customer):
278        return getattr(customer, 'suspended_comment', None)
279
[4594]280    def update(self, SUBMIT=None, camefrom=None):
[4608]281        self.camefrom = camefrom
[4594]282        if SUBMIT is not None:
[4608]283            if self.request.principal.id != 'zope.anybody':
[7700]284                self.flash(_('You logged in.'))
[11947]285                if self.request.principal.user_type == 'customer':
[11952]286                    customer = grok.getSite()['customers'][
[9545]287                        self.request.principal.id]
[11947]288                    rel_link = '/customers/%s' % self.request.principal.id
[11979]289                    # Maybe we need this in Ikoba too
[11975]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')
[12816]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)
[6686]303                    return
[4608]304                if not self.camefrom:
[11439]305                    self.redirect(self.application_url() + '/index')
[4608]306                    return
307                self.redirect(self.camefrom)
[4610]308                return
[11975]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
[11254]338            self.flash(_('You entered invalid credentials.'), type='danger')
[9334]339            return
[4594]340
[4600]341
[11949]342class LogoutPage(IkobaPage):
[4610]343    """A logout page. Calling this page will log the current user out.
344    """
[11949]345    grok.context(IIkobaObject)
[5498]346    grok.require('waeup.Public')
[4610]347    grok.name('logout')
[6146]348
[4610]349    def update(self):
350        if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
351            auth = getUtility(IAuthentication)
352            ILogout(auth).logout(self.request)
[11949]353            self.flash(_("You have been logged out. Thanks for using WAeUP Ikoba!"))
[11439]354        self.redirect(self.application_url() + '/index')
355        return
[4604]356
[7674]357
[11949]358class LanguageChangePage(IkobaPage):
[7674]359    """ Language switch
360    """
[11949]361    grok.context(IIkobaObject)
[7674]362    grok.name('change_language')
363    grok.require('waeup.Public')
364
365    def update(self, lang='en', view_name='@@index'):
[11949]366        self.response.setCookie('ikoba.language', lang, path='/')
[7674]367        self.redirect(self.url(self.context, view_name))
368        return
369
370    def render(self):
371        return
372
[4609]373#
[7231]374# Contact form...
375#
376
[13066]377class ContactAdminForm(IkobaFormPage):
[7231]378    grok.name('contactadmin')
[11954]379    #grok.context(ICompany)
[7231]380    grok.template('contactform')
381    grok.require('waeup.Authenticated')
382    pnav = 2
383    form_fields = grok.AutoFields(IContactForm).select('body')
384
[9857]385    def update(self):
386        super(ContactAdminForm, self).update()
387        self.form_fields.get('body').field.default = None
388        return
389
[7231]390    @property
391    def config(self):
392        return grok.getSite()['configuration']
393
[7465]394    def label(self):
[7700]395        return _(u'Contact ${a}', mapping = {'a': self.config.name_admin})
[7231]396
397    @property
398    def get_user_account(self):
399        return get_user_account(self.request)
400
[7700]401    @action(_('Send message now'), style='primary')
[7231]402    def send(self, *args, **data):
[7234]403        fullname = self.request.principal.title
404        try:
[7402]405            email = self.request.principal.email
[7234]406        except AttributeError:
[7402]407            email = self.config.email_admin
[7234]408        username = self.request.principal.id
[7240]409        usertype = getattr(self.request.principal,
410                           'user_type', 'system').title()
[11949]411        ikoba_utils = getUtility(IIkobaUtils)
412        success = ikoba_utils.sendContactForm(
[7402]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)
[7490]417        # Success is always True if sendContactForm didn't fail.
418        # TODO: Catch exceptions.
[7231]419        if success:
[7700]420            self.flash(_('Your message has been sent.'))
[7231]421        return
422
423class EnquiriesForm(ContactAdminForm):
[8415]424    """Captcha'd page to let anonymous send emails to the administrator.
425    """
[7231]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
[8415]432    def update(self):
[9857]433        super(EnquiriesForm, self).update()
[8415]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)
[9857]438        return
[8415]439
[7700]440    @action(_('Send now'), style='primary')
[7231]441    def send(self, *args, **data):
[8415]442        if not self.captcha_result.is_valid:
443            # Captcha will display error messages automatically.
444            # No need to flash something.
445            return
[11949]446        ikoba_utils = getUtility(IIkobaUtils)
447        success = ikoba_utils.sendContactForm(
[7402]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)
[7231]452        if success:
[7700]453            self.flash(_('Your message has been sent.'))
[7231]454        else:
[11254]455            self.flash(_('A smtp server error occurred.'), type='danger')
[7231]456        return
457
458#
[11954]459# Company related pages...
[4609]460#
[4604]461
[11954]462class CompanyPage(IkobaDisplayFormPage):
463    """ The main company page.
[4584]464    """
[5498]465    grok.require('waeup.Public')
[4584]466    grok.name('index')
[11954]467    grok.context(ICompany)
[4648]468    pnav = 0
[7703]469    label = ''
[6146]470
[6065]471    @property
[6907]472    def frontpage(self):
[11949]473        lang = self.request.cookies.get('ikoba.language')
[7703]474        html = self.context['configuration'].frontpage_dict.get(lang,'')
475        if html =='':
[11949]476            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
[7703]477            html = self.context[
478                'configuration'].frontpage_dict.get(portal_language,'')
479        if html =='':
[11949]480            return _(u'<h1>Welcome to WAeUP.Ikoba</h1>')
[7703]481        else:
482            return html
[4584]483
[11949]484class AdministrationPage(IkobaPage):
[4935]485    """ The administration overview page.
486    """
487    grok.name('administration')
[11954]488    grok.context(ICompany)
[8367]489    grok.require('waeup.managePortal')
[7700]490    label = _(u'Administration')
[4935]491    pnav = 0
492
[4988]493class RSS20Feed(grok.View):
494    """An RSS 2.0 feed.
495    """
496    grok.name('feed.rss')
[11954]497    grok.context(ICompany)
[4988]498    grok.require('waeup.Public')
[11954]499    grok.template('companyrss20feed')
[4988]500
501    name = 'General news feed'
[11949]502    description = 'waeup.ikoba now supports RSS 2.0 feeds :-)'
[4988]503    language = None
504    date = None
505    buildDate = None
506    editor = None
507    webmaster = None
508
509    @property
510    def title(self):
[11954]511        return getattr(grok.getSite(), 'name', u'Sample Company')
[4988]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 ()
[6146]526
[4627]527#
528# User container pages...
529#
[6146]530
[11949]531class UsersContainerPage(IkobaPage):
[4632]532    """Overview page for all local users.
533    """
[4627]534    grok.require('waeup.manageUsers')
[7172]535    grok.context(IUsersContainer)
[4627]536    grok.name('index')
[12525]537    label = _('Officers')
[7707]538    manage_button = _(u'Manage')
539    delete_button = _(u'Remove')
[6146]540
[7162]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')
[4627]544        if delete is not None and userid is not None:
545            self.context.delUser(userid)
[7700]546            self.flash(_('User account ${a} successfully deleted.',
547                mapping = {'a':  userid}))
[11949]548            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]549            self.context.__parent__.logger.info(
550                '%s - removed: %s' % (ob_class, userid))
[4632]551
[7176]552    def getLocalRoles(self, account):
[7177]553        local_roles = account.getLocalRoles()
554        local_roles_string = ''
555        site_url = self.url(grok.getSite())
556        for local_role in local_roles.keys():
[10227]557            role_title = getattr(
558                dict(get_all_roles()).get(local_role, None), 'title', None)
[7177]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
[7176]566
[7178]567    def getSiteRoles(self, account):
568        site_roles = account.roles
569        site_roles_string = ''
570        for site_role in site_roles:
[7186]571            role_title = dict(get_all_roles())[site_role].title
[9495]572            site_roles_string += '%s <br />' % role_title
[7178]573        return site_roles_string
[7176]574
[11949]575class AddUserFormPage(IkobaAddFormPage):
[8079]576    """Add a user account.
577    """
[4633]578    grok.require('waeup.manageUsers')
[7172]579    grok.context(IUsersContainer)
[4633]580    grok.name('add')
[7149]581    grok.template('usereditformpage')
[4633]582    form_fields = grok.AutoFields(IUserAccount)
[7700]583    label = _('Add user')
[4633]584
[7700]585    @action(_('Add user'), style='primary')
[4633]586    def addUser(self, **data):
587        name = data['name']
588        title = data['title']
[7221]589        email = data['email']
[7233]590        phone = data['phone']
[4633]591        description = data['description']
[7149]592        #password = data['password']
[4633]593        roles = data['roles']
[7149]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:
[11254]601                self.flash( ' '.join(errors), type='danger')
[7149]602                return
[4633]603        try:
[7221]604            self.context.addUser(name, password, title=title, email=email,
[7233]605                                 phone=phone, description=description,
606                                 roles=roles)
[7700]607            self.flash(_('User account ${a} successfully added.',
608                mapping = {'a': name}))
[11949]609            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]610            self.context.__parent__.logger.info(
611                '%s - added: %s' % (ob_class, name))
[5047]612        except KeyError:
[6246]613            self.status = self.flash('The userid chosen already exists '
[11254]614                                  'in the database.', type='danger')
[5047]615            return
[4633]616        self.redirect(self.url(self.context))
617
[11949]618class UserManageFormPage(IkobaEditFormPage):
[7162]619    """Manage a user account.
[4632]620    """
621    grok.context(IUserAccount)
[7162]622    grok.name('manage')
623    grok.template('usereditformpage')
[7164]624    grok.require('waeup.manageUsers')
[7162]625    form_fields = grok.AutoFields(IUserAccount).omit('name')
[4632]626
[7162]627    def label(self):
[7700]628        return _("Edit user ${a}", mapping = {'a':self.context.__name__})
[7162]629
[7197]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
[8415]634        return
[7197]635
[7700]636    @action(_('Save'), style='primary')
[4632]637    def save(self, **data):
[7149]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:
[11254]645                self.flash( ' '.join(errors), type='danger')
[7149]646                return
[7659]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 = []
[7149]652        if password:
653            # Now we know that the form has no errors and can set password ...
654            self.context.setPassword(password)
[7659]655            changed_fields.append('password')
656        fields_string = ' + '.join(changed_fields)
657        if fields_string:
[11949]658            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[7659]659            self.context.__parent__.logger.info(
[8739]660                '%s - %s edited: %s' % (
661                ob_class, self.context.name, fields_string))
[7700]662        self.flash(_('User settings have been saved.'))
[4632]663        return
[6146]664
[7700]665    @action(_('Cancel'), validator=NullValidator)
[4632]666    def cancel(self, **data):
667        self.redirect(self.url(self.context.__parent__))
668        return
[4639]669
[7231]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
[7232]678    def label(self):
[7700]679        return _(u'Send message to ${a}', mapping = {'a':self.context.title})
[7232]680
[7700]681    @action(_('Send message now'), style='primary')
[7231]682    def send(self, *args, **data):
[7234]683        try:
[7402]684            email = self.request.principal.email
[7234]685        except AttributeError:
[7402]686            email = self.config.email_admin
[7240]687        usertype = getattr(self.request.principal,
688                           'user_type', 'system').title()
[11949]689        ikoba_utils = getUtility(IIkobaUtils)
690        success = ikoba_utils.sendContactForm(
[7402]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)
[7490]695        # Success is always True if sendContactForm didn't fail.
696        # TODO: Catch exceptions.
[7231]697        if success:
[7700]698            self.flash(_('Your message has been sent.'))
[7231]699        return
700
[7165]701class UserEditFormPage(UserManageFormPage):
702    """Edit a user account by user
[7164]703    """
704    grok.name('index')
705    grok.require('waeup.editUser')
[7197]706    form_fields = grok.AutoFields(IUserAccount).omit(
707        'name', 'description', 'roles')
[7700]708    label = _(u"My Preferences")
[7164]709
[7197]710    def setUpWidgets(self, ignore_request=False):
711        super(UserManageFormPage,self).setUpWidgets(ignore_request)
712        self.widgets['title'].displayWidth = 30
713
[11949]714class MyRolesPage(IkobaPage):
[7179]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')
[7700]721    label = _(u"My Roles")
[7179]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:
[7186]728            role_title = dict(get_all_roles())[local_role].title
[7179]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:
[7186]737            role_title = dict(get_all_roles())[site_role].title
[7179]738            site_roles_userfriendly.append(role_title)
739        return site_roles_userfriendly
740
[4661]741#
[6907]742# Configuration pages...
743#
744
[11949]745class ConfigurationContainerManageFormPage(IkobaEditFormPage):
[6916]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.
[6907]748    """
749    grok.require('waeup.managePortalConfiguration')
750    grok.name('index')
751    grok.context(IConfigurationContainer)
752    pnav = 0
[7700]753    label = _(u'Edit portal configuration')
[11254]754    form_fields = grok.AutoFields(IConfigurationContainer).omit(
755        'frontpage_dict')
[6907]756
[7707]757    @action(_('Save'), style='primary')
[6907]758    def save(self, **data):
[8739]759        msave(self, **data)
[12229]760        frontpage = getattr(self.context, 'frontpage', None)
761        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
762        self.context.frontpage_dict = html2dict(frontpage, portal_language)
[6907]763        return
764
[11438]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)
[6907]770    def updatePlugins(self, **data):
771        grok.getSite().updatePlugins()
[7700]772        self.flash(_('Plugins were updated. See log file for details.'))
[6907]773        return
774
775#
[4671]776# Datacenter pages...
777#
[4661]778
[11949]779class DatacenterPage(IkobaEditFormPage):
[4671]780    grok.context(IDataCenter)
781    grok.name('index')
[8366]782    grok.require('waeup.manageDataCenter')
[7700]783    label = _(u'Data Center')
[4671]784    pnav = 0
785
[8366]786    @jsaction(_('Remove selected'))
787    def delFiles(self, **data):
788        form = self.request.form
[9701]789        if 'val_id' in form:
[8366]790            child_id = form['val_id']
791        else:
[11254]792            self.flash(_('No item selected.'), type='danger')
[8366]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:
[11254]803                self.flash(_('OSError: The file could not be deleted.'),
804                           type='danger')
[8366]805                return
806        if len(deleted):
807            self.flash(_('Successfully deleted: ${a}',
808                mapping = {'a': ', '.join(deleted)}))
[11949]809            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]810            self.context.logger.info(
811                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
[8366]812        return
813
[11949]814class DatacenterFinishedPage(IkobaEditFormPage):
[9023]815    grok.context(IDataCenter)
816    grok.name('processed')
817    grok.require('waeup.manageDataCenter')
818    label = _(u'Processed Files')
819    pnav = 0
[11254]820    cancel_button =_('Back to Data Center')
[9023]821
[11254]822    def update(self, CANCEL=None):
823        if CANCEL is not None:
824            self.redirect(self.url(self.context))
825            return
[9023]826        return super(DatacenterFinishedPage, self).update()
827
[11949]828class DatacenterUploadPage(IkobaPage):
[4674]829    grok.context(IDataCenter)
830    grok.name('upload')
[8366]831    grok.require('waeup.manageDataCenter')
[9024]832    label = _(u'Upload portal data as CSV file')
[4674]833    pnav = 0
[9707]834    max_files = 20
[7705]835    upload_button =_(u'Upload')
[11254]836    cancel_button =_(u'Back to Data Center')
[6146]837
[9322]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
[9323]844    def _notifyImportManagers(self, filename,
845        normalized_filename, importer, import_mode):
[9311]846        """Send email to Import Managers
847        """
[9322]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(']')
[9323]857            mail_table += '%s: %s ...\n' % (line[0], data)
[9322]858        # Collect all recipient addresses
[11949]859        ikoba_utils = getUtility(IIkobaUtils)
[9311]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(
[9323]877                      _('${a}: ${b} uploaded',
878                      mapping = {'a':config.acronym, 'b':filename}),
[11949]879                      'waeup.ikoba',
880                      target_language=ikoba_utils.PORTAL_LANGUAGE)
[9322]881            text = _("""File: ${a}
[9323]882Importer: ${b}
[9322]883Import Mode: ${c}
884Datasets: ${d}
885
886${e}
887
[9334]888Comment by Import Manager:""", mapping = {'a':normalized_filename,
[9322]889                'b':importer,
890                'c':import_mode,
891                'd':uploadfile.lines - 1,
892                'e':mail_table})
[11949]893            success = ikoba_utils.sendContactForm(
[9311]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:
[11254]902                self.flash(_('An smtp server error occurred.'), type='danger')
[9311]903            return
904
[9322]905    def update(self, uploadfile=None, import_mode=None,
906               importer=None, CANCEL=None, SUBMIT=None):
[9610]907        number_of_pendings = len(self.context.getPendingFiles())
908        if number_of_pendings > self.max_files:
909            self.flash(
[11254]910                _('Maximum number of files in the data center exceeded.'),
911                  type='danger')
[9610]912            self.redirect(self.url(self.context))
913            return
[4679]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
[8510]921            #if 'pending' in filename:
[11254]922            #    self.flash(_("You can't re-upload pending data files."), type='danger')
[8510]923            #    return
[8366]924            if not filename.endswith('.csv'):
[11254]925                self.flash(_("Only csv files are allowed."), type='danger')
[8366]926                return
[9038]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):
[11254]933                self.flash(_("File with same name was uploaded earlier."),
934                           type='danger')
[9038]935                return
936            target = os.path.join(self.context.storage, normalized_filename)
[9930]937            filecontent = uploadfile.read()
[11949]938            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9931]939            logger = self.context.logger
[9930]940
[10029]941            # Forbid certain characters in import files.
[10676]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. "
[11254]948                    "Please replace." % invalid_line), type='danger')
[10676]949                logger.info('%s - invalid file uploaded: %s' %
950                            (ob_class, target))
951                return
[9930]952
953            open(target, 'wb').write(filecontent)
[8573]954            os.chmod(target, 0664)
[8739]955            logger.info('%s - uploaded: %s' % (ob_class, target))
[9323]956            self._notifyImportManagers(filename,
[9322]957                normalized_filename, importer, import_mode)
[9311]958
[4679]959        except IOError:
[11254]960            self.flash('Error while uploading file. Please retry.', type='danger')
961            self.flash('I/O error: %s' % sys.exc_info()[1], type='danger')
[4679]962            return
963        self.redirect(self.url(self.context))
[4671]964
[4858]965    def getNormalizedFileName(self, filename):
966        """Build sane filename.
[4679]967
[4858]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.
[8511]973        Pending data filenames remain unchanged.
[4858]974        """
[8511]975        if filename.endswith('.pending.csv'):
976            return filename
[4858]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
[9024]984    def getImporters(self):
985        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
[12283]986        ikoba_utils = getUtility(IIkobaUtils)
[9025]987        importer_props = []
988        for x in importers:
[12283]989            if not x.util_name in ikoba_utils.BATCH_PROCESSOR_NAMES:
990                continue
[12842]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
[9025]995            iface_fields = schema.getFields(x.iface)
996            available_fields = []
997            for key in iface_fields.keys():
[9033]998                iface_fields[key] = (iface_fields[key].__class__.__name__,
999                    iface_fields[key].required)
[9025]1000            for value in x.available_fields:
1001                available_fields.append(
1002                    dict(f_name=value,
[9033]1003                         f_type=iface_fields.get(value, (None, False))[0],
1004                         f_required=iface_fields.get(value, (None, False))[1]
1005                         )
1006                    )
[9025]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'])
[9024]1011
[8366]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):
[11949]1022        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]1023        self.context.logger.info(
1024            '%s - downloaded: %s' % (ob_class, self.filename))
[8366]1025        self.response.setHeader(
1026            'Content-Type', 'text/csv; charset=UTF-8')
1027        self.response.setHeader(
[9074]1028            'Content-Disposition:', 'attachment; filename="%s' %
1029            self.filename.replace('finished/',''))
[8366]1030        fullpath = os.path.join(self.context.storage, self.filename)
1031        return open(fullpath, 'rb').read()
1032
[9032]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):
[11949]1045        #ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9032]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
[11949]1056class DatacenterImportStep1(IkobaPage):
[4858]1057    """Manual import step 1: choose file
1058    """
1059    grok.context(IDataCenter)
1060    grok.name('import1')
1061    grok.template('datacenterimport1page')
[9590]1062    grok.require('waeup.manageDataCenter')
[7700]1063    label = _(u'Process CSV file')
[4858]1064    pnav = 0
[11254]1065    cancel_button =_(u'Back to Data Center')
[4858]1066
1067    def getFiles(self):
[9023]1068        files = self.context.getPendingFiles(sort='date')
[4858]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
[6146]1074
[4858]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
[11949]1081            session = ISession(self.request)['waeup.ikoba']
[4858]1082            session['import_filename'] = select
1083            self.redirect(self.url(self.context, '@@import2'))
1084
[11949]1085class DatacenterImportStep2(IkobaPage):
[7933]1086    """Manual import step 2: choose processor
[4858]1087    """
1088    grok.context(IDataCenter)
1089    grok.name('import2')
1090    grok.template('datacenterimport2page')
[9590]1091    grok.require('waeup.manageDataCenter')
[7700]1092    label = _(u'Process CSV file')
[4858]1093    pnav = 0
[7705]1094    cancel_button =_(u'Cancel')
1095    back_button =_(u'Back to step 1')
1096    proceed_button =_(u'Proceed to step 3')
[4858]1097
1098    filename = None
1099    mode = 'create'
1100    importer = None
[5000]1101    mode_locked = False
[4858]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()
[6146]1108
[8651]1109    def getPreviewTable(self):
[9322]1110        return getPreviewTable(self, 3)
[8651]1111
[4858]1112    def getImporters(self):
1113        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
[12283]1114        ikoba_utils = getUtility(IIkobaUtils)
[7954]1115        importers = sorted(
[12283]1116            [dict(title=x.name, name=x.util_name) for x in importers
1117            if x.util_name in ikoba_utils.BATCH_PROCESSOR_NAMES])
[4858]1118        return importers
[5000]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
[6828]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(
[7700]1141                    _("Double headers: each column name may only appear once. "))
[6828]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 />'
[7700]1149            warnings += _('Replace imported file!')
[6828]1150            return warnings
1151        return False
1152
[4858]1153    def update(self, mode=None, importer=None,
1154               back1=None, cancel=None, proceed=None):
[11949]1155        session = ISession(self.request)['waeup.ikoba']
[4858]1156        self.filename = session.get('import_filename', None)
[6146]1157
[4858]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:
[11254]1162            self.flash(_('Import aborted.'), type='warning')
[4858]1163            self.redirect(self.url(self.context))
1164            return
1165        self.mode = mode or session.get('import_mode', self.mode)
[5000]1166        filename_mode = self.getModeFromFilename(self.filename)
1167        if filename_mode is not None:
1168            self.mode = filename_mode
1169            self.mode_locked = True
[4858]1170        self.importer = importer or session.get('import_importer', None)
[6837]1171        session['import_importer'] = self.importer
1172        if self.importer and 'update' in self.importer:
1173            if self.mode != 'update':
[11254]1174                self.flash(_('Update mode only!'), type='warning')
[6837]1175                self.mode_locked = True
1176                self.mode = 'update'
1177                proceed = None
[4858]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)
[6828]1183        warnings = self.getWarnings()
1184        if not warnings:
1185            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1186        else:
1187            self.reader = ()
[11254]1188            self.flash(warnings, type='warning')
[4858]1189
[11949]1190class DatacenterImportStep3(IkobaPage):
[4858]1191    """Manual import step 3: modify header
1192    """
1193    grok.context(IDataCenter)
1194    grok.name('import3')
1195    grok.template('datacenterimport3page')
[9590]1196    grok.require('waeup.manageDataCenter')
[7700]1197    label = _(u'Process CSV file')
[4858]1198    pnav = 0
[7705]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')
[4858]1204
1205    filename = None
1206    mode = None
1207    importername = None
[6146]1208
[4858]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()
[6146]1218
[8651]1219    def getPreviewTable(self):
1220        """Get transposed table with 1 sample record.
1221
1222        The first column contains the headers.
[4858]1223        """
[8651]1224        if not self.reader:
1225            return
[8783]1226        headers = self.getPreviewHeader()
[4858]1227        num = 0
[8651]1228        data = []
1229        for line in self.reader:
1230            if num > 0:
[4858]1231                break
1232            num += 1
[8651]1233            data.append(line)
1234        result = []
[8783]1235        field_num = 0
1236        for name in headers:
[8651]1237            result_line = []
[8783]1238            result_line.append(field_num)
1239            field_num += 1
[8651]1240            for d in data:
1241                result_line.append(d[name])
1242            result.append(result_line)
[4858]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)
[6828]1264        if result:
1265            warnings = ""
1266            for line in result:
1267                warnings += line + '<br />'
[7700]1268            warnings += _('Edit headers or replace imported file!')
[6828]1269            return warnings
1270        return False
[6146]1271
[4858]1272    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
[11949]1273        session = ISession(self.request)['waeup.ikoba']
[4858]1274        self.filename = session.get('import_filename', None)
1275        self.mode = session.get('import_mode', None)
1276        self.importername = session.get('import_importer', None)
[6146]1277
[4858]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:
[11254]1285            self.flash(_('Import aborted.'), type='warning')
[4858]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'))
[6828]1298        warnings = self.getWarnings()
1299        if warnings:
[11254]1300            self.flash(warnings, type='warning')
[4858]1301
[11949]1302class DatacenterImportStep4(IkobaPage):
[4858]1303    """Manual import step 4: do actual import
1304    """
1305    grok.context(IDataCenter)
1306    grok.name('import4')
1307    grok.template('datacenterimport4page')
[8367]1308    grok.require('waeup.importData')
[7700]1309    label = _(u'Process CSV file')
[4858]1310    pnav = 0
[10099]1311    back_button =_(u'Process next')
[4858]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:
[10099]1321            self.redirect(self.url(self.context, '@@import1'))
[4858]1322            return
[11949]1323        session = ISession(self.request)['waeup.ikoba']
[4858]1324        self.filename = session.get('import_filename', None)
1325        self.mode = session.get('import_mode', None)
1326        self.importername = session.get('import_importer', None)
[9368]1327        # If the import file contains only one column
1328        # the import_headerfields attribute is a string.
[9369]1329        ihf = session.get('import_headerfields', None)
1330        if not isinstance(ihf, list):
1331            self.headerfields = ihf.split()
1332        else:
1333            self.headerfields = ihf
[6146]1334
[4858]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:
[4909]1341            logfilename = "datacenter.log"
[4858]1342            session['logname'] = logfilename
1343            self.redirect(self.url(self.context, '@@show'))
1344            return
[6146]1345
[4858]1346        self.fullpath = os.path.join(self.context.storage, self.filename)
1347        self.importer = getUtility(IBatchProcessor, name=self.importername)
[4898]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(
[4858]1353            self.fullpath, self.headerfields, self.mode,
[4887]1354            self.request.principal.id, logger=self.context.logger)
[4898]1355        # Put result files in desired locations...
1356        self.context.distProcessedFiles(
[4997]1357            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1358            self.mode)
[4898]1359
[4858]1360        if self.warn_num:
[7700]1361            self.flash(_('Processing of ${a} rows failed.',
[11254]1362                mapping = {'a':self.warn_num}), type='warning')
[7700]1363        self.flash(_('Successfully processed ${a} rows.',
1364            mapping = {'a':linenum - self.warn_num}))
[4858]1365
[11949]1366class DatacenterLogsOverview(IkobaPage):
[4858]1367    grok.context(IDataCenter)
1368    grok.name('logs')
1369    grok.template('datacenterlogspage')
[8367]1370    grok.require('waeup.manageDataCenter')
[7700]1371    label = _(u'Show logfiles')
[4858]1372    pnav = 0
[7705]1373    back_button = _(u'Back to Data Center')
1374    show_button = _(u'Show')
[4858]1375
[8529]1376    def update(self, back=None):
[4858]1377        if back is not None:
1378            self.redirect(self.url(self.context))
1379            return
1380        self.files = self.context.getLogFiles()
1381
[11949]1382class DatacenterLogsFileview(IkobaPage):
[4858]1383    grok.context(IDataCenter)
1384    grok.name('show')
1385    grok.template('datacenterlogsshowfilepage')
[8367]1386    grok.require('waeup.manageDataCenter')
[7700]1387    title = _(u'Data Center')
[4858]1388    pnav = 0
[7749]1389    search_button = _('Search')
[11254]1390    back_button = _('Back to Data Center')
[8529]1391    placeholder = _('Enter a regular expression here...')
[4858]1392
[7465]1393    def label(self):
1394        return "Logfile %s" % self.filename
1395
[8529]1396    def update(self, back=None, query=None, logname=None):
[7750]1397        if os.name != 'posix':
1398            self.flash(
1399                _('Log files can only be searched ' +
[11254]1400                  'on Unix-based operating systems.'), type='danger')
[7750]1401            self.redirect(self.url(self.context, '@@logs'))
1402            return
[4858]1403        if back is not None or logname is None:
1404            self.redirect(self.url(self.context, '@@logs'))
1405            return
1406        self.filename = logname
[8529]1407        self.query = query
[11947]1408        if not query:
[7749]1409            return
[8515]1410        try:
[8529]1411            self.result = ''.join(
[8515]1412                self.context.queryLogfiles(logname, query))
1413        except ValueError:
[11254]1414            self.flash(_('Invalid search expression.'), type='danger')
[8529]1415            return
1416        if not self.result:
[11254]1417            self.flash(_('No search results found.'), type='warning')
[8515]1418        return
[7749]1419
[11949]1420class DatacenterSettings(IkobaPage):
[4679]1421    grok.context(IDataCenter)
1422    grok.name('manage')
1423    grok.template('datacentermanagepage')
[8739]1424    grok.require('waeup.managePortal')
[7700]1425    label = _('Edit data center settings')
[4679]1426    pnav = 0
[7705]1427    save_button =_(u'Save')
1428    reset_button =_(u'Reset')
[11254]1429    cancel_button =_(u'Back to Data Center')
[4677]1430
[4679]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:
[7738]1445                self.flash(_('File already existed (not copied): ${a}',
[11254]1446                    mapping = {'a':name}), type='danger')
[6612]1447        except:
[12192]1448            self.flash(_('Given storage path cannot be used: ${a}',
[11254]1449                        mapping = {'a':sys.exc_info()[1]}), type='danger')
[4679]1450            return
1451        if newpath:
[7700]1452            self.flash(_('New storage path succefully set.'))
[11949]1453            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]1454            self.context.logger.info(
1455                '%s - storage path set: %s' % (ob_class, newpath))
[4679]1456            self.redirect(self.url(self.context))
1457        return
1458
[11949]1459class ExportCSVPage(IkobaPage):
[7908]1460    grok.context(IDataCenter)
1461    grok.name('export')
1462    grok.template('datacenterexportpage')
[10244]1463    grok.require('waeup.exportData')
[7974]1464    label = _('Download portal data as CSV file')
[7908]1465    pnav = 0
[9217]1466    export_button = _(u'Create CSV file')
[11254]1467    cancel_button =_(u'Back to Data Center')
[7908]1468
1469    def getExporters(self):
[12283]1470        exporter_utils = getUtilitiesFor(ICSVExporter)
1471        ikoba_utils = getUtility(IIkobaUtils)
[7908]1472        title_name_tuples = [
[12283]1473            (util.title, name) for name, util in exporter_utils
1474            if name in ikoba_utils.EXPORTER_NAMES]
[7908]1475        return sorted(title_name_tuples)
1476
[11254]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
[9822]1482        if CREATE:
[13802]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
[9822]1494            job_id = self.context.start_export_job(
1495                exporter, self.request.principal.id)
[11949]1496            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1497            self.context.logger.info(
1498                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
[9822]1499        if DISCARD and job_id:
1500            entry = self.context.entry_from_job_id(job_id)
1501            self.context.delete_export_entry(entry)
[11949]1502            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1503            self.context.logger.info(
1504                '%s - discarded: job_id=%s' % (ob_class, job_id))
[9822]1505            self.flash(_('Discarded export') + ' %s' % job_id)
1506        self.entries = doll_up(self, user=None)
[7908]1507        return
1508
1509class ExportCSVView(grok.View):
1510    grok.context(IDataCenter)
[9822]1511    grok.name('download_export')
[10244]1512    grok.require('waeup.exportData')
[7908]1513
[9326]1514    def render(self, job_id=None):
[9217]1515        manager = getUtility(IJobManager)
1516        job = manager.get(job_id)
1517        if job is None:
[7908]1518            return
[9217]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))
[9837]1529        filename = filename.replace('.csv', '_%s.csv' % job_id)
[7908]1530        self.response.setHeader(
1531            'Content-Type', 'text/csv; charset=UTF-8')
[9032]1532        self.response.setHeader(
[9217]1533            'Content-Disposition', 'attachment; filename="%s' % filename)
1534        # remove job and running_exports entry from context
[9822]1535        #self.context.delete_export_entry(
1536        #    self.context.entry_from_job_id(job_id))
[11949]1537        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1538        self.context.logger.info(
1539            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
[9217]1540        return result
[7908]1541
[13066]1542class ChangePasswordRequestPage(IkobaFormPage):
[8346]1543    """Captcha'd page for all kind of users to request a password change.
1544    """
[11954]1545    grok.context(ICompany)
[8777]1546    grok.name('changepw')
[8346]1547    grok.require('waeup.Anonymous')
[8777]1548    grok.template('changepw')
[8346]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
[12540]1559    def _searchUser(self, email):
[11947]1560        # Search customer
[11977]1561        cat = queryUtility(ICatalog, name='customers_catalog')
[12540]1562        results = list(cat.searchResults(email=(email,email)))
1563        if len(results) == 1:
1564            return results[0]
[8346]1565        # Search portal user
[12540]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]
[8346]1573        return None
1574
[9172]1575    @action(_('Send login credentials to email address'), style='primary')
[8346]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
[11947]1581        # Search customer
[8346]1582        email = data['email']
[12540]1583        user = self._searchUser(email)
[8346]1584        if user is None:
[11254]1585            self.flash(_('No record found.'), type='warning')
[8346]1586            return
1587        # Change password
[11949]1588        ikoba_utils = getUtility(IIkobaUtils)
1589        password = ikoba_utils.genPassword()
[8858]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')
[11949]1600        success = ikoba_utils.sendCredentials(
[8858]1601            IUserAccount(user),password,url_info,msg)
[8346]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:
[11254]1606            self.flash(_('An smtp server error occurred.'), type='danger')
[11949]1607        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8740]1608        self.context.logger.info(
[12540]1609            '%s - %s - %s' % (ob_class, IUserAccount(user).name, data['email']))
[8515]1610        return
Note: See TracBrowser for help on using the repository browser.