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

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

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

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