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

Last change on this file was 15258, checked in by Henrik Bettermann, 6 years ago

Do not allow uploading data with trailing whitespaces.

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