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

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

Replace 'Portal Users' by 'Officers' because also customers are portal users.

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