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

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

Registration and Application Portal/System? (RAPS)

Adjust localization.

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