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

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

Ticket #11 compromise

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

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