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

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

Ticket #11 compromise

Redirect to CustomerChangePasswordPage? if PasswordMandate? was used so that customers are reminded of changing the password. But we do not require a password change. Maybe customers feel comfortable with the generated password.

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