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

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

Registration and Application Portal/System? (RAPS)

Adjust localization.

  • Property svn:keywords set to Id
File size: 57.5 KB
RevLine 
[7195]1## $Id: pages.py 12192 2014-12-10 16:38:15Z 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
[11949]44from waeup.ikoba.browser.layout import (
45    IkobaPage, IkobaForm, IkobaEditFormPage, IkobaAddFormPage,
46    IkobaDisplayFormPage, NullValidator)
47from waeup.ikoba.browser.interfaces import (
[11954]48    ICompany, ICaptchaManager, IChangePassword)
[11949]49from waeup.ikoba.browser.layout import jsaction, action, UtilityView
50from waeup.ikoba.interfaces import MessageFactory as _
51from waeup.ikoba.interfaces import(
52    IIkobaObject, IUsersContainer, IUserAccount, IDataCenter,
53    IIkobaXMLImporter, IIkobaXMLExporter, IBatchProcessor,
[6916]54    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
[12186]55    IJobManager,
56    IPasswordValidator, IContactForm, IIkobaUtils, ICSVExporter)
[11949]57from waeup.ikoba.permissions import (
[9311]58    get_users_with_local_roles, get_all_roles, get_all_users,
59    get_users_with_role)
[9217]60
[11949]61from waeup.ikoba.authentication import LocalRoleSetEvent
62from waeup.ikoba.widgets.htmlwidget import HTMLDisplayWidget
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 ConfigurationContainerDisplayFormPage(IkobaDisplayFormPage):
[6907]726    """View page of the configuration container.
727    """
728    grok.require('waeup.managePortalConfiguration')
729    grok.name('view')
730    grok.context(IConfigurationContainer)
731    pnav = 0
[7700]732    label = _(u'View portal configuration')
[6907]733    form_fields = grok.AutoFields(IConfigurationContainer)
[8361]734    form_fields['frontpage'].custom_widget = HTMLDisplayWidget
[6907]735
[11254]736
[11949]737class ConfigurationContainerManageFormPage(IkobaEditFormPage):
[6916]738    """Manage page of the configuration container. We always use the
739    manage page in the UI not the view page, thus we use the index name here.
[6907]740    """
741    grok.require('waeup.managePortalConfiguration')
742    grok.name('index')
743    grok.context(IConfigurationContainer)
744    pnav = 0
[7700]745    label = _(u'Edit portal configuration')
[11254]746    form_fields = grok.AutoFields(IConfigurationContainer).omit(
747        'frontpage_dict')
[6907]748
[7702]749    def _frontpage(self):
[7485]750        view = ConfigurationContainerDisplayFormPage(
751            self.context,self.request)
752        view.setUpWidgets()
753        return view.widgets['frontpage']()
754
[7707]755    @action(_('Save'), style='primary')
[6907]756    def save(self, **data):
[8739]757        msave(self, **data)
[7702]758        self.context.frontpage_dict = self._frontpage()
[6907]759        return
760
[11438]761    @action(_('Update plugins'),
762              tooltip=_('For experts only!'),
763              warning=_('Plugins may only be updated after software upgrades. '
764                        'Are you really sure?'),
765              validator=NullValidator)
[6907]766    def updatePlugins(self, **data):
767        grok.getSite().updatePlugins()
[7700]768        self.flash(_('Plugins were updated. See log file for details.'))
[6907]769        return
770
771#
[4671]772# Datacenter pages...
773#
[4661]774
[11949]775class DatacenterPage(IkobaEditFormPage):
[4671]776    grok.context(IDataCenter)
777    grok.name('index')
[8366]778    grok.require('waeup.manageDataCenter')
[7700]779    label = _(u'Data Center')
[4671]780    pnav = 0
781
[8366]782    @jsaction(_('Remove selected'))
783    def delFiles(self, **data):
784        form = self.request.form
[9701]785        if 'val_id' in form:
[8366]786            child_id = form['val_id']
787        else:
[11254]788            self.flash(_('No item selected.'), type='danger')
[8366]789            return
790        if not isinstance(child_id, list):
791            child_id = [child_id]
792        deleted = []
793        for id in child_id:
794            fullpath = os.path.join(self.context.storage, id)
795            try:
796                os.remove(fullpath)
797                deleted.append(id)
798            except OSError:
[11254]799                self.flash(_('OSError: The file could not be deleted.'),
800                           type='danger')
[8366]801                return
802        if len(deleted):
803            self.flash(_('Successfully deleted: ${a}',
804                mapping = {'a': ', '.join(deleted)}))
[11949]805            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]806            self.context.logger.info(
807                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
[8366]808        return
809
[11949]810class DatacenterFinishedPage(IkobaEditFormPage):
[9023]811    grok.context(IDataCenter)
812    grok.name('processed')
813    grok.require('waeup.manageDataCenter')
814    label = _(u'Processed Files')
815    pnav = 0
[11254]816    cancel_button =_('Back to Data Center')
[9023]817
[11254]818    def update(self, CANCEL=None):
819        if CANCEL is not None:
820            self.redirect(self.url(self.context))
821            return
[9023]822        return super(DatacenterFinishedPage, self).update()
823
[11949]824class DatacenterUploadPage(IkobaPage):
[4674]825    grok.context(IDataCenter)
826    grok.name('upload')
[8366]827    grok.require('waeup.manageDataCenter')
[9024]828    label = _(u'Upload portal data as CSV file')
[4674]829    pnav = 0
[9707]830    max_files = 20
[7705]831    upload_button =_(u'Upload')
[11254]832    cancel_button =_(u'Back to Data Center')
[6146]833
[9322]834    def getPreviewHeader(self):
835        """Get the header fields of uploaded CSV file.
836        """
837        reader = csv.reader(open(self.fullpath, 'rb'))
838        return reader.next()
839
[9323]840    def _notifyImportManagers(self, filename,
841        normalized_filename, importer, import_mode):
[9311]842        """Send email to Import Managers
843        """
[9322]844        # Get information about file
845        self.fullpath = os.path.join(self.context.storage, normalized_filename)
846        uploadfile = DataCenterFile(self.fullpath)
847        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
848        table = getPreviewTable(self, 3)
849        mail_table = ''
850        for line in table:
851            header = line[0]
852            data = str(line[1:]).strip('[').strip(']')
[9323]853            mail_table += '%s: %s ...\n' % (line[0], data)
[9322]854        # Collect all recipient addresses
[11949]855        ikoba_utils = getUtility(IIkobaUtils)
[9311]856        import_managers = get_users_with_role(
857            'waeup.ImportManager', grok.getSite())
858        rcpt_addrs = ','.join(
859            [user['user_email'] for user in import_managers if
860                user['user_email'] is not None])
861        if rcpt_addrs:
862            config = grok.getSite()['configuration']
863            fullname = self.request.principal.title
864            try:
865                email = self.request.principal.email
866            except AttributeError:
867                email = config.email_admin
868            username = self.request.principal.id
869            usertype = getattr(self.request.principal,
870                               'user_type', 'system').title()
871            rcpt_name = _('Import Manager')
872            subject = translate(
[9323]873                      _('${a}: ${b} uploaded',
874                      mapping = {'a':config.acronym, 'b':filename}),
[11949]875                      'waeup.ikoba',
876                      target_language=ikoba_utils.PORTAL_LANGUAGE)
[9322]877            text = _("""File: ${a}
[9323]878Importer: ${b}
[9322]879Import Mode: ${c}
880Datasets: ${d}
881
882${e}
883
[9334]884Comment by Import Manager:""", mapping = {'a':normalized_filename,
[9322]885                'b':importer,
886                'c':import_mode,
887                'd':uploadfile.lines - 1,
888                'e':mail_table})
[11949]889            success = ikoba_utils.sendContactForm(
[9311]890                    fullname,email,
891                    rcpt_name,rcpt_addrs,
892                    username,usertype,config.name,
893                    text,subject)
894            if success:
895                self.flash(
896                    _('All import managers have been notified by email.'))
897            else:
[11254]898                self.flash(_('An smtp server error occurred.'), type='danger')
[9311]899            return
900
[9322]901    def update(self, uploadfile=None, import_mode=None,
902               importer=None, CANCEL=None, SUBMIT=None):
[9610]903        number_of_pendings = len(self.context.getPendingFiles())
904        if number_of_pendings > self.max_files:
905            self.flash(
[11254]906                _('Maximum number of files in the data center exceeded.'),
907                  type='danger')
[9610]908            self.redirect(self.url(self.context))
909            return
[4679]910        if CANCEL is not None:
911            self.redirect(self.url(self.context))
912            return
913        if not uploadfile:
914            return
915        try:
916            filename = uploadfile.filename
[8510]917            #if 'pending' in filename:
[11254]918            #    self.flash(_("You can't re-upload pending data files."), type='danger')
[8510]919            #    return
[8366]920            if not filename.endswith('.csv'):
[11254]921                self.flash(_("Only csv files are allowed."), type='danger')
[8366]922                return
[9038]923            normalized_filename = self.getNormalizedFileName(filename)
924            finished_file = os.path.join(
925                self.context.storage, 'finished', normalized_filename)
926            unfinished_file = os.path.join(
927                self.context.storage, 'unfinished', normalized_filename)
928            if os.path.exists(finished_file) or os.path.exists(unfinished_file):
[11254]929                self.flash(_("File with same name was uploaded earlier."),
930                           type='danger')
[9038]931                return
932            target = os.path.join(self.context.storage, normalized_filename)
[9930]933            filecontent = uploadfile.read()
[11949]934            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9931]935            logger = self.context.logger
[9930]936
[10029]937            # Forbid certain characters in import files.
[10676]938            invalid_line = check_csv_charset(filecontent.splitlines())
939            if invalid_line:
940                self.flash(_(
941                    "Your file contains forbidden characters or "
942                    "has invalid CSV format. "
943                    "First problematic line detected: line %s. "
[11254]944                    "Please replace." % invalid_line), type='danger')
[10676]945                logger.info('%s - invalid file uploaded: %s' %
946                            (ob_class, target))
947                return
[9930]948
949            open(target, 'wb').write(filecontent)
[8573]950            os.chmod(target, 0664)
[8739]951            logger.info('%s - uploaded: %s' % (ob_class, target))
[9323]952            self._notifyImportManagers(filename,
[9322]953                normalized_filename, importer, import_mode)
[9311]954
[4679]955        except IOError:
[11254]956            self.flash('Error while uploading file. Please retry.', type='danger')
957            self.flash('I/O error: %s' % sys.exc_info()[1], type='danger')
[4679]958            return
959        self.redirect(self.url(self.context))
[4671]960
[4858]961    def getNormalizedFileName(self, filename):
962        """Build sane filename.
[4679]963
[4858]964        An uploaded file foo.csv will be stored as foo_USERNAME.csv
965        where username is the principal id of the currently logged in
966        user.
967
968        Spaces in filename are replaced by underscore.
[8511]969        Pending data filenames remain unchanged.
[4858]970        """
[8511]971        if filename.endswith('.pending.csv'):
972            return filename
[4858]973        username = self.request.principal.id
974        filename = filename.replace(' ', '_')
975        # Only accept typical filname chars...
976        filtered_username = ''.join(re.findall('[a-zA-Z0-9_\.\-]', username))
977        base, ext = os.path.splitext(filename)
978        return '%s_%s%s' % (base, filtered_username, ext.lower())
979
[9024]980    def getImporters(self):
981        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
[9025]982        importer_props = []
983        for x in importers:
984            iface_fields = schema.getFields(x.iface)
985            available_fields = []
986            for key in iface_fields.keys():
[9033]987                iface_fields[key] = (iface_fields[key].__class__.__name__,
988                    iface_fields[key].required)
[9025]989            for value in x.available_fields:
990                available_fields.append(
991                    dict(f_name=value,
[9033]992                         f_type=iface_fields.get(value, (None, False))[0],
993                         f_required=iface_fields.get(value, (None, False))[1]
994                         )
995                    )
[9025]996            available_fields = sorted(available_fields, key=lambda k: k['f_name'])
997            importer_props.append(
998                dict(title=x.name, name=x.util_name, fields=available_fields))
999        return sorted(importer_props, key=lambda k: k['title'])
[9024]1000
[8366]1001class FileDownloadView(UtilityView, grok.View):
1002    grok.context(IDataCenter)
1003    grok.name('download')
1004    grok.require('waeup.manageDataCenter')
1005
1006    def update(self, filename=None):
1007        self.filename = self.request.form['filename']
1008        return
1009
1010    def render(self):
[11949]1011        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]1012        self.context.logger.info(
1013            '%s - downloaded: %s' % (ob_class, self.filename))
[8366]1014        self.response.setHeader(
1015            'Content-Type', 'text/csv; charset=UTF-8')
1016        self.response.setHeader(
[9074]1017            'Content-Disposition:', 'attachment; filename="%s' %
1018            self.filename.replace('finished/',''))
[8366]1019        fullpath = os.path.join(self.context.storage, self.filename)
1020        return open(fullpath, 'rb').read()
1021
[9032]1022class SkeletonDownloadView(UtilityView, grok.View):
1023    grok.context(IDataCenter)
1024    grok.name('skeleton')
1025    grok.require('waeup.manageDataCenter')
1026
1027    def update(self, processorname=None):
1028        self.processorname = self.request.form['name']
1029        self.filename = ('%s_000.csv' %
1030            self.processorname.replace('processor','import'))
1031        return
1032
1033    def render(self):
[11949]1034        #ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9032]1035        #self.context.logger.info(
1036        #    '%s - skeleton downloaded: %s' % (ob_class, self.filename))
1037        self.response.setHeader(
1038            'Content-Type', 'text/csv; charset=UTF-8')
1039        self.response.setHeader(
1040            'Content-Disposition:', 'attachment; filename="%s' % self.filename)
1041        processor = getUtility(IBatchProcessor, name=self.processorname)
1042        csv_data = processor.get_csv_skeleton()
1043        return csv_data
1044
[11949]1045class DatacenterImportStep1(IkobaPage):
[4858]1046    """Manual import step 1: choose file
1047    """
1048    grok.context(IDataCenter)
1049    grok.name('import1')
1050    grok.template('datacenterimport1page')
[9590]1051    grok.require('waeup.manageDataCenter')
[7700]1052    label = _(u'Process CSV file')
[4858]1053    pnav = 0
[11254]1054    cancel_button =_(u'Back to Data Center')
[4858]1055
1056    def getFiles(self):
[9023]1057        files = self.context.getPendingFiles(sort='date')
[4858]1058        for file in files:
1059            name = file.name
1060            if not name.endswith('.csv') and not name.endswith('.pending'):
1061                continue
1062            yield file
[6146]1063
[4858]1064    def update(self, filename=None, select=None, cancel=None):
1065        if cancel is not None:
1066            self.redirect(self.url(self.context))
1067            return
1068        if select is not None:
1069            # A filename was selected
[11949]1070            session = ISession(self.request)['waeup.ikoba']
[4858]1071            session['import_filename'] = select
1072            self.redirect(self.url(self.context, '@@import2'))
1073
[11949]1074class DatacenterImportStep2(IkobaPage):
[7933]1075    """Manual import step 2: choose processor
[4858]1076    """
1077    grok.context(IDataCenter)
1078    grok.name('import2')
1079    grok.template('datacenterimport2page')
[9590]1080    grok.require('waeup.manageDataCenter')
[7700]1081    label = _(u'Process CSV file')
[4858]1082    pnav = 0
[7705]1083    cancel_button =_(u'Cancel')
1084    back_button =_(u'Back to step 1')
1085    proceed_button =_(u'Proceed to step 3')
[4858]1086
1087    filename = None
1088    mode = 'create'
1089    importer = None
[5000]1090    mode_locked = False
[4858]1091
1092    def getPreviewHeader(self):
1093        """Get the header fields of attached CSV file.
1094        """
1095        reader = csv.reader(open(self.fullpath, 'rb'))
1096        return reader.next()
[6146]1097
[8651]1098    def getPreviewTable(self):
[9322]1099        return getPreviewTable(self, 3)
[8651]1100
[4858]1101    def getImporters(self):
1102        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
[7954]1103        importers = sorted(
1104            [dict(title=x.name, name=x.util_name) for x in importers])
[4858]1105        return importers
[5000]1106
1107    def getModeFromFilename(self, filename):
1108        """Lookup filename or path and return included mode name or None.
1109        """
1110        if not filename.endswith('.pending.csv'):
1111            return None
1112        base = os.path.basename(filename)
1113        parts = base.rsplit('.', 3)
1114        if len(parts) != 4:
1115            return None
1116        if parts[1] not in ['create', 'update', 'remove']:
1117            return None
1118        return parts[1]
1119
[6828]1120    def getWarnings(self):
1121        import sys
1122        result = []
1123        try:
1124            headerfields = self.getPreviewHeader()
1125            headerfields_clean = list(set(headerfields))
1126            if len(headerfields) > len(headerfields_clean):
1127                result.append(
[7700]1128                    _("Double headers: each column name may only appear once. "))
[6828]1129        except:
1130            fatal = '%s' % sys.exc_info()[1]
1131            result.append(fatal)
1132        if result:
1133            warnings = ""
1134            for line in result:
1135                warnings += line + '<br />'
[7700]1136            warnings += _('Replace imported file!')
[6828]1137            return warnings
1138        return False
1139
[4858]1140    def update(self, mode=None, importer=None,
1141               back1=None, cancel=None, proceed=None):
[11949]1142        session = ISession(self.request)['waeup.ikoba']
[4858]1143        self.filename = session.get('import_filename', None)
[6146]1144
[4858]1145        if self.filename is None or back1 is not None:
1146            self.redirect(self.url(self.context, '@@import1'))
1147            return
1148        if cancel is not None:
[11254]1149            self.flash(_('Import aborted.'), type='warning')
[4858]1150            self.redirect(self.url(self.context))
1151            return
1152        self.mode = mode or session.get('import_mode', self.mode)
[5000]1153        filename_mode = self.getModeFromFilename(self.filename)
1154        if filename_mode is not None:
1155            self.mode = filename_mode
1156            self.mode_locked = True
[4858]1157        self.importer = importer or session.get('import_importer', None)
[6837]1158        session['import_importer'] = self.importer
1159        if self.importer and 'update' in self.importer:
1160            if self.mode != 'update':
[11254]1161                self.flash(_('Update mode only!'), type='warning')
[6837]1162                self.mode_locked = True
1163                self.mode = 'update'
1164                proceed = None
[4858]1165        session['import_mode'] = self.mode
1166        if proceed is not None:
1167            self.redirect(self.url(self.context, '@@import3'))
1168            return
1169        self.fullpath = os.path.join(self.context.storage, self.filename)
[6828]1170        warnings = self.getWarnings()
1171        if not warnings:
1172            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1173        else:
1174            self.reader = ()
[11254]1175            self.flash(warnings, type='warning')
[4858]1176
[11949]1177class DatacenterImportStep3(IkobaPage):
[4858]1178    """Manual import step 3: modify header
1179    """
1180    grok.context(IDataCenter)
1181    grok.name('import3')
1182    grok.template('datacenterimport3page')
[9590]1183    grok.require('waeup.manageDataCenter')
[7700]1184    label = _(u'Process CSV file')
[4858]1185    pnav = 0
[7705]1186    cancel_button =_(u'Cancel')
1187    reset_button =_(u'Reset')
1188    update_button =_(u'Set headerfields')
1189    back_button =_(u'Back to step 2')
1190    proceed_button =_(u'Perform import')
[4858]1191
1192    filename = None
1193    mode = None
1194    importername = None
[6146]1195
[4858]1196    @property
1197    def nextstep(self):
1198        return self.url(self.context, '@@import4')
1199
1200    def getPreviewHeader(self):
1201        """Get the header fields of attached CSV file.
1202        """
1203        reader = csv.reader(open(self.fullpath, 'rb'))
1204        return reader.next()
[6146]1205
[8651]1206    def getPreviewTable(self):
1207        """Get transposed table with 1 sample record.
1208
1209        The first column contains the headers.
[4858]1210        """
[8651]1211        if not self.reader:
1212            return
[8783]1213        headers = self.getPreviewHeader()
[4858]1214        num = 0
[8651]1215        data = []
1216        for line in self.reader:
1217            if num > 0:
[4858]1218                break
1219            num += 1
[8651]1220            data.append(line)
1221        result = []
[8783]1222        field_num = 0
1223        for name in headers:
[8651]1224            result_line = []
[8783]1225            result_line.append(field_num)
1226            field_num += 1
[8651]1227            for d in data:
1228                result_line.append(d[name])
1229            result.append(result_line)
[4858]1230        return result
1231
1232    def getPossibleHeaders(self):
1233        """Get the possible headers.
1234
1235        The headers are described as dicts {value:internal_name,
1236        title:displayed_name}
1237        """
1238        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
1239        headers = self.importer.getHeaders()
1240        result.extend([dict(title=x, value=x) for x in headers])
1241        return result
1242
1243    def getWarnings(self):
1244        import sys
1245        result = []
1246        try:
1247            self.importer.checkHeaders(self.headerfields, mode=self.mode)
1248        except:
1249            fatal = '%s' % sys.exc_info()[1]
1250            result.append(fatal)
[6828]1251        if result:
1252            warnings = ""
1253            for line in result:
1254                warnings += line + '<br />'
[7700]1255            warnings += _('Edit headers or replace imported file!')
[6828]1256            return warnings
1257        return False
[6146]1258
[4858]1259    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
[11949]1260        session = ISession(self.request)['waeup.ikoba']
[4858]1261        self.filename = session.get('import_filename', None)
1262        self.mode = session.get('import_mode', None)
1263        self.importername = session.get('import_importer', None)
[6146]1264
[4858]1265        if None in (self.filename, self.mode, self.importername):
1266            self.redirect(self.url(self.context, '@@import2'))
1267            return
1268        if back2 is not None:
1269            self.redirect(self.url(self.context ,'@@import2'))
1270            return
1271        if cancel is not None:
[11254]1272            self.flash(_('Import aborted.'), type='warning')
[4858]1273            self.redirect(self.url(self.context))
1274            return
1275
1276        self.fullpath = os.path.join(self.context.storage, self.filename)
1277        self.headerfields = headerfield or self.getPreviewHeader()
1278        session['import_headerfields'] = self.headerfields
1279
1280        if proceed is not None:
1281            self.redirect(self.url(self.context, '@@import4'))
1282            return
1283        self.importer = getUtility(IBatchProcessor, name=self.importername)
1284        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
[6828]1285        warnings = self.getWarnings()
1286        if warnings:
[11254]1287            self.flash(warnings, type='warning')
[4858]1288
[11949]1289class DatacenterImportStep4(IkobaPage):
[4858]1290    """Manual import step 4: do actual import
1291    """
1292    grok.context(IDataCenter)
1293    grok.name('import4')
1294    grok.template('datacenterimport4page')
[8367]1295    grok.require('waeup.importData')
[7700]1296    label = _(u'Process CSV file')
[4858]1297    pnav = 0
[10099]1298    back_button =_(u'Process next')
[4858]1299
1300    filename = None
1301    mode = None
1302    importername = None
1303    headerfields = None
1304    warnnum = None
1305
1306    def update(self, back=None, finish=None, showlog=None):
1307        if finish is not None:
[10099]1308            self.redirect(self.url(self.context, '@@import1'))
[4858]1309            return
[11949]1310        session = ISession(self.request)['waeup.ikoba']
[4858]1311        self.filename = session.get('import_filename', None)
1312        self.mode = session.get('import_mode', None)
1313        self.importername = session.get('import_importer', None)
[9368]1314        # If the import file contains only one column
1315        # the import_headerfields attribute is a string.
[9369]1316        ihf = session.get('import_headerfields', None)
1317        if not isinstance(ihf, list):
1318            self.headerfields = ihf.split()
1319        else:
1320            self.headerfields = ihf
[6146]1321
[4858]1322        if None in (self.filename, self.mode, self.importername,
1323                    self.headerfields):
1324            self.redirect(self.url(self.context, '@@import3'))
1325            return
1326
1327        if showlog is not None:
[4909]1328            logfilename = "datacenter.log"
[4858]1329            session['logname'] = logfilename
1330            self.redirect(self.url(self.context, '@@show'))
1331            return
[6146]1332
[4858]1333        self.fullpath = os.path.join(self.context.storage, self.filename)
1334        self.importer = getUtility(IBatchProcessor, name=self.importername)
[4898]1335
1336        # Perform batch processing...
1337        # XXX: This might be better placed in datacenter module.
1338        (linenum, self.warn_num,
1339         fin_path, pending_path) = self.importer.doImport(
[4858]1340            self.fullpath, self.headerfields, self.mode,
[4887]1341            self.request.principal.id, logger=self.context.logger)
[4898]1342        # Put result files in desired locations...
1343        self.context.distProcessedFiles(
[4997]1344            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1345            self.mode)
[4898]1346
[4858]1347        if self.warn_num:
[7700]1348            self.flash(_('Processing of ${a} rows failed.',
[11254]1349                mapping = {'a':self.warn_num}), type='warning')
[7700]1350        self.flash(_('Successfully processed ${a} rows.',
1351            mapping = {'a':linenum - self.warn_num}))
[4858]1352
[11949]1353class DatacenterLogsOverview(IkobaPage):
[4858]1354    grok.context(IDataCenter)
1355    grok.name('logs')
1356    grok.template('datacenterlogspage')
[8367]1357    grok.require('waeup.manageDataCenter')
[7700]1358    label = _(u'Show logfiles')
[4858]1359    pnav = 0
[7705]1360    back_button = _(u'Back to Data Center')
1361    show_button = _(u'Show')
[4858]1362
[8529]1363    def update(self, back=None):
[4858]1364        if back is not None:
1365            self.redirect(self.url(self.context))
1366            return
1367        self.files = self.context.getLogFiles()
1368
[11949]1369class DatacenterLogsFileview(IkobaPage):
[4858]1370    grok.context(IDataCenter)
1371    grok.name('show')
1372    grok.template('datacenterlogsshowfilepage')
[8367]1373    grok.require('waeup.manageDataCenter')
[7700]1374    title = _(u'Data Center')
[4858]1375    pnav = 0
[7749]1376    search_button = _('Search')
[11254]1377    back_button = _('Back to Data Center')
[8529]1378    placeholder = _('Enter a regular expression here...')
[4858]1379
[7465]1380    def label(self):
1381        return "Logfile %s" % self.filename
1382
[8529]1383    def update(self, back=None, query=None, logname=None):
[7750]1384        if os.name != 'posix':
1385            self.flash(
1386                _('Log files can only be searched ' +
[11254]1387                  'on Unix-based operating systems.'), type='danger')
[7750]1388            self.redirect(self.url(self.context, '@@logs'))
1389            return
[4858]1390        if back is not None or logname is None:
1391            self.redirect(self.url(self.context, '@@logs'))
1392            return
1393        self.filename = logname
[8529]1394        self.query = query
[11947]1395        if not query:
[7749]1396            return
[8515]1397        try:
[8529]1398            self.result = ''.join(
[8515]1399                self.context.queryLogfiles(logname, query))
1400        except ValueError:
[11254]1401            self.flash(_('Invalid search expression.'), type='danger')
[8529]1402            return
1403        if not self.result:
[11254]1404            self.flash(_('No search results found.'), type='warning')
[8515]1405        return
[7749]1406
[11949]1407class DatacenterSettings(IkobaPage):
[4679]1408    grok.context(IDataCenter)
1409    grok.name('manage')
1410    grok.template('datacentermanagepage')
[8739]1411    grok.require('waeup.managePortal')
[7700]1412    label = _('Edit data center settings')
[4679]1413    pnav = 0
[7705]1414    save_button =_(u'Save')
1415    reset_button =_(u'Reset')
[11254]1416    cancel_button =_(u'Back to Data Center')
[4677]1417
[4679]1418    def update(self, newpath=None, move=False, overwrite=False,
1419               save=None, cancel=None):
1420        if move:
1421            move = True
1422        if overwrite:
1423            overwrite = True
1424        if newpath is None:
1425            return
1426        if cancel is not None:
1427            self.redirect(self.url(self.context))
1428            return
1429        try:
1430            not_copied = self.context.setStoragePath(newpath, move=move)
1431            for name in not_copied:
[7738]1432                self.flash(_('File already existed (not copied): ${a}',
[11254]1433                    mapping = {'a':name}), type='danger')
[6612]1434        except:
[12192]1435            self.flash(_('Given storage path cannot be used: ${a}',
[11254]1436                        mapping = {'a':sys.exc_info()[1]}), type='danger')
[4679]1437            return
1438        if newpath:
[7700]1439            self.flash(_('New storage path succefully set.'))
[11949]1440            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8739]1441            self.context.logger.info(
1442                '%s - storage path set: %s' % (ob_class, newpath))
[4679]1443            self.redirect(self.url(self.context))
1444        return
1445
[11949]1446class ExportCSVPage(IkobaPage):
[7908]1447    grok.context(IDataCenter)
1448    grok.name('export')
1449    grok.template('datacenterexportpage')
[10244]1450    grok.require('waeup.exportData')
[7974]1451    label = _('Download portal data as CSV file')
[7908]1452    pnav = 0
[9217]1453    export_button = _(u'Create CSV file')
[11254]1454    cancel_button =_(u'Back to Data Center')
[7908]1455
1456    def getExporters(self):
1457        utils = getUtilitiesFor(ICSVExporter)
1458        title_name_tuples = [
[11947]1459            (util.title, name) for name, util in utils]
[7908]1460        return sorted(title_name_tuples)
1461
[11254]1462    def update(self, CREATE=None, DISCARD=None, exporter=None,
1463               job_id=None, CANCEL=None):
1464        if CANCEL is not None:
1465            self.redirect(self.url(self.context))
1466            return
[9822]1467        if CREATE:
1468            job_id = self.context.start_export_job(
1469                exporter, self.request.principal.id)
[11949]1470            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1471            self.context.logger.info(
1472                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
[9822]1473        if DISCARD and job_id:
1474            entry = self.context.entry_from_job_id(job_id)
1475            self.context.delete_export_entry(entry)
[11949]1476            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1477            self.context.logger.info(
1478                '%s - discarded: job_id=%s' % (ob_class, job_id))
[9822]1479            self.flash(_('Discarded export') + ' %s' % job_id)
1480        self.entries = doll_up(self, user=None)
[7908]1481        return
1482
1483class ExportCSVView(grok.View):
1484    grok.context(IDataCenter)
[9822]1485    grok.name('download_export')
[10244]1486    grok.require('waeup.exportData')
[7908]1487
[9326]1488    def render(self, job_id=None):
[9217]1489        manager = getUtility(IJobManager)
1490        job = manager.get(job_id)
1491        if job is None:
[7908]1492            return
[9217]1493        if hasattr(job.result, 'traceback'):
1494            # XXX: Some error happened. Do something more approriate here...
1495            return
1496        path = job.result
1497        if not os.path.exists(path):
1498            # XXX: Do something more appropriate here...
1499            return
1500        result = open(path, 'rb').read()
1501        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
1502        filename = "%s_%s" % (acronym, os.path.basename(path))
[9837]1503        filename = filename.replace('.csv', '_%s.csv' % job_id)
[7908]1504        self.response.setHeader(
1505            'Content-Type', 'text/csv; charset=UTF-8')
[9032]1506        self.response.setHeader(
[9217]1507            'Content-Disposition', 'attachment; filename="%s' % filename)
1508        # remove job and running_exports entry from context
[9822]1509        #self.context.delete_export_entry(
1510        #    self.context.entry_from_job_id(job_id))
[11949]1511        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[9836]1512        self.context.logger.info(
1513            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
[9217]1514        return result
[7908]1515
[11949]1516class ChangePasswordRequestPage(IkobaForm):
[8346]1517    """Captcha'd page for all kind of users to request a password change.
1518    """
[11954]1519    grok.context(ICompany)
[8777]1520    grok.name('changepw')
[8346]1521    grok.require('waeup.Anonymous')
[8777]1522    grok.template('changepw')
[8346]1523    label = _('Send me a new password')
1524    form_fields = grok.AutoFields(IChangePassword)
1525
1526    def update(self):
1527        # Handle captcha
1528        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1529        self.captcha_result = self.captcha.verify(self.request)
1530        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1531        return
1532
1533    def _searchUser(self, identifier, email):
[11947]1534        # Search customer
[11977]1535        cat = queryUtility(ICatalog, name='customers_catalog')
1536        results = cat.searchResults(
1537            reg_number=(identifier, identifier),
1538            email=(email,email))
1539        for result in results:
1540            if result.customer_id == identifier \
1541                or result.reg_number == identifier:
1542                return result
[8346]1543        # Search portal user
1544        user = grok.getSite()['users'].get(identifier, None)
1545        if user is not None and user.email == email:
1546            return user
1547        return None
1548
[9172]1549    @action(_('Send login credentials to email address'), style='primary')
[8346]1550    def request(self, **data):
1551        if not self.captcha_result.is_valid:
1552            # Captcha will display error messages automatically.
1553            # No need to flash something.
1554            return
[11947]1555        # Search customer
[8346]1556        identifier = data['identifier']
1557        email = data['email']
1558        user = self._searchUser(identifier, email)
1559        if user is None:
[11254]1560            self.flash(_('No record found.'), type='warning')
[8346]1561            return
1562        # Change password
[11949]1563        ikoba_utils = getUtility(IIkobaUtils)
1564        password = ikoba_utils.genPassword()
[8858]1565        mandate = PasswordMandate()
1566        mandate.params['password'] = password
1567        mandate.params['user'] = user
1568        site = grok.getSite()
1569        site['mandates'].addMandate(mandate)
1570        # Send email with credentials
1571        args = {'mandate_id':mandate.mandate_id}
1572        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1573        url_info = u'Confirmation link: %s' % mandate_url
1574        msg = _('You have successfully requested a password for the')
[11949]1575        success = ikoba_utils.sendCredentials(
[8858]1576            IUserAccount(user),password,url_info,msg)
[8346]1577        if success:
1578            self.flash(_('An email with your user name and password ' +
1579                'has been sent to ${a}.', mapping = {'a':email}))
1580        else:
[11254]1581            self.flash(_('An smtp server error occurred.'), type='danger')
[11949]1582        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
[8740]1583        self.context.logger.info(
1584            '%s - %s - %s' % (ob_class, data['identifier'], data['email']))
[8515]1585        return
Note: See TracBrowser for help on using the repository browser.