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

Last change on this file since 13808 was 13808, checked in by Henrik Bettermann, 9 years ago

Escape HTML in Logfiles when displayed in Browser.

When logfiles are displayed in datacenter, included
HTML tags should show up as tags and not be rendered
by the browser. We therefore cgi.escape logfile
contents.

See r13495 and r13496.

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