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

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

We do not need the HTMLDisplayWidget. Use simple helper function instead. Tests will follow.

  • Property svn:keywords set to Id
File size: 57.1 KB
RevLine 
[7195]1## $Id: pages.py 12229 2014-12-14 15:45:55Z 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')
[7700]517    label = _('Portal Users')
[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)
[9025]966        importer_props = []
967        for x in importers:
968            iface_fields = schema.getFields(x.iface)
969            available_fields = []
970            for key in iface_fields.keys():
[9033]971                iface_fields[key] = (iface_fields[key].__class__.__name__,
972                    iface_fields[key].required)
[9025]973            for value in x.available_fields:
974                available_fields.append(
975                    dict(f_name=value,
[9033]976                         f_type=iface_fields.get(value, (None, False))[0],
977                         f_required=iface_fields.get(value, (None, False))[1]
978                         )
979                    )
[9025]980            available_fields = sorted(available_fields, key=lambda k: k['f_name'])
981            importer_props.append(
982                dict(title=x.name, name=x.util_name, fields=available_fields))
983        return sorted(importer_props, key=lambda k: k['title'])
[9024]984
[8366]985class FileDownloadView(UtilityView, grok.View):
986    grok.context(IDataCenter)
987    grok.name('download')
988    grok.require('waeup.manageDataCenter')
989
990    def update(self, filename=None):
991        self.filename = self.request.form['filename']
992        return
993
994    def render(self):
[11949]995        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]996        self.context.logger.info(
997            '%s - downloaded: %s' % (ob_class, self.filename))
[8366]998        self.response.setHeader(
999            'Content-Type', 'text/csv; charset=UTF-8')
1000        self.response.setHeader(
[9074]1001            'Content-Disposition:', 'attachment; filename="%s' %
1002            self.filename.replace('finished/',''))
[8366]1003        fullpath = os.path.join(self.context.storage, self.filename)
1004        return open(fullpath, 'rb').read()
1005
[9032]1006class SkeletonDownloadView(UtilityView, grok.View):
1007    grok.context(IDataCenter)
1008    grok.name('skeleton')
1009    grok.require('waeup.manageDataCenter')
1010
1011    def update(self, processorname=None):
1012        self.processorname = self.request.form['name']
1013        self.filename = ('%s_000.csv' %
1014            self.processorname.replace('processor','import'))
1015        return
1016
1017    def render(self):
[11949]1018        #ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9032]1019        #self.context.logger.info(
1020        #    '%s - skeleton downloaded: %s' % (ob_class, self.filename))
1021        self.response.setHeader(
1022            'Content-Type', 'text/csv; charset=UTF-8')
1023        self.response.setHeader(
1024            'Content-Disposition:', 'attachment; filename="%s' % self.filename)
1025        processor = getUtility(IBatchProcessor, name=self.processorname)
1026        csv_data = processor.get_csv_skeleton()
1027        return csv_data
1028
[11949]1029class DatacenterImportStep1(IkobaPage):
[4858]1030    """Manual import step 1: choose file
1031    """
1032    grok.context(IDataCenter)
1033    grok.name('import1')
1034    grok.template('datacenterimport1page')
[9590]1035    grok.require('waeup.manageDataCenter')
[7700]1036    label = _(u'Process CSV file')
[4858]1037    pnav = 0
[11254]1038    cancel_button =_(u'Back to Data Center')
[4858]1039
1040    def getFiles(self):
[9023]1041        files = self.context.getPendingFiles(sort='date')
[4858]1042        for file in files:
1043            name = file.name
1044            if not name.endswith('.csv') and not name.endswith('.pending'):
1045                continue
1046            yield file
[6146]1047
[4858]1048    def update(self, filename=None, select=None, cancel=None):
1049        if cancel is not None:
1050            self.redirect(self.url(self.context))
1051            return
1052        if select is not None:
1053            # A filename was selected
[11949]1054            session = ISession(self.request)['waeup.ikoba']
[4858]1055            session['import_filename'] = select
1056            self.redirect(self.url(self.context, '@@import2'))
1057
[11949]1058class DatacenterImportStep2(IkobaPage):
[7933]1059    """Manual import step 2: choose processor
[4858]1060    """
1061    grok.context(IDataCenter)
1062    grok.name('import2')
1063    grok.template('datacenterimport2page')
[9590]1064    grok.require('waeup.manageDataCenter')
[7700]1065    label = _(u'Process CSV file')
[4858]1066    pnav = 0
[7705]1067    cancel_button =_(u'Cancel')
1068    back_button =_(u'Back to step 1')
1069    proceed_button =_(u'Proceed to step 3')
[4858]1070
1071    filename = None
1072    mode = 'create'
1073    importer = None
[5000]1074    mode_locked = False
[4858]1075
1076    def getPreviewHeader(self):
1077        """Get the header fields of attached CSV file.
1078        """
1079        reader = csv.reader(open(self.fullpath, 'rb'))
1080        return reader.next()
[6146]1081
[8651]1082    def getPreviewTable(self):
[9322]1083        return getPreviewTable(self, 3)
[8651]1084
[4858]1085    def getImporters(self):
1086        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
[7954]1087        importers = sorted(
1088            [dict(title=x.name, name=x.util_name) for x in importers])
[4858]1089        return importers
[5000]1090
1091    def getModeFromFilename(self, filename):
1092        """Lookup filename or path and return included mode name or None.
1093        """
1094        if not filename.endswith('.pending.csv'):
1095            return None
1096        base = os.path.basename(filename)
1097        parts = base.rsplit('.', 3)
1098        if len(parts) != 4:
1099            return None
1100        if parts[1] not in ['create', 'update', 'remove']:
1101            return None
1102        return parts[1]
1103
[6828]1104    def getWarnings(self):
1105        import sys
1106        result = []
1107        try:
1108            headerfields = self.getPreviewHeader()
1109            headerfields_clean = list(set(headerfields))
1110            if len(headerfields) > len(headerfields_clean):
1111                result.append(
[7700]1112                    _("Double headers: each column name may only appear once. "))
[6828]1113        except:
1114            fatal = '%s' % sys.exc_info()[1]
1115            result.append(fatal)
1116        if result:
1117            warnings = ""
1118            for line in result:
1119                warnings += line + '<br />'
[7700]1120            warnings += _('Replace imported file!')
[6828]1121            return warnings
1122        return False
1123
[4858]1124    def update(self, mode=None, importer=None,
1125               back1=None, cancel=None, proceed=None):
[11949]1126        session = ISession(self.request)['waeup.ikoba']
[4858]1127        self.filename = session.get('import_filename', None)
[6146]1128
[4858]1129        if self.filename is None or back1 is not None:
1130            self.redirect(self.url(self.context, '@@import1'))
1131            return
1132        if cancel is not None:
[11254]1133            self.flash(_('Import aborted.'), type='warning')
[4858]1134            self.redirect(self.url(self.context))
1135            return
1136        self.mode = mode or session.get('import_mode', self.mode)
[5000]1137        filename_mode = self.getModeFromFilename(self.filename)
1138        if filename_mode is not None:
1139            self.mode = filename_mode
1140            self.mode_locked = True
[4858]1141        self.importer = importer or session.get('import_importer', None)
[6837]1142        session['import_importer'] = self.importer
1143        if self.importer and 'update' in self.importer:
1144            if self.mode != 'update':
[11254]1145                self.flash(_('Update mode only!'), type='warning')
[6837]1146                self.mode_locked = True
1147                self.mode = 'update'
1148                proceed = None
[4858]1149        session['import_mode'] = self.mode
1150        if proceed is not None:
1151            self.redirect(self.url(self.context, '@@import3'))
1152            return
1153        self.fullpath = os.path.join(self.context.storage, self.filename)
[6828]1154        warnings = self.getWarnings()
1155        if not warnings:
1156            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1157        else:
1158            self.reader = ()
[11254]1159            self.flash(warnings, type='warning')
[4858]1160
[11949]1161class DatacenterImportStep3(IkobaPage):
[4858]1162    """Manual import step 3: modify header
1163    """
1164    grok.context(IDataCenter)
1165    grok.name('import3')
1166    grok.template('datacenterimport3page')
[9590]1167    grok.require('waeup.manageDataCenter')
[7700]1168    label = _(u'Process CSV file')
[4858]1169    pnav = 0
[7705]1170    cancel_button =_(u'Cancel')
1171    reset_button =_(u'Reset')
1172    update_button =_(u'Set headerfields')
1173    back_button =_(u'Back to step 2')
1174    proceed_button =_(u'Perform import')
[4858]1175
1176    filename = None
1177    mode = None
1178    importername = None
[6146]1179
[4858]1180    @property
1181    def nextstep(self):
1182        return self.url(self.context, '@@import4')
1183
1184    def getPreviewHeader(self):
1185        """Get the header fields of attached CSV file.
1186        """
1187        reader = csv.reader(open(self.fullpath, 'rb'))
1188        return reader.next()
[6146]1189
[8651]1190    def getPreviewTable(self):
1191        """Get transposed table with 1 sample record.
1192
1193        The first column contains the headers.
[4858]1194        """
[8651]1195        if not self.reader:
1196            return
[8783]1197        headers = self.getPreviewHeader()
[4858]1198        num = 0
[8651]1199        data = []
1200        for line in self.reader:
1201            if num > 0:
[4858]1202                break
1203            num += 1
[8651]1204            data.append(line)
1205        result = []
[8783]1206        field_num = 0
1207        for name in headers:
[8651]1208            result_line = []
[8783]1209            result_line.append(field_num)
1210            field_num += 1
[8651]1211            for d in data:
1212                result_line.append(d[name])
1213            result.append(result_line)
[4858]1214        return result
1215
1216    def getPossibleHeaders(self):
1217        """Get the possible headers.
1218
1219        The headers are described as dicts {value:internal_name,
1220        title:displayed_name}
1221        """
1222        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
1223        headers = self.importer.getHeaders()
1224        result.extend([dict(title=x, value=x) for x in headers])
1225        return result
1226
1227    def getWarnings(self):
1228        import sys
1229        result = []
1230        try:
1231            self.importer.checkHeaders(self.headerfields, mode=self.mode)
1232        except:
1233            fatal = '%s' % sys.exc_info()[1]
1234            result.append(fatal)
[6828]1235        if result:
1236            warnings = ""
1237            for line in result:
1238                warnings += line + '<br />'
[7700]1239            warnings += _('Edit headers or replace imported file!')
[6828]1240            return warnings
1241        return False
[6146]1242
[4858]1243    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
[11949]1244        session = ISession(self.request)['waeup.ikoba']
[4858]1245        self.filename = session.get('import_filename', None)
1246        self.mode = session.get('import_mode', None)
1247        self.importername = session.get('import_importer', None)
[6146]1248
[4858]1249        if None in (self.filename, self.mode, self.importername):
1250            self.redirect(self.url(self.context, '@@import2'))
1251            return
1252        if back2 is not None:
1253            self.redirect(self.url(self.context ,'@@import2'))
1254            return
1255        if cancel is not None:
[11254]1256            self.flash(_('Import aborted.'), type='warning')
[4858]1257            self.redirect(self.url(self.context))
1258            return
1259
1260        self.fullpath = os.path.join(self.context.storage, self.filename)
1261        self.headerfields = headerfield or self.getPreviewHeader()
1262        session['import_headerfields'] = self.headerfields
1263
1264        if proceed is not None:
1265            self.redirect(self.url(self.context, '@@import4'))
1266            return
1267        self.importer = getUtility(IBatchProcessor, name=self.importername)
1268        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
[6828]1269        warnings = self.getWarnings()
1270        if warnings:
[11254]1271            self.flash(warnings, type='warning')
[4858]1272
[11949]1273class DatacenterImportStep4(IkobaPage):
[4858]1274    """Manual import step 4: do actual import
1275    """
1276    grok.context(IDataCenter)
1277    grok.name('import4')
1278    grok.template('datacenterimport4page')
[8367]1279    grok.require('waeup.importData')
[7700]1280    label = _(u'Process CSV file')
[4858]1281    pnav = 0
[10099]1282    back_button =_(u'Process next')
[4858]1283
1284    filename = None
1285    mode = None
1286    importername = None
1287    headerfields = None
1288    warnnum = None
1289
1290    def update(self, back=None, finish=None, showlog=None):
1291        if finish is not None:
[10099]1292            self.redirect(self.url(self.context, '@@import1'))
[4858]1293            return
[11949]1294        session = ISession(self.request)['waeup.ikoba']
[4858]1295        self.filename = session.get('import_filename', None)
1296        self.mode = session.get('import_mode', None)
1297        self.importername = session.get('import_importer', None)
[9368]1298        # If the import file contains only one column
1299        # the import_headerfields attribute is a string.
[9369]1300        ihf = session.get('import_headerfields', None)
1301        if not isinstance(ihf, list):
1302            self.headerfields = ihf.split()
1303        else:
1304            self.headerfields = ihf
[6146]1305
[4858]1306        if None in (self.filename, self.mode, self.importername,
1307                    self.headerfields):
1308            self.redirect(self.url(self.context, '@@import3'))
1309            return
1310
1311        if showlog is not None:
[4909]1312            logfilename = "datacenter.log"
[4858]1313            session['logname'] = logfilename
1314            self.redirect(self.url(self.context, '@@show'))
1315            return
[6146]1316
[4858]1317        self.fullpath = os.path.join(self.context.storage, self.filename)
1318        self.importer = getUtility(IBatchProcessor, name=self.importername)
[4898]1319
1320        # Perform batch processing...
1321        # XXX: This might be better placed in datacenter module.
1322        (linenum, self.warn_num,
1323         fin_path, pending_path) = self.importer.doImport(
[4858]1324            self.fullpath, self.headerfields, self.mode,
[4887]1325            self.request.principal.id, logger=self.context.logger)
[4898]1326        # Put result files in desired locations...
1327        self.context.distProcessedFiles(
[4997]1328            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1329            self.mode)
[4898]1330
[4858]1331        if self.warn_num:
[7700]1332            self.flash(_('Processing of ${a} rows failed.',
[11254]1333                mapping = {'a':self.warn_num}), type='warning')
[7700]1334        self.flash(_('Successfully processed ${a} rows.',
1335            mapping = {'a':linenum - self.warn_num}))
[4858]1336
[11949]1337class DatacenterLogsOverview(IkobaPage):
[4858]1338    grok.context(IDataCenter)
1339    grok.name('logs')
1340    grok.template('datacenterlogspage')
[8367]1341    grok.require('waeup.manageDataCenter')
[7700]1342    label = _(u'Show logfiles')
[4858]1343    pnav = 0
[7705]1344    back_button = _(u'Back to Data Center')
1345    show_button = _(u'Show')
[4858]1346
[8529]1347    def update(self, back=None):
[4858]1348        if back is not None:
1349            self.redirect(self.url(self.context))
1350            return
1351        self.files = self.context.getLogFiles()
1352
[11949]1353class DatacenterLogsFileview(IkobaPage):
[4858]1354    grok.context(IDataCenter)
1355    grok.name('show')
1356    grok.template('datacenterlogsshowfilepage')
[8367]1357    grok.require('waeup.manageDataCenter')
[7700]1358    title = _(u'Data Center')
[4858]1359    pnav = 0
[7749]1360    search_button = _('Search')
[11254]1361    back_button = _('Back to Data Center')
[8529]1362    placeholder = _('Enter a regular expression here...')
[4858]1363
[7465]1364    def label(self):
1365        return "Logfile %s" % self.filename
1366
[8529]1367    def update(self, back=None, query=None, logname=None):
[7750]1368        if os.name != 'posix':
1369            self.flash(
1370                _('Log files can only be searched ' +
[11254]1371                  'on Unix-based operating systems.'), type='danger')
[7750]1372            self.redirect(self.url(self.context, '@@logs'))
1373            return
[4858]1374        if back is not None or logname is None:
1375            self.redirect(self.url(self.context, '@@logs'))
1376            return
1377        self.filename = logname
[8529]1378        self.query = query
[11947]1379        if not query:
[7749]1380            return
[8515]1381        try:
[8529]1382            self.result = ''.join(
[8515]1383                self.context.queryLogfiles(logname, query))
1384        except ValueError:
[11254]1385            self.flash(_('Invalid search expression.'), type='danger')
[8529]1386            return
1387        if not self.result:
[11254]1388            self.flash(_('No search results found.'), type='warning')
[8515]1389        return
[7749]1390
[11949]1391class DatacenterSettings(IkobaPage):
[4679]1392    grok.context(IDataCenter)
1393    grok.name('manage')
1394    grok.template('datacentermanagepage')
[8739]1395    grok.require('waeup.managePortal')
[7700]1396    label = _('Edit data center settings')
[4679]1397    pnav = 0
[7705]1398    save_button =_(u'Save')
1399    reset_button =_(u'Reset')
[11254]1400    cancel_button =_(u'Back to Data Center')
[4677]1401
[4679]1402    def update(self, newpath=None, move=False, overwrite=False,
1403               save=None, cancel=None):
1404        if move:
1405            move = True
1406        if overwrite:
1407            overwrite = True
1408        if newpath is None:
1409            return
1410        if cancel is not None:
1411            self.redirect(self.url(self.context))
1412            return
1413        try:
1414            not_copied = self.context.setStoragePath(newpath, move=move)
1415            for name in not_copied:
[7738]1416                self.flash(_('File already existed (not copied): ${a}',
[11254]1417                    mapping = {'a':name}), type='danger')
[6612]1418        except:
[12192]1419            self.flash(_('Given storage path cannot be used: ${a}',
[11254]1420                        mapping = {'a':sys.exc_info()[1]}), type='danger')
[4679]1421            return
1422        if newpath:
[7700]1423            self.flash(_('New storage path succefully set.'))
[11949]1424            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]1425            self.context.logger.info(
1426                '%s - storage path set: %s' % (ob_class, newpath))
[4679]1427            self.redirect(self.url(self.context))
1428        return
1429
[11949]1430class ExportCSVPage(IkobaPage):
[7908]1431    grok.context(IDataCenter)
1432    grok.name('export')
1433    grok.template('datacenterexportpage')
[10244]1434    grok.require('waeup.exportData')
[7974]1435    label = _('Download portal data as CSV file')
[7908]1436    pnav = 0
[9217]1437    export_button = _(u'Create CSV file')
[11254]1438    cancel_button =_(u'Back to Data Center')
[7908]1439
1440    def getExporters(self):
1441        utils = getUtilitiesFor(ICSVExporter)
1442        title_name_tuples = [
[11947]1443            (util.title, name) for name, util in utils]
[7908]1444        return sorted(title_name_tuples)
1445
[11254]1446    def update(self, CREATE=None, DISCARD=None, exporter=None,
1447               job_id=None, CANCEL=None):
1448        if CANCEL is not None:
1449            self.redirect(self.url(self.context))
1450            return
[9822]1451        if CREATE:
1452            job_id = self.context.start_export_job(
1453                exporter, self.request.principal.id)
[11949]1454            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1455            self.context.logger.info(
1456                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
[9822]1457        if DISCARD and job_id:
1458            entry = self.context.entry_from_job_id(job_id)
1459            self.context.delete_export_entry(entry)
[11949]1460            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1461            self.context.logger.info(
1462                '%s - discarded: job_id=%s' % (ob_class, job_id))
[9822]1463            self.flash(_('Discarded export') + ' %s' % job_id)
1464        self.entries = doll_up(self, user=None)
[7908]1465        return
1466
1467class ExportCSVView(grok.View):
1468    grok.context(IDataCenter)
[9822]1469    grok.name('download_export')
[10244]1470    grok.require('waeup.exportData')
[7908]1471
[9326]1472    def render(self, job_id=None):
[9217]1473        manager = getUtility(IJobManager)
1474        job = manager.get(job_id)
1475        if job is None:
[7908]1476            return
[9217]1477        if hasattr(job.result, 'traceback'):
1478            # XXX: Some error happened. Do something more approriate here...
1479            return
1480        path = job.result
1481        if not os.path.exists(path):
1482            # XXX: Do something more appropriate here...
1483            return
1484        result = open(path, 'rb').read()
1485        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
1486        filename = "%s_%s" % (acronym, os.path.basename(path))
[9837]1487        filename = filename.replace('.csv', '_%s.csv' % job_id)
[7908]1488        self.response.setHeader(
1489            'Content-Type', 'text/csv; charset=UTF-8')
[9032]1490        self.response.setHeader(
[9217]1491            'Content-Disposition', 'attachment; filename="%s' % filename)
1492        # remove job and running_exports entry from context
[9822]1493        #self.context.delete_export_entry(
1494        #    self.context.entry_from_job_id(job_id))
[11949]1495        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1496        self.context.logger.info(
1497            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
[9217]1498        return result
[7908]1499
[11949]1500class ChangePasswordRequestPage(IkobaForm):
[8346]1501    """Captcha'd page for all kind of users to request a password change.
1502    """
[11954]1503    grok.context(ICompany)
[8777]1504    grok.name('changepw')
[8346]1505    grok.require('waeup.Anonymous')
[8777]1506    grok.template('changepw')
[8346]1507    label = _('Send me a new password')
1508    form_fields = grok.AutoFields(IChangePassword)
1509
1510    def update(self):
1511        # Handle captcha
1512        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1513        self.captcha_result = self.captcha.verify(self.request)
1514        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1515        return
1516
1517    def _searchUser(self, identifier, email):
[11947]1518        # Search customer
[11977]1519        cat = queryUtility(ICatalog, name='customers_catalog')
1520        results = cat.searchResults(
1521            reg_number=(identifier, identifier),
1522            email=(email,email))
1523        for result in results:
1524            if result.customer_id == identifier \
1525                or result.reg_number == identifier:
1526                return result
[8346]1527        # Search portal user
1528        user = grok.getSite()['users'].get(identifier, None)
1529        if user is not None and user.email == email:
1530            return user
1531        return None
1532
[9172]1533    @action(_('Send login credentials to email address'), style='primary')
[8346]1534    def request(self, **data):
1535        if not self.captcha_result.is_valid:
1536            # Captcha will display error messages automatically.
1537            # No need to flash something.
1538            return
[11947]1539        # Search customer
[8346]1540        identifier = data['identifier']
1541        email = data['email']
1542        user = self._searchUser(identifier, email)
1543        if user is None:
[11254]1544            self.flash(_('No record found.'), type='warning')
[8346]1545            return
1546        # Change password
[11949]1547        ikoba_utils = getUtility(IIkobaUtils)
1548        password = ikoba_utils.genPassword()
[8858]1549        mandate = PasswordMandate()
1550        mandate.params['password'] = password
1551        mandate.params['user'] = user
1552        site = grok.getSite()
1553        site['mandates'].addMandate(mandate)
1554        # Send email with credentials
1555        args = {'mandate_id':mandate.mandate_id}
1556        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1557        url_info = u'Confirmation link: %s' % mandate_url
1558        msg = _('You have successfully requested a password for the')
[11949]1559        success = ikoba_utils.sendCredentials(
[8858]1560            IUserAccount(user),password,url_info,msg)
[8346]1561        if success:
1562            self.flash(_('An email with your user name and password ' +
1563                'has been sent to ${a}.', mapping = {'a':email}))
1564        else:
[11254]1565            self.flash(_('An smtp server error occurred.'), type='danger')
[11949]1566        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8740]1567        self.context.logger.info(
1568            '%s - %s - %s' % (ob_class, data['identifier'], data['email']))
[8515]1569        return
Note: See TracBrowser for help on using the repository browser.