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

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

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

  • Property svn:keywords set to Id
File size: 57.4 KB
Line 
1## $Id: pages.py 12525 2015-01-30 11:24:26Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18""" Viewing components for Ikoba objects.
19"""
20# XXX: All csv ops should move to a dedicated module soon
21import unicodecsv as csv
22import grok
23import os
24import re
25import sys
26from datetime import datetime, timedelta
27from urllib import urlencode
28from hurry.query import Eq, Text
29from hurry.query.query import Query
30from zope import schema
31from zope.i18n import translate
32from zope.authentication.interfaces import (
33    IAuthentication, IUnauthenticatedPrincipal, ILogout)
34from zope.catalog.interfaces import ICatalog
35from zope.component import (
36    getUtility, queryUtility, createObject, getAllUtilitiesRegisteredFor,
37    getUtilitiesFor,
38    )
39from zope.event import notify
40from zope.security import checkPermission
41from zope.securitypolicy.interfaces import IPrincipalRoleManager
42from zope.session.interfaces import ISession
43from zope.password.interfaces import IPasswordManager
44from waeup.ikoba.utils.helpers import html2dict
45from waeup.ikoba.browser.layout import (
46    IkobaPage, IkobaForm, IkobaEditFormPage, IkobaAddFormPage,
47    IkobaDisplayFormPage, NullValidator)
48from waeup.ikoba.browser.interfaces import (
49    ICompany, ICaptchaManager, IChangePassword)
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,
55    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
56    IJobManager,
57    IPasswordValidator, IContactForm, IIkobaUtils, ICSVExporter)
58from waeup.ikoba.permissions import (
59    get_users_with_local_roles, get_all_roles, get_all_users,
60    get_users_with_role)
61
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
66
67FORBIDDEN_CHARACTERS = (160,)
68
69grok.context(IIkobaObject)
70grok.templatedir('templates')
71
72def add_local_role(view, tab, **data):
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:
76        view.flash('No user selected.', type='danger')
77        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
78        return
79    role_manager = IPrincipalRoleManager(view.context)
80    role_manager.assignRoleToPrincipal(localrole, user)
81    notify(LocalRoleSetEvent(view.context, localrole, user, granted=True))
82    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
83    grok.getSite().logger.info(
84        '%s - added: %s|%s' % (ob_class, user, localrole))
85    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
86    return
87
88def del_local_roles(view, tab, **data):
89    child_ids = view.request.form.get('role_id', None)
90    if child_ids is None:
91        view.flash(_('No local role selected.'), type='danger')
92        view.redirect(view.url(view.context, '@@manage')+'#tab%s' % tab)
93        return
94    if not isinstance(child_ids, list):
95        child_ids = [child_ids]
96    deleted = []
97    role_manager = IPrincipalRoleManager(view.context)
98    for child_id in child_ids:
99        localrole = child_id.split('|')[1]
100        user_name = child_id.split('|')[0]
101        try:
102            role_manager.unsetRoleForPrincipal(localrole, user_name)
103            notify(LocalRoleSetEvent(
104                    view.context, localrole, user_name, granted=False))
105            deleted.append(child_id)
106        except:
107            view.flash('Could not remove %s: %s: %s' % (
108                    child_id, sys.exc_info()[0], sys.exc_info()[1]),
109                    type='danger')
110    if len(deleted):
111        view.flash(
112            _('Local role successfully removed: ${a}',
113            mapping = {'a':', '.join(deleted)}))
114        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
115        grok.getSite().logger.info(
116            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
117    view.redirect(view.url(view.context, u'@@manage')+'#tab%s' % tab)
118    return
119
120def delSubobjects(view, redirect, tab=None, subcontainer=None):
121    form = view.request.form
122    if 'val_id' in form:
123        child_id = form['val_id']
124    else:
125        view.flash(_('No item selected.'), type='danger')
126        if tab:
127            view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
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' % (
144                    id, sys.exc_info()[0], sys.exc_info()[1]), type='danger')
145    if len(deleted):
146        view.flash(_('Successfully removed: ${a}',
147            mapping = {'a': ', '.join(deleted)}))
148        ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
149        grok.getSite().logger.info(
150            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
151    if tab:
152        view.redirect(view.url(view.context, redirect)+'#tab%s' % tab)
153    else:
154        view.redirect(view.url(view.context, redirect))
155    return
156
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
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.'))
189    ob_class = view.__implemented__.__name__.replace('waeup.ikoba.','')
190    if fields_string:
191        grok.getSite().logger.info('%s - %s - saved: %s' % (ob_class, view.context.__name__, fields_string))
192    return
193
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')
204        args = ', '.join(['%s=%s' % (item[0], item[1])
205            for item in job.kwargs.items()])
206        status = job.finished and 'ready' or 'running'
207        status = job.failed and 'FAILED' or status
208        start_time = getattr(job, 'begin_after', None)
209        time_delta = None
210        if start_time:
211            tz = getUtility(IIkobaUtils).tzinfo
212            time_delta = datetime.now(tz) - start_time
213            start_time = start_time.astimezone(tz).strftime(
214                "%Y-%m-%d %H:%M:%S %Z")
215        download_url = view.url(view.context, 'download_export',
216                                data=dict(job_id=job_id))
217        show_download_button = job.finished and not \
218                               job.failed and time_delta and \
219                               time_delta.days < 1
220        new_entry = dict(
221            job=job_id,
222            creator=user_id,
223            args=args,
224            exporter=exporter_title,
225            status=status,
226            start_time=start_time,
227            download_url=download_url,
228            show_download_button = show_download_button,
229            show_refresh_button = not job.finished,
230            show_discard_button = job.finished,)
231        entries.append(new_entry)
232    return entries
233
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
249#
250# Login/logout and language switch pages...
251#
252
253class LoginPage(IkobaPage):
254    """A login page, available for all objects.
255    """
256    grok.name('login')
257    grok.context(IIkobaObject)
258    grok.require('waeup.Public')
259    label = _(u'Login')
260    camefrom = None
261    login_button = label
262
263    def _comment(self, customer):
264        return getattr(customer, 'suspended_comment', None)
265
266    def update(self, SUBMIT=None, camefrom=None):
267        self.camefrom = camefrom
268        if SUBMIT is not None:
269            if self.request.principal.id != 'zope.anybody':
270                self.flash(_('You logged in.'))
271                if self.request.principal.user_type == 'customer':
272                    customer = grok.getSite()['customers'][
273                        self.request.principal.id]
274                    rel_link = '/customers/%s' % self.request.principal.id
275                    # Maybe we need this in Ikoba too
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')
282                    self.redirect(self.application_url() + rel_link)
283                    return
284                if not self.camefrom:
285                    self.redirect(self.application_url() + '/index')
286                    return
287                self.redirect(self.camefrom)
288                return
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
318            self.flash(_('You entered invalid credentials.'), type='danger')
319            return
320
321
322class LogoutPage(IkobaPage):
323    """A logout page. Calling this page will log the current user out.
324    """
325    grok.context(IIkobaObject)
326    grok.require('waeup.Public')
327    grok.name('logout')
328
329    def update(self):
330        if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
331            auth = getUtility(IAuthentication)
332            ILogout(auth).logout(self.request)
333            self.flash(_("You have been logged out. Thanks for using WAeUP Ikoba!"))
334        self.redirect(self.application_url() + '/index')
335        return
336
337
338class LanguageChangePage(IkobaPage):
339    """ Language switch
340    """
341    grok.context(IIkobaObject)
342    grok.name('change_language')
343    grok.require('waeup.Public')
344
345    def update(self, lang='en', view_name='@@index'):
346        self.response.setCookie('ikoba.language', lang, path='/')
347        self.redirect(self.url(self.context, view_name))
348        return
349
350    def render(self):
351        return
352
353#
354# Contact form...
355#
356
357class ContactAdminForm(IkobaForm):
358    grok.name('contactadmin')
359    #grok.context(ICompany)
360    grok.template('contactform')
361    grok.require('waeup.Authenticated')
362    pnav = 2
363    form_fields = grok.AutoFields(IContactForm).select('body')
364
365    def update(self):
366        super(ContactAdminForm, self).update()
367        self.form_fields.get('body').field.default = None
368        return
369
370    @property
371    def config(self):
372        return grok.getSite()['configuration']
373
374    def label(self):
375        return _(u'Contact ${a}', mapping = {'a': self.config.name_admin})
376
377    @property
378    def get_user_account(self):
379        return get_user_account(self.request)
380
381    @action(_('Send message now'), style='primary')
382    def send(self, *args, **data):
383        fullname = self.request.principal.title
384        try:
385            email = self.request.principal.email
386        except AttributeError:
387            email = self.config.email_admin
388        username = self.request.principal.id
389        usertype = getattr(self.request.principal,
390                           'user_type', 'system').title()
391        ikoba_utils = getUtility(IIkobaUtils)
392        success = ikoba_utils.sendContactForm(
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)
397        # Success is always True if sendContactForm didn't fail.
398        # TODO: Catch exceptions.
399        if success:
400            self.flash(_('Your message has been sent.'))
401        return
402
403class EnquiriesForm(ContactAdminForm):
404    """Captcha'd page to let anonymous send emails to the administrator.
405    """
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
412    def update(self):
413        super(EnquiriesForm, self).update()
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)
418        return
419
420    @action(_('Send now'), style='primary')
421    def send(self, *args, **data):
422        if not self.captcha_result.is_valid:
423            # Captcha will display error messages automatically.
424            # No need to flash something.
425            return
426        ikoba_utils = getUtility(IIkobaUtils)
427        success = ikoba_utils.sendContactForm(
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)
432        if success:
433            self.flash(_('Your message has been sent.'))
434        else:
435            self.flash(_('A smtp server error occurred.'), type='danger')
436        return
437
438#
439# Company related pages...
440#
441
442class CompanyPage(IkobaDisplayFormPage):
443    """ The main company page.
444    """
445    grok.require('waeup.Public')
446    grok.name('index')
447    grok.context(ICompany)
448    pnav = 0
449    label = ''
450
451    @property
452    def frontpage(self):
453        lang = self.request.cookies.get('ikoba.language')
454        html = self.context['configuration'].frontpage_dict.get(lang,'')
455        if html =='':
456            portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
457            html = self.context[
458                'configuration'].frontpage_dict.get(portal_language,'')
459        if html =='':
460            return _(u'<h1>Welcome to WAeUP.Ikoba</h1>')
461        else:
462            return html
463
464class AdministrationPage(IkobaPage):
465    """ The administration overview page.
466    """
467    grok.name('administration')
468    grok.context(ICompany)
469    grok.require('waeup.managePortal')
470    label = _(u'Administration')
471    pnav = 0
472
473class RSS20Feed(grok.View):
474    """An RSS 2.0 feed.
475    """
476    grok.name('feed.rss')
477    grok.context(ICompany)
478    grok.require('waeup.Public')
479    grok.template('companyrss20feed')
480
481    name = 'General news feed'
482    description = 'waeup.ikoba now supports RSS 2.0 feeds :-)'
483    language = None
484    date = None
485    buildDate = None
486    editor = None
487    webmaster = None
488
489    @property
490    def title(self):
491        return getattr(grok.getSite(), 'name', u'Sample Company')
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 ()
506
507#
508# User container pages...
509#
510
511class UsersContainerPage(IkobaPage):
512    """Overview page for all local users.
513    """
514    grok.require('waeup.manageUsers')
515    grok.context(IUsersContainer)
516    grok.name('index')
517    label = _('Officers')
518    manage_button = _(u'Manage')
519    delete_button = _(u'Remove')
520
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')
524        if delete is not None and userid is not None:
525            self.context.delUser(userid)
526            self.flash(_('User account ${a} successfully deleted.',
527                mapping = {'a':  userid}))
528            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
529            self.context.__parent__.logger.info(
530                '%s - removed: %s' % (ob_class, userid))
531
532    def getLocalRoles(self, account):
533        local_roles = account.getLocalRoles()
534        local_roles_string = ''
535        site_url = self.url(grok.getSite())
536        for local_role in local_roles.keys():
537            role_title = getattr(
538                dict(get_all_roles()).get(local_role, None), 'title', None)
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
546
547    def getSiteRoles(self, account):
548        site_roles = account.roles
549        site_roles_string = ''
550        for site_role in site_roles:
551            role_title = dict(get_all_roles())[site_role].title
552            site_roles_string += '%s <br />' % role_title
553        return site_roles_string
554
555class AddUserFormPage(IkobaAddFormPage):
556    """Add a user account.
557    """
558    grok.require('waeup.manageUsers')
559    grok.context(IUsersContainer)
560    grok.name('add')
561    grok.template('usereditformpage')
562    form_fields = grok.AutoFields(IUserAccount)
563    label = _('Add user')
564
565    @action(_('Add user'), style='primary')
566    def addUser(self, **data):
567        name = data['name']
568        title = data['title']
569        email = data['email']
570        phone = data['phone']
571        description = data['description']
572        #password = data['password']
573        roles = data['roles']
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:
581                self.flash( ' '.join(errors), type='danger')
582                return
583        try:
584            self.context.addUser(name, password, title=title, email=email,
585                                 phone=phone, description=description,
586                                 roles=roles)
587            self.flash(_('User account ${a} successfully added.',
588                mapping = {'a': name}))
589            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
590            self.context.__parent__.logger.info(
591                '%s - added: %s' % (ob_class, name))
592        except KeyError:
593            self.status = self.flash('The userid chosen already exists '
594                                  'in the database.', type='danger')
595            return
596        self.redirect(self.url(self.context))
597
598class UserManageFormPage(IkobaEditFormPage):
599    """Manage a user account.
600    """
601    grok.context(IUserAccount)
602    grok.name('manage')
603    grok.template('usereditformpage')
604    grok.require('waeup.manageUsers')
605    form_fields = grok.AutoFields(IUserAccount).omit('name')
606
607    def label(self):
608        return _("Edit user ${a}", mapping = {'a':self.context.__name__})
609
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
614        return
615
616    @action(_('Save'), style='primary')
617    def save(self, **data):
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:
625                self.flash( ' '.join(errors), type='danger')
626                return
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 = []
632        if password:
633            # Now we know that the form has no errors and can set password ...
634            self.context.setPassword(password)
635            changed_fields.append('password')
636        fields_string = ' + '.join(changed_fields)
637        if fields_string:
638            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
639            self.context.__parent__.logger.info(
640                '%s - %s edited: %s' % (
641                ob_class, self.context.name, fields_string))
642        self.flash(_('User settings have been saved.'))
643        return
644
645    @action(_('Cancel'), validator=NullValidator)
646    def cancel(self, **data):
647        self.redirect(self.url(self.context.__parent__))
648        return
649
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
658    def label(self):
659        return _(u'Send message to ${a}', mapping = {'a':self.context.title})
660
661    @action(_('Send message now'), style='primary')
662    def send(self, *args, **data):
663        try:
664            email = self.request.principal.email
665        except AttributeError:
666            email = self.config.email_admin
667        usertype = getattr(self.request.principal,
668                           'user_type', 'system').title()
669        ikoba_utils = getUtility(IIkobaUtils)
670        success = ikoba_utils.sendContactForm(
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)
675        # Success is always True if sendContactForm didn't fail.
676        # TODO: Catch exceptions.
677        if success:
678            self.flash(_('Your message has been sent.'))
679        return
680
681class UserEditFormPage(UserManageFormPage):
682    """Edit a user account by user
683    """
684    grok.name('index')
685    grok.require('waeup.editUser')
686    form_fields = grok.AutoFields(IUserAccount).omit(
687        'name', 'description', 'roles')
688    label = _(u"My Preferences")
689
690    def setUpWidgets(self, ignore_request=False):
691        super(UserManageFormPage,self).setUpWidgets(ignore_request)
692        self.widgets['title'].displayWidth = 30
693
694class MyRolesPage(IkobaPage):
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')
701    label = _(u"My Roles")
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:
708            role_title = dict(get_all_roles())[local_role].title
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:
717            role_title = dict(get_all_roles())[site_role].title
718            site_roles_userfriendly.append(role_title)
719        return site_roles_userfriendly
720
721#
722# Configuration pages...
723#
724
725class ConfigurationContainerManageFormPage(IkobaEditFormPage):
726    """Manage page of the configuration container. We always use the
727    manage page in the UI not the view page, thus we use the index name here.
728    """
729    grok.require('waeup.managePortalConfiguration')
730    grok.name('index')
731    grok.context(IConfigurationContainer)
732    pnav = 0
733    label = _(u'Edit portal configuration')
734    form_fields = grok.AutoFields(IConfigurationContainer).omit(
735        'frontpage_dict')
736
737    @action(_('Save'), style='primary')
738    def save(self, **data):
739        msave(self, **data)
740        frontpage = getattr(self.context, 'frontpage', None)
741        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
742        self.context.frontpage_dict = html2dict(frontpage, portal_language)
743        return
744
745    @action(_('Update plugins'),
746              tooltip=_('For experts only!'),
747              warning=_('Plugins may only be updated after software upgrades. '
748                        'Are you really sure?'),
749              validator=NullValidator)
750    def updatePlugins(self, **data):
751        grok.getSite().updatePlugins()
752        self.flash(_('Plugins were updated. See log file for details.'))
753        return
754
755#
756# Datacenter pages...
757#
758
759class DatacenterPage(IkobaEditFormPage):
760    grok.context(IDataCenter)
761    grok.name('index')
762    grok.require('waeup.manageDataCenter')
763    label = _(u'Data Center')
764    pnav = 0
765
766    @jsaction(_('Remove selected'))
767    def delFiles(self, **data):
768        form = self.request.form
769        if 'val_id' in form:
770            child_id = form['val_id']
771        else:
772            self.flash(_('No item selected.'), type='danger')
773            return
774        if not isinstance(child_id, list):
775            child_id = [child_id]
776        deleted = []
777        for id in child_id:
778            fullpath = os.path.join(self.context.storage, id)
779            try:
780                os.remove(fullpath)
781                deleted.append(id)
782            except OSError:
783                self.flash(_('OSError: The file could not be deleted.'),
784                           type='danger')
785                return
786        if len(deleted):
787            self.flash(_('Successfully deleted: ${a}',
788                mapping = {'a': ', '.join(deleted)}))
789            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
790            self.context.logger.info(
791                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
792        return
793
794class DatacenterFinishedPage(IkobaEditFormPage):
795    grok.context(IDataCenter)
796    grok.name('processed')
797    grok.require('waeup.manageDataCenter')
798    label = _(u'Processed Files')
799    pnav = 0
800    cancel_button =_('Back to Data Center')
801
802    def update(self, CANCEL=None):
803        if CANCEL is not None:
804            self.redirect(self.url(self.context))
805            return
806        return super(DatacenterFinishedPage, self).update()
807
808class DatacenterUploadPage(IkobaPage):
809    grok.context(IDataCenter)
810    grok.name('upload')
811    grok.require('waeup.manageDataCenter')
812    label = _(u'Upload portal data as CSV file')
813    pnav = 0
814    max_files = 20
815    upload_button =_(u'Upload')
816    cancel_button =_(u'Back to Data Center')
817
818    def getPreviewHeader(self):
819        """Get the header fields of uploaded CSV file.
820        """
821        reader = csv.reader(open(self.fullpath, 'rb'))
822        return reader.next()
823
824    def _notifyImportManagers(self, filename,
825        normalized_filename, importer, import_mode):
826        """Send email to Import Managers
827        """
828        # Get information about file
829        self.fullpath = os.path.join(self.context.storage, normalized_filename)
830        uploadfile = DataCenterFile(self.fullpath)
831        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
832        table = getPreviewTable(self, 3)
833        mail_table = ''
834        for line in table:
835            header = line[0]
836            data = str(line[1:]).strip('[').strip(']')
837            mail_table += '%s: %s ...\n' % (line[0], data)
838        # Collect all recipient addresses
839        ikoba_utils = getUtility(IIkobaUtils)
840        import_managers = get_users_with_role(
841            'waeup.ImportManager', grok.getSite())
842        rcpt_addrs = ','.join(
843            [user['user_email'] for user in import_managers if
844                user['user_email'] is not None])
845        if rcpt_addrs:
846            config = grok.getSite()['configuration']
847            fullname = self.request.principal.title
848            try:
849                email = self.request.principal.email
850            except AttributeError:
851                email = config.email_admin
852            username = self.request.principal.id
853            usertype = getattr(self.request.principal,
854                               'user_type', 'system').title()
855            rcpt_name = _('Import Manager')
856            subject = translate(
857                      _('${a}: ${b} uploaded',
858                      mapping = {'a':config.acronym, 'b':filename}),
859                      'waeup.ikoba',
860                      target_language=ikoba_utils.PORTAL_LANGUAGE)
861            text = _("""File: ${a}
862Importer: ${b}
863Import Mode: ${c}
864Datasets: ${d}
865
866${e}
867
868Comment by Import Manager:""", mapping = {'a':normalized_filename,
869                'b':importer,
870                'c':import_mode,
871                'd':uploadfile.lines - 1,
872                'e':mail_table})
873            success = ikoba_utils.sendContactForm(
874                    fullname,email,
875                    rcpt_name,rcpt_addrs,
876                    username,usertype,config.name,
877                    text,subject)
878            if success:
879                self.flash(
880                    _('All import managers have been notified by email.'))
881            else:
882                self.flash(_('An smtp server error occurred.'), type='danger')
883            return
884
885    def update(self, uploadfile=None, import_mode=None,
886               importer=None, CANCEL=None, SUBMIT=None):
887        number_of_pendings = len(self.context.getPendingFiles())
888        if number_of_pendings > self.max_files:
889            self.flash(
890                _('Maximum number of files in the data center exceeded.'),
891                  type='danger')
892            self.redirect(self.url(self.context))
893            return
894        if CANCEL is not None:
895            self.redirect(self.url(self.context))
896            return
897        if not uploadfile:
898            return
899        try:
900            filename = uploadfile.filename
901            #if 'pending' in filename:
902            #    self.flash(_("You can't re-upload pending data files."), type='danger')
903            #    return
904            if not filename.endswith('.csv'):
905                self.flash(_("Only csv files are allowed."), type='danger')
906                return
907            normalized_filename = self.getNormalizedFileName(filename)
908            finished_file = os.path.join(
909                self.context.storage, 'finished', normalized_filename)
910            unfinished_file = os.path.join(
911                self.context.storage, 'unfinished', normalized_filename)
912            if os.path.exists(finished_file) or os.path.exists(unfinished_file):
913                self.flash(_("File with same name was uploaded earlier."),
914                           type='danger')
915                return
916            target = os.path.join(self.context.storage, normalized_filename)
917            filecontent = uploadfile.read()
918            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
919            logger = self.context.logger
920
921            # Forbid certain characters in import files.
922            invalid_line = check_csv_charset(filecontent.splitlines())
923            if invalid_line:
924                self.flash(_(
925                    "Your file contains forbidden characters or "
926                    "has invalid CSV format. "
927                    "First problematic line detected: line %s. "
928                    "Please replace." % invalid_line), type='danger')
929                logger.info('%s - invalid file uploaded: %s' %
930                            (ob_class, target))
931                return
932
933            open(target, 'wb').write(filecontent)
934            os.chmod(target, 0664)
935            logger.info('%s - uploaded: %s' % (ob_class, target))
936            self._notifyImportManagers(filename,
937                normalized_filename, importer, import_mode)
938
939        except IOError:
940            self.flash('Error while uploading file. Please retry.', type='danger')
941            self.flash('I/O error: %s' % sys.exc_info()[1], type='danger')
942            return
943        self.redirect(self.url(self.context))
944
945    def getNormalizedFileName(self, filename):
946        """Build sane filename.
947
948        An uploaded file foo.csv will be stored as foo_USERNAME.csv
949        where username is the principal id of the currently logged in
950        user.
951
952        Spaces in filename are replaced by underscore.
953        Pending data filenames remain unchanged.
954        """
955        if filename.endswith('.pending.csv'):
956            return filename
957        username = self.request.principal.id
958        filename = filename.replace(' ', '_')
959        # Only accept typical filname chars...
960        filtered_username = ''.join(re.findall('[a-zA-Z0-9_\.\-]', username))
961        base, ext = os.path.splitext(filename)
962        return '%s_%s%s' % (base, filtered_username, ext.lower())
963
964    def getImporters(self):
965        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
966        ikoba_utils = getUtility(IIkobaUtils)
967        importer_props = []
968        for x in importers:
969            if not x.util_name in ikoba_utils.BATCH_PROCESSOR_NAMES:
970                continue
971            iface_fields = schema.getFields(x.iface)
972            available_fields = []
973            for key in iface_fields.keys():
974                iface_fields[key] = (iface_fields[key].__class__.__name__,
975                    iface_fields[key].required)
976            for value in x.available_fields:
977                available_fields.append(
978                    dict(f_name=value,
979                         f_type=iface_fields.get(value, (None, False))[0],
980                         f_required=iface_fields.get(value, (None, False))[1]
981                         )
982                    )
983            available_fields = sorted(available_fields, key=lambda k: k['f_name'])
984            importer_props.append(
985                dict(title=x.name, name=x.util_name, fields=available_fields))
986        return sorted(importer_props, key=lambda k: k['title'])
987
988class FileDownloadView(UtilityView, grok.View):
989    grok.context(IDataCenter)
990    grok.name('download')
991    grok.require('waeup.manageDataCenter')
992
993    def update(self, filename=None):
994        self.filename = self.request.form['filename']
995        return
996
997    def render(self):
998        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
999        self.context.logger.info(
1000            '%s - downloaded: %s' % (ob_class, self.filename))
1001        self.response.setHeader(
1002            'Content-Type', 'text/csv; charset=UTF-8')
1003        self.response.setHeader(
1004            'Content-Disposition:', 'attachment; filename="%s' %
1005            self.filename.replace('finished/',''))
1006        fullpath = os.path.join(self.context.storage, self.filename)
1007        return open(fullpath, 'rb').read()
1008
1009class SkeletonDownloadView(UtilityView, grok.View):
1010    grok.context(IDataCenter)
1011    grok.name('skeleton')
1012    grok.require('waeup.manageDataCenter')
1013
1014    def update(self, processorname=None):
1015        self.processorname = self.request.form['name']
1016        self.filename = ('%s_000.csv' %
1017            self.processorname.replace('processor','import'))
1018        return
1019
1020    def render(self):
1021        #ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1022        #self.context.logger.info(
1023        #    '%s - skeleton downloaded: %s' % (ob_class, self.filename))
1024        self.response.setHeader(
1025            'Content-Type', 'text/csv; charset=UTF-8')
1026        self.response.setHeader(
1027            'Content-Disposition:', 'attachment; filename="%s' % self.filename)
1028        processor = getUtility(IBatchProcessor, name=self.processorname)
1029        csv_data = processor.get_csv_skeleton()
1030        return csv_data
1031
1032class DatacenterImportStep1(IkobaPage):
1033    """Manual import step 1: choose file
1034    """
1035    grok.context(IDataCenter)
1036    grok.name('import1')
1037    grok.template('datacenterimport1page')
1038    grok.require('waeup.manageDataCenter')
1039    label = _(u'Process CSV file')
1040    pnav = 0
1041    cancel_button =_(u'Back to Data Center')
1042
1043    def getFiles(self):
1044        files = self.context.getPendingFiles(sort='date')
1045        for file in files:
1046            name = file.name
1047            if not name.endswith('.csv') and not name.endswith('.pending'):
1048                continue
1049            yield file
1050
1051    def update(self, filename=None, select=None, cancel=None):
1052        if cancel is not None:
1053            self.redirect(self.url(self.context))
1054            return
1055        if select is not None:
1056            # A filename was selected
1057            session = ISession(self.request)['waeup.ikoba']
1058            session['import_filename'] = select
1059            self.redirect(self.url(self.context, '@@import2'))
1060
1061class DatacenterImportStep2(IkobaPage):
1062    """Manual import step 2: choose processor
1063    """
1064    grok.context(IDataCenter)
1065    grok.name('import2')
1066    grok.template('datacenterimport2page')
1067    grok.require('waeup.manageDataCenter')
1068    label = _(u'Process CSV file')
1069    pnav = 0
1070    cancel_button =_(u'Cancel')
1071    back_button =_(u'Back to step 1')
1072    proceed_button =_(u'Proceed to step 3')
1073
1074    filename = None
1075    mode = 'create'
1076    importer = None
1077    mode_locked = False
1078
1079    def getPreviewHeader(self):
1080        """Get the header fields of attached CSV file.
1081        """
1082        reader = csv.reader(open(self.fullpath, 'rb'))
1083        return reader.next()
1084
1085    def getPreviewTable(self):
1086        return getPreviewTable(self, 3)
1087
1088    def getImporters(self):
1089        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
1090        ikoba_utils = getUtility(IIkobaUtils)
1091        importers = sorted(
1092            [dict(title=x.name, name=x.util_name) for x in importers
1093            if x.util_name in ikoba_utils.BATCH_PROCESSOR_NAMES])
1094        return importers
1095
1096    def getModeFromFilename(self, filename):
1097        """Lookup filename or path and return included mode name or None.
1098        """
1099        if not filename.endswith('.pending.csv'):
1100            return None
1101        base = os.path.basename(filename)
1102        parts = base.rsplit('.', 3)
1103        if len(parts) != 4:
1104            return None
1105        if parts[1] not in ['create', 'update', 'remove']:
1106            return None
1107        return parts[1]
1108
1109    def getWarnings(self):
1110        import sys
1111        result = []
1112        try:
1113            headerfields = self.getPreviewHeader()
1114            headerfields_clean = list(set(headerfields))
1115            if len(headerfields) > len(headerfields_clean):
1116                result.append(
1117                    _("Double headers: each column name may only appear once. "))
1118        except:
1119            fatal = '%s' % sys.exc_info()[1]
1120            result.append(fatal)
1121        if result:
1122            warnings = ""
1123            for line in result:
1124                warnings += line + '<br />'
1125            warnings += _('Replace imported file!')
1126            return warnings
1127        return False
1128
1129    def update(self, mode=None, importer=None,
1130               back1=None, cancel=None, proceed=None):
1131        session = ISession(self.request)['waeup.ikoba']
1132        self.filename = session.get('import_filename', None)
1133
1134        if self.filename is None or back1 is not None:
1135            self.redirect(self.url(self.context, '@@import1'))
1136            return
1137        if cancel is not None:
1138            self.flash(_('Import aborted.'), type='warning')
1139            self.redirect(self.url(self.context))
1140            return
1141        self.mode = mode or session.get('import_mode', self.mode)
1142        filename_mode = self.getModeFromFilename(self.filename)
1143        if filename_mode is not None:
1144            self.mode = filename_mode
1145            self.mode_locked = True
1146        self.importer = importer or session.get('import_importer', None)
1147        session['import_importer'] = self.importer
1148        if self.importer and 'update' in self.importer:
1149            if self.mode != 'update':
1150                self.flash(_('Update mode only!'), type='warning')
1151                self.mode_locked = True
1152                self.mode = 'update'
1153                proceed = None
1154        session['import_mode'] = self.mode
1155        if proceed is not None:
1156            self.redirect(self.url(self.context, '@@import3'))
1157            return
1158        self.fullpath = os.path.join(self.context.storage, self.filename)
1159        warnings = self.getWarnings()
1160        if not warnings:
1161            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1162        else:
1163            self.reader = ()
1164            self.flash(warnings, type='warning')
1165
1166class DatacenterImportStep3(IkobaPage):
1167    """Manual import step 3: modify header
1168    """
1169    grok.context(IDataCenter)
1170    grok.name('import3')
1171    grok.template('datacenterimport3page')
1172    grok.require('waeup.manageDataCenter')
1173    label = _(u'Process CSV file')
1174    pnav = 0
1175    cancel_button =_(u'Cancel')
1176    reset_button =_(u'Reset')
1177    update_button =_(u'Set headerfields')
1178    back_button =_(u'Back to step 2')
1179    proceed_button =_(u'Perform import')
1180
1181    filename = None
1182    mode = None
1183    importername = None
1184
1185    @property
1186    def nextstep(self):
1187        return self.url(self.context, '@@import4')
1188
1189    def getPreviewHeader(self):
1190        """Get the header fields of attached CSV file.
1191        """
1192        reader = csv.reader(open(self.fullpath, 'rb'))
1193        return reader.next()
1194
1195    def getPreviewTable(self):
1196        """Get transposed table with 1 sample record.
1197
1198        The first column contains the headers.
1199        """
1200        if not self.reader:
1201            return
1202        headers = self.getPreviewHeader()
1203        num = 0
1204        data = []
1205        for line in self.reader:
1206            if num > 0:
1207                break
1208            num += 1
1209            data.append(line)
1210        result = []
1211        field_num = 0
1212        for name in headers:
1213            result_line = []
1214            result_line.append(field_num)
1215            field_num += 1
1216            for d in data:
1217                result_line.append(d[name])
1218            result.append(result_line)
1219        return result
1220
1221    def getPossibleHeaders(self):
1222        """Get the possible headers.
1223
1224        The headers are described as dicts {value:internal_name,
1225        title:displayed_name}
1226        """
1227        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
1228        headers = self.importer.getHeaders()
1229        result.extend([dict(title=x, value=x) for x in headers])
1230        return result
1231
1232    def getWarnings(self):
1233        import sys
1234        result = []
1235        try:
1236            self.importer.checkHeaders(self.headerfields, mode=self.mode)
1237        except:
1238            fatal = '%s' % sys.exc_info()[1]
1239            result.append(fatal)
1240        if result:
1241            warnings = ""
1242            for line in result:
1243                warnings += line + '<br />'
1244            warnings += _('Edit headers or replace imported file!')
1245            return warnings
1246        return False
1247
1248    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
1249        session = ISession(self.request)['waeup.ikoba']
1250        self.filename = session.get('import_filename', None)
1251        self.mode = session.get('import_mode', None)
1252        self.importername = session.get('import_importer', None)
1253
1254        if None in (self.filename, self.mode, self.importername):
1255            self.redirect(self.url(self.context, '@@import2'))
1256            return
1257        if back2 is not None:
1258            self.redirect(self.url(self.context ,'@@import2'))
1259            return
1260        if cancel is not None:
1261            self.flash(_('Import aborted.'), type='warning')
1262            self.redirect(self.url(self.context))
1263            return
1264
1265        self.fullpath = os.path.join(self.context.storage, self.filename)
1266        self.headerfields = headerfield or self.getPreviewHeader()
1267        session['import_headerfields'] = self.headerfields
1268
1269        if proceed is not None:
1270            self.redirect(self.url(self.context, '@@import4'))
1271            return
1272        self.importer = getUtility(IBatchProcessor, name=self.importername)
1273        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1274        warnings = self.getWarnings()
1275        if warnings:
1276            self.flash(warnings, type='warning')
1277
1278class DatacenterImportStep4(IkobaPage):
1279    """Manual import step 4: do actual import
1280    """
1281    grok.context(IDataCenter)
1282    grok.name('import4')
1283    grok.template('datacenterimport4page')
1284    grok.require('waeup.importData')
1285    label = _(u'Process CSV file')
1286    pnav = 0
1287    back_button =_(u'Process next')
1288
1289    filename = None
1290    mode = None
1291    importername = None
1292    headerfields = None
1293    warnnum = None
1294
1295    def update(self, back=None, finish=None, showlog=None):
1296        if finish is not None:
1297            self.redirect(self.url(self.context, '@@import1'))
1298            return
1299        session = ISession(self.request)['waeup.ikoba']
1300        self.filename = session.get('import_filename', None)
1301        self.mode = session.get('import_mode', None)
1302        self.importername = session.get('import_importer', None)
1303        # If the import file contains only one column
1304        # the import_headerfields attribute is a string.
1305        ihf = session.get('import_headerfields', None)
1306        if not isinstance(ihf, list):
1307            self.headerfields = ihf.split()
1308        else:
1309            self.headerfields = ihf
1310
1311        if None in (self.filename, self.mode, self.importername,
1312                    self.headerfields):
1313            self.redirect(self.url(self.context, '@@import3'))
1314            return
1315
1316        if showlog is not None:
1317            logfilename = "datacenter.log"
1318            session['logname'] = logfilename
1319            self.redirect(self.url(self.context, '@@show'))
1320            return
1321
1322        self.fullpath = os.path.join(self.context.storage, self.filename)
1323        self.importer = getUtility(IBatchProcessor, name=self.importername)
1324
1325        # Perform batch processing...
1326        # XXX: This might be better placed in datacenter module.
1327        (linenum, self.warn_num,
1328         fin_path, pending_path) = self.importer.doImport(
1329            self.fullpath, self.headerfields, self.mode,
1330            self.request.principal.id, logger=self.context.logger)
1331        # Put result files in desired locations...
1332        self.context.distProcessedFiles(
1333            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1334            self.mode)
1335
1336        if self.warn_num:
1337            self.flash(_('Processing of ${a} rows failed.',
1338                mapping = {'a':self.warn_num}), type='warning')
1339        self.flash(_('Successfully processed ${a} rows.',
1340            mapping = {'a':linenum - self.warn_num}))
1341
1342class DatacenterLogsOverview(IkobaPage):
1343    grok.context(IDataCenter)
1344    grok.name('logs')
1345    grok.template('datacenterlogspage')
1346    grok.require('waeup.manageDataCenter')
1347    label = _(u'Show logfiles')
1348    pnav = 0
1349    back_button = _(u'Back to Data Center')
1350    show_button = _(u'Show')
1351
1352    def update(self, back=None):
1353        if back is not None:
1354            self.redirect(self.url(self.context))
1355            return
1356        self.files = self.context.getLogFiles()
1357
1358class DatacenterLogsFileview(IkobaPage):
1359    grok.context(IDataCenter)
1360    grok.name('show')
1361    grok.template('datacenterlogsshowfilepage')
1362    grok.require('waeup.manageDataCenter')
1363    title = _(u'Data Center')
1364    pnav = 0
1365    search_button = _('Search')
1366    back_button = _('Back to Data Center')
1367    placeholder = _('Enter a regular expression here...')
1368
1369    def label(self):
1370        return "Logfile %s" % self.filename
1371
1372    def update(self, back=None, query=None, logname=None):
1373        if os.name != 'posix':
1374            self.flash(
1375                _('Log files can only be searched ' +
1376                  'on Unix-based operating systems.'), type='danger')
1377            self.redirect(self.url(self.context, '@@logs'))
1378            return
1379        if back is not None or logname is None:
1380            self.redirect(self.url(self.context, '@@logs'))
1381            return
1382        self.filename = logname
1383        self.query = query
1384        if not query:
1385            return
1386        try:
1387            self.result = ''.join(
1388                self.context.queryLogfiles(logname, query))
1389        except ValueError:
1390            self.flash(_('Invalid search expression.'), type='danger')
1391            return
1392        if not self.result:
1393            self.flash(_('No search results found.'), type='warning')
1394        return
1395
1396class DatacenterSettings(IkobaPage):
1397    grok.context(IDataCenter)
1398    grok.name('manage')
1399    grok.template('datacentermanagepage')
1400    grok.require('waeup.managePortal')
1401    label = _('Edit data center settings')
1402    pnav = 0
1403    save_button =_(u'Save')
1404    reset_button =_(u'Reset')
1405    cancel_button =_(u'Back to Data Center')
1406
1407    def update(self, newpath=None, move=False, overwrite=False,
1408               save=None, cancel=None):
1409        if move:
1410            move = True
1411        if overwrite:
1412            overwrite = True
1413        if newpath is None:
1414            return
1415        if cancel is not None:
1416            self.redirect(self.url(self.context))
1417            return
1418        try:
1419            not_copied = self.context.setStoragePath(newpath, move=move)
1420            for name in not_copied:
1421                self.flash(_('File already existed (not copied): ${a}',
1422                    mapping = {'a':name}), type='danger')
1423        except:
1424            self.flash(_('Given storage path cannot be used: ${a}',
1425                        mapping = {'a':sys.exc_info()[1]}), type='danger')
1426            return
1427        if newpath:
1428            self.flash(_('New storage path succefully set.'))
1429            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1430            self.context.logger.info(
1431                '%s - storage path set: %s' % (ob_class, newpath))
1432            self.redirect(self.url(self.context))
1433        return
1434
1435class ExportCSVPage(IkobaPage):
1436    grok.context(IDataCenter)
1437    grok.name('export')
1438    grok.template('datacenterexportpage')
1439    grok.require('waeup.exportData')
1440    label = _('Download portal data as CSV file')
1441    pnav = 0
1442    export_button = _(u'Create CSV file')
1443    cancel_button =_(u'Back to Data Center')
1444
1445    def getExporters(self):
1446        exporter_utils = getUtilitiesFor(ICSVExporter)
1447        ikoba_utils = getUtility(IIkobaUtils)
1448        title_name_tuples = [
1449            (util.title, name) for name, util in exporter_utils
1450            if name in ikoba_utils.EXPORTER_NAMES]
1451        return sorted(title_name_tuples)
1452
1453    def update(self, CREATE=None, DISCARD=None, exporter=None,
1454               job_id=None, CANCEL=None):
1455        if CANCEL is not None:
1456            self.redirect(self.url(self.context))
1457            return
1458        if CREATE:
1459            job_id = self.context.start_export_job(
1460                exporter, self.request.principal.id)
1461            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1462            self.context.logger.info(
1463                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
1464        if DISCARD and job_id:
1465            entry = self.context.entry_from_job_id(job_id)
1466            self.context.delete_export_entry(entry)
1467            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1468            self.context.logger.info(
1469                '%s - discarded: job_id=%s' % (ob_class, job_id))
1470            self.flash(_('Discarded export') + ' %s' % job_id)
1471        self.entries = doll_up(self, user=None)
1472        return
1473
1474class ExportCSVView(grok.View):
1475    grok.context(IDataCenter)
1476    grok.name('download_export')
1477    grok.require('waeup.exportData')
1478
1479    def render(self, job_id=None):
1480        manager = getUtility(IJobManager)
1481        job = manager.get(job_id)
1482        if job is None:
1483            return
1484        if hasattr(job.result, 'traceback'):
1485            # XXX: Some error happened. Do something more approriate here...
1486            return
1487        path = job.result
1488        if not os.path.exists(path):
1489            # XXX: Do something more appropriate here...
1490            return
1491        result = open(path, 'rb').read()
1492        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
1493        filename = "%s_%s" % (acronym, os.path.basename(path))
1494        filename = filename.replace('.csv', '_%s.csv' % job_id)
1495        self.response.setHeader(
1496            'Content-Type', 'text/csv; charset=UTF-8')
1497        self.response.setHeader(
1498            'Content-Disposition', 'attachment; filename="%s' % filename)
1499        # remove job and running_exports entry from context
1500        #self.context.delete_export_entry(
1501        #    self.context.entry_from_job_id(job_id))
1502        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1503        self.context.logger.info(
1504            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
1505        return result
1506
1507class ChangePasswordRequestPage(IkobaForm):
1508    """Captcha'd page for all kind of users to request a password change.
1509    """
1510    grok.context(ICompany)
1511    grok.name('changepw')
1512    grok.require('waeup.Anonymous')
1513    grok.template('changepw')
1514    label = _('Send me a new password')
1515    form_fields = grok.AutoFields(IChangePassword)
1516
1517    def update(self):
1518        # Handle captcha
1519        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1520        self.captcha_result = self.captcha.verify(self.request)
1521        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1522        return
1523
1524    def _searchUser(self, identifier, email):
1525        # Search customer
1526        cat = queryUtility(ICatalog, name='customers_catalog')
1527        results = cat.searchResults(
1528            reg_number=(identifier, identifier),
1529            email=(email,email))
1530        for result in results:
1531            if result.customer_id == identifier \
1532                or result.reg_number == identifier:
1533                return result
1534        # Search portal user
1535        user = grok.getSite()['users'].get(identifier, None)
1536        if user is not None and user.email == email:
1537            return user
1538        return None
1539
1540    @action(_('Send login credentials to email address'), style='primary')
1541    def request(self, **data):
1542        if not self.captcha_result.is_valid:
1543            # Captcha will display error messages automatically.
1544            # No need to flash something.
1545            return
1546        # Search customer
1547        identifier = data['identifier']
1548        email = data['email']
1549        user = self._searchUser(identifier, email)
1550        if user is None:
1551            self.flash(_('No record found.'), type='warning')
1552            return
1553        # Change password
1554        ikoba_utils = getUtility(IIkobaUtils)
1555        password = ikoba_utils.genPassword()
1556        mandate = PasswordMandate()
1557        mandate.params['password'] = password
1558        mandate.params['user'] = user
1559        site = grok.getSite()
1560        site['mandates'].addMandate(mandate)
1561        # Send email with credentials
1562        args = {'mandate_id':mandate.mandate_id}
1563        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1564        url_info = u'Confirmation link: %s' % mandate_url
1565        msg = _('You have successfully requested a password for the')
1566        success = ikoba_utils.sendCredentials(
1567            IUserAccount(user),password,url_info,msg)
1568        if success:
1569            self.flash(_('An email with your user name and password ' +
1570                'has been sent to ${a}.', mapping = {'a':email}))
1571        else:
1572            self.flash(_('An smtp server error occurred.'), type='danger')
1573        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1574        self.context.logger.info(
1575            '%s - %s - %s' % (ob_class, data['identifier'], data['email']))
1576        return
Note: See TracBrowser for help on using the repository browser.