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

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

Skip User Processor if user isn't allowed to manage users.

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