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

Last change on this file since 14221 was 14221, checked in by Henrik Bettermann, 8 years ago

Count mandates on configuration page and provide 'Purge' button.

  • Property svn:keywords set to Id
File size: 66.3 KB
Line 
1## $Id: pages.py 14221 2016-10-20 21:13:17Z 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                self.flash(_(
1145                    "Your file contains forbidden characters or "
1146                    "has invalid CSV format. "
1147                    "First problematic line detected: line %s. "
1148                    "Please replace." % invalid_line), type='danger')
1149                logger.info('%s - invalid file uploaded: %s' %
1150                            (ob_class, target))
1151                return
1152
1153            open(target, 'wb').write(filecontent)
1154            os.chmod(target, 0664)
1155            logger.info('%s - uploaded: %s' % (ob_class, target))
1156            self._notifyImportManagers(filename,
1157                normalized_filename, importer, import_mode)
1158
1159        except IOError:
1160            self.flash('Error while uploading file. Please retry.', type='danger')
1161            self.flash('I/O error: %s' % sys.exc_info()[1], type='danger')
1162            return
1163        self.redirect(self.url(self.context))
1164
1165    def getNormalizedFileName(self, filename):
1166        """Build sane filename.
1167
1168        An uploaded file foo.csv will be stored as foo_USERNAME.csv
1169        where username is the principal id of the currently logged in
1170        user.
1171
1172        Spaces in filename are replaced by underscore.
1173        Pending data filenames remain unchanged.
1174        """
1175        if filename.endswith('.pending.csv'):
1176            return filename
1177        username = self.request.principal.id
1178        filename = filename.replace(' ', '_')
1179        # Only accept typical filname chars...
1180        filtered_username = ''.join(re.findall('[a-zA-Z0-9_\.\-]', username))
1181        base, ext = os.path.splitext(filename)
1182        return '%s_%s%s' % (base, filtered_username, ext.lower())
1183
1184    def getImporters(self):
1185        return getImporters(self.context)
1186
1187class FileDownloadView(UtilityView, grok.View):
1188    grok.context(IDataCenter)
1189    grok.name('download')
1190    grok.require('waeup.manageDataCenter')
1191
1192    def update(self, filename=None):
1193        self.filename = self.request.form['filename']
1194        return
1195
1196    def render(self):
1197        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1198        self.context.logger.info(
1199            '%s - downloaded: %s' % (ob_class, self.filename))
1200        self.response.setHeader(
1201            'Content-Type', 'text/csv; charset=UTF-8')
1202        self.response.setHeader(
1203            'Content-Disposition:', 'attachment; filename="%s' %
1204            self.filename.replace('finished/',''))
1205        fullpath = os.path.join(self.context.storage, self.filename)
1206        return open(fullpath, 'rb').read()
1207
1208class DatacenterImportStep1(IkobaPage):
1209    """Manual import step 1: choose file
1210    """
1211    grok.context(IDataCenter)
1212    grok.name('import1')
1213    grok.template('datacenterimport1page')
1214    grok.require('waeup.manageDataCenter')
1215    label = _(u'Process CSV file')
1216    pnav = 0
1217    cancel_button =_(u'Back to Data Center')
1218
1219    def getFiles(self):
1220        files = self.context.getPendingFiles(sort='date')
1221        for file in files:
1222            name = file.name
1223            if not name.endswith('.csv') and not name.endswith('.pending'):
1224                continue
1225            yield file
1226
1227    def update(self, filename=None, select=None, cancel=None):
1228        if not grok.getSite()['configuration'].maintmode_enabled_by and \
1229            not self.request.principal.id == 'admin':
1230            self.flash(
1231                _('Portal must be in maintenance mode for data import.'),
1232                type='warning')
1233            self.redirect(self.url(self.context))
1234            return
1235        if cancel is not None:
1236            self.redirect(self.url(self.context))
1237            return
1238        if select is not None:
1239            # A filename was selected
1240            session = ISession(self.request)['waeup.ikoba']
1241            session['import_filename'] = select
1242            self.redirect(self.url(self.context, '@@import2'))
1243        return
1244
1245class DatacenterImportStep2(IkobaPage):
1246    """Manual import step 2: choose processor
1247    """
1248    grok.context(IDataCenter)
1249    grok.name('import2')
1250    grok.template('datacenterimport2page')
1251    grok.require('waeup.manageDataCenter')
1252    label = _(u'Process CSV file')
1253    pnav = 0
1254    cancel_button =_(u'Cancel')
1255    back_button =_(u'Back to step 1')
1256    proceed_button =_(u'Proceed to step 3')
1257
1258    filename = None
1259    mode = 'create'
1260    importer = None
1261    mode_locked = False
1262
1263    def getPreviewHeader(self):
1264        """Get the header fields of attached CSV file.
1265        """
1266        reader = csv.reader(open(self.fullpath, 'rb'))
1267        return reader.next()
1268
1269    def getPreviewTable(self):
1270        return getPreviewTable(self, 3)
1271
1272    def getImporters(self):
1273        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
1274        ikoba_utils = getUtility(IIkobaUtils)
1275        importers = sorted(
1276            [dict(title=x.name, name=x.util_name) for x in importers
1277            if x.util_name in ikoba_utils.BATCH_PROCESSOR_NAMES])
1278        return importers
1279
1280    def getModeFromFilename(self, filename):
1281        """Lookup filename or path and return included mode name or None.
1282        """
1283        if not filename.endswith('.pending.csv'):
1284            return None
1285        base = os.path.basename(filename)
1286        parts = base.rsplit('.', 3)
1287        if len(parts) != 4:
1288            return None
1289        if parts[1] not in ['create', 'update', 'remove']:
1290            return None
1291        return parts[1]
1292
1293    def getWarnings(self):
1294        import sys
1295        result = []
1296        try:
1297            headerfields = self.getPreviewHeader()
1298            headerfields_clean = list(set(headerfields))
1299            if len(headerfields) > len(headerfields_clean):
1300                result.append(
1301                    _("Double headers: each column name may only appear once. "))
1302        except:
1303            fatal = '%s' % sys.exc_info()[1]
1304            result.append(fatal)
1305        if result:
1306            warnings = ""
1307            for line in result:
1308                warnings += line + '<br />'
1309            warnings += _('Replace imported file!')
1310            return warnings
1311        return False
1312
1313    def update(self, mode=None, importer=None,
1314               back1=None, cancel=None, proceed=None):
1315        session = ISession(self.request)['waeup.ikoba']
1316        self.filename = session.get('import_filename', None)
1317
1318        if self.filename is None or back1 is not None:
1319            self.redirect(self.url(self.context, '@@import1'))
1320            return
1321        if cancel is not None:
1322            self.flash(_('Import aborted.'), type='warning')
1323            self.redirect(self.url(self.context))
1324            return
1325        self.mode = mode or session.get('import_mode', self.mode)
1326        filename_mode = self.getModeFromFilename(self.filename)
1327        if filename_mode is not None:
1328            self.mode = filename_mode
1329            self.mode_locked = True
1330        self.importer = importer or session.get('import_importer', None)
1331        session['import_importer'] = self.importer
1332        if self.importer and 'update' in self.importer:
1333            if self.mode != 'update':
1334                self.flash(_('Update mode only!'), type='warning')
1335                self.mode_locked = True
1336                self.mode = 'update'
1337                proceed = None
1338        session['import_mode'] = self.mode
1339        if proceed is not None:
1340            self.redirect(self.url(self.context, '@@import3'))
1341            return
1342        self.fullpath = os.path.join(self.context.storage, self.filename)
1343        warnings = self.getWarnings()
1344        if not warnings:
1345            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1346        else:
1347            self.reader = ()
1348            self.flash(warnings, type='warning')
1349
1350class DatacenterImportStep3(IkobaPage):
1351    """Manual import step 3: modify header
1352    """
1353    grok.context(IDataCenter)
1354    grok.name('import3')
1355    grok.template('datacenterimport3page')
1356    grok.require('waeup.manageDataCenter')
1357    label = _(u'Process CSV file')
1358    pnav = 0
1359    cancel_button =_(u'Cancel')
1360    reset_button =_(u'Reset')
1361    update_button =_(u'Set headerfields')
1362    back_button =_(u'Back to step 2')
1363    proceed_button =_(u'Perform import')
1364
1365    filename = None
1366    mode = None
1367    importername = None
1368
1369    @property
1370    def nextstep(self):
1371        return self.url(self.context, '@@import4')
1372
1373    def getPreviewHeader(self):
1374        """Get the header fields of attached CSV file.
1375        """
1376        reader = csv.reader(open(self.fullpath, 'rb'))
1377        return reader.next()
1378
1379    def getPreviewTable(self):
1380        """Get transposed table with 1 sample record.
1381
1382        The first column contains the headers.
1383        """
1384        if not self.reader:
1385            return
1386        headers = self.getPreviewHeader()
1387        num = 0
1388        data = []
1389        for line in self.reader:
1390            if num > 0:
1391                break
1392            num += 1
1393            data.append(line)
1394        result = []
1395        field_num = 0
1396        for name in headers:
1397            result_line = []
1398            result_line.append(field_num)
1399            field_num += 1
1400            for d in data:
1401                result_line.append(d[name])
1402            result.append(result_line)
1403        return result
1404
1405    def getPossibleHeaders(self):
1406        """Get the possible headers.
1407
1408        The headers are described as dicts {value:internal_name,
1409        title:displayed_name}
1410        """
1411        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
1412        headers = self.importer.getHeaders()
1413        result.extend([dict(title=x, value=x) for x in headers])
1414        return result
1415
1416    def getWarnings(self):
1417        import sys
1418        result = []
1419        try:
1420            self.importer.checkHeaders(self.headerfields, mode=self.mode)
1421        except:
1422            fatal = '%s' % sys.exc_info()[1]
1423            result.append(fatal)
1424        if result:
1425            warnings = ""
1426            for line in result:
1427                warnings += line + '<br />'
1428            warnings += _('Edit headers or replace imported file!')
1429            return warnings
1430        return False
1431
1432    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
1433        session = ISession(self.request)['waeup.ikoba']
1434        self.filename = session.get('import_filename', None)
1435        self.mode = session.get('import_mode', None)
1436        self.importername = session.get('import_importer', None)
1437
1438        if None in (self.filename, self.mode, self.importername):
1439            self.redirect(self.url(self.context, '@@import2'))
1440            return
1441        if back2 is not None:
1442            self.redirect(self.url(self.context ,'@@import2'))
1443            return
1444        if cancel is not None:
1445            self.flash(_('Import aborted.'), type='warning')
1446            self.redirect(self.url(self.context))
1447            return
1448
1449        self.fullpath = os.path.join(self.context.storage, self.filename)
1450        self.headerfields = headerfield or self.getPreviewHeader()
1451        session['import_headerfields'] = self.headerfields
1452
1453        if proceed is not None:
1454            self.redirect(self.url(self.context, '@@import4'))
1455            return
1456        self.importer = getUtility(IBatchProcessor, name=self.importername)
1457        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1458        warnings = self.getWarnings()
1459        if warnings:
1460            self.flash(warnings, type='warning')
1461
1462class DatacenterImportStep4(IkobaPage):
1463    """Manual import step 4: do actual import
1464    """
1465    grok.context(IDataCenter)
1466    grok.name('import4')
1467    grok.template('datacenterimport4page')
1468    grok.require('waeup.importData')
1469    label = _(u'Process CSV file')
1470    pnav = 0
1471    back_button =_(u'Process next')
1472
1473    filename = None
1474    mode = None
1475    importername = None
1476    headerfields = None
1477    warnnum = None
1478
1479    def update(self, back=None, finish=None, showlog=None):
1480        if finish is not None:
1481            self.redirect(self.url(self.context, '@@import1'))
1482            return
1483        if not grok.getSite()['configuration'].maintmode_enabled_by and \
1484            not self.request.principal.id == 'admin':
1485            self.flash(
1486                _('File has not been imported. '
1487                  'Portal must be in maintenance mode for data import.'),
1488                type='danger')
1489            self.redirect(self.url(self.context))
1490            return
1491        session = ISession(self.request)['waeup.ikoba']
1492        self.filename = session.get('import_filename', None)
1493        self.mode = session.get('import_mode', None)
1494        self.importername = session.get('import_importer', None)
1495        # If the import file contains only one column
1496        # the import_headerfields attribute is a string.
1497        ihf = session.get('import_headerfields', None)
1498        if not isinstance(ihf, list):
1499            self.headerfields = ihf.split()
1500        else:
1501            self.headerfields = ihf
1502
1503        if None in (self.filename, self.mode, self.importername,
1504                    self.headerfields):
1505            self.redirect(self.url(self.context, '@@import3'))
1506            return
1507
1508        if showlog is not None:
1509            logfilename = "datacenter.log"
1510            session['logname'] = logfilename
1511            self.redirect(self.url(self.context, '@@show'))
1512            return
1513
1514        self.fullpath = os.path.join(self.context.storage, self.filename)
1515        self.importer = getUtility(IBatchProcessor, name=self.importername)
1516
1517        # Perform batch processing...
1518        # XXX: This might be better placed in datacenter module.
1519        (linenum, self.warn_num,
1520         fin_path, pending_path) = self.importer.doImport(
1521            self.fullpath, self.headerfields, self.mode,
1522            self.request.principal.id, logger=self.context.logger)
1523        # Put result files in desired locations...
1524        self.context.distProcessedFiles(
1525            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1526            self.mode)
1527        # Disable maintenance mode if set.
1528        if grok.getSite()['configuration'].maintmode_enabled_by:
1529            grok.getSite()['configuration'].maintmode_enabled_by = None
1530            self.flash(_('Maintenance mode disabled.'))
1531
1532        if self.warn_num:
1533            self.flash(_('Processing of ${a} rows failed.',
1534                mapping = {'a':self.warn_num}), type='warning')
1535        self.flash(_('Successfully processed ${a} rows.',
1536            mapping = {'a':linenum - self.warn_num}))
1537
1538class DatacenterLogsOverview(IkobaPage):
1539    grok.context(IDataCenter)
1540    grok.name('logs')
1541    grok.template('datacenterlogspage')
1542    grok.require('waeup.manageDataCenter')
1543    label = _(u'Show logfiles')
1544    pnav = 0
1545    back_button = _(u'Back to Data Center')
1546    show_button = _(u'Show')
1547
1548    def update(self, back=None):
1549        if back is not None:
1550            self.redirect(self.url(self.context))
1551            return
1552        self.files = self.context.getLogFiles()
1553
1554class DatacenterLogsFileview(IkobaPage):
1555    grok.context(IDataCenter)
1556    grok.name('show')
1557    grok.template('datacenterlogsshowfilepage')
1558    grok.require('waeup.manageDataCenter')
1559    title = _(u'Data Center')
1560    pnav = 0
1561    search_button = _('Search')
1562    back_button = _('Back to Data Center')
1563    placeholder = _('Enter a regular expression here...')
1564
1565    def label(self):
1566        return "Logfile %s" % self.filename
1567
1568    def update(self, back=None, query=None, logname=None):
1569        if os.name != 'posix':
1570            self.flash(
1571                _('Log files can only be searched ' +
1572                  'on Unix-based operating systems.'), type='danger')
1573            self.redirect(self.url(self.context, '@@logs'))
1574            return
1575        if back is not None or logname is None:
1576            self.redirect(self.url(self.context, '@@logs'))
1577            return
1578        self.filename = logname
1579        self.query = query
1580        if not query:
1581            return
1582        try:
1583            self.result = cgi.escape(
1584                ''.join(self.context.queryLogfiles(logname, query)))
1585        except ValueError:
1586            self.flash(_('Invalid search expression.'), type='danger')
1587            return
1588        if not self.result:
1589            self.flash(_('No search results found.'), type='warning')
1590        return
1591
1592class DatacenterSettings(IkobaPage):
1593    grok.context(IDataCenter)
1594    grok.name('manage')
1595    grok.template('datacentermanagepage')
1596    grok.require('waeup.managePortal')
1597    label = _('Edit data center settings')
1598    pnav = 0
1599    save_button =_(u'Save')
1600    reset_button =_(u'Reset')
1601    cancel_button =_(u'Back to Data Center')
1602
1603    def update(self, newpath=None, move=False, overwrite=False,
1604               save=None, cancel=None):
1605        if move:
1606            move = True
1607        if overwrite:
1608            overwrite = True
1609        if newpath is None:
1610            return
1611        if cancel is not None:
1612            self.redirect(self.url(self.context))
1613            return
1614        try:
1615            not_copied = self.context.setStoragePath(newpath, move=move)
1616            for name in not_copied:
1617                self.flash(_('File already existed (not copied): ${a}',
1618                    mapping = {'a':name}), type='danger')
1619        except:
1620            self.flash(_('Given storage path cannot be used: ${a}',
1621                        mapping = {'a':sys.exc_info()[1]}), type='danger')
1622            return
1623        if newpath:
1624            self.flash(_('New storage path succefully set.'))
1625            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1626            self.context.logger.info(
1627                '%s - storage path set: %s' % (ob_class, newpath))
1628            self.redirect(self.url(self.context))
1629        return
1630
1631class ExportCSVPage(IkobaPage):
1632    grok.context(IDataCenter)
1633    grok.name('export')
1634    grok.template('datacenterexportpage')
1635    grok.require('waeup.exportData')
1636    label = _('Download portal data as CSV file')
1637    pnav = 0
1638    export_button = _(u'Create CSV file')
1639    cancel_button =_(u'Back to Data Center')
1640
1641    def getExporters(self):
1642        exporter_utils = getUtilitiesFor(ICSVExporter)
1643        ikoba_utils = getUtility(IIkobaUtils)
1644        title_name_tuples = [
1645            (util.title, name) for name, util in exporter_utils
1646            if name in ikoba_utils.EXPORTER_NAMES]
1647        return sorted(title_name_tuples)
1648
1649    def update(self, CREATE=None, DISCARD=None, PURGE=None, exporter=None,
1650               job_id=None, CANCEL=None):
1651        if CANCEL is not None:
1652            self.redirect(self.url(self.context))
1653            return
1654        if CREATE:
1655            utils = queryUtility(IIkobaUtils)
1656            if utils.export_disabled_message():
1657                self.flash(utils.export_disabled_message(), type='danger')
1658                self.entries = doll_up(self, user=None)
1659                return
1660            if running_jobs(self):
1661                self.flash(_(
1662                    "Sorry, another export job is running. "
1663                    "Please try again later."), type='danger')
1664                self.entries = doll_up(self, user=None)
1665                return
1666            job_id = self.context.start_export_job(
1667                exporter, self.request.principal.id)
1668            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1669            self.context.logger.info(
1670                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
1671        if DISCARD and job_id:
1672            entry = self.context.entry_from_job_id(job_id)
1673            self.context.delete_export_entry(entry)
1674            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1675            self.context.logger.info(
1676                '%s - discarded: job_id=%s' % (ob_class, job_id))
1677            self.flash(_('Discarded export') + ' %s' % job_id)
1678        self.entries = doll_up(self, user=None, purge=PURGE)
1679        if PURGE:
1680            self.flash(_('Exports successfully purged.'))
1681        return
1682
1683class ExportCSVView(grok.View):
1684    grok.context(IDataCenter)
1685    grok.name('download_export')
1686    grok.require('waeup.exportData')
1687
1688    def render(self, job_id=None):
1689        manager = getUtility(IJobManager)
1690        job = manager.get(job_id)
1691        if job is None:
1692            return
1693        if hasattr(job.result, 'traceback'):
1694            # XXX: Some error happened. Do something more approriate here...
1695            return
1696        path = job.result
1697        if not os.path.exists(path):
1698            # XXX: Do something more appropriate here...
1699            return
1700        result = open(path, 'rb').read()
1701        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
1702        filename = "%s_%s" % (acronym, os.path.basename(path))
1703        filename = filename.replace('.csv', '_%s.csv' % job_id)
1704        self.response.setHeader(
1705            'Content-Type', 'text/csv; charset=UTF-8')
1706        self.response.setHeader(
1707            'Content-Disposition', 'attachment; filename="%s' % filename)
1708        # remove job and running_exports entry from context
1709        #self.context.delete_export_entry(
1710        #    self.context.entry_from_job_id(job_id))
1711        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1712        self.context.logger.info(
1713            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
1714        return result
1715
1716class SwitchMaintModePage(UtilityView, grok.View):
1717    """Import managers must disable authentication for all other users
1718    before starting an import. This pages switches maintenance mode
1719    on and off.
1720    """
1721    grok.context(IDataCenter)
1722    grok.name('switchmaintmode')
1723    grok.require('waeup.importData')
1724
1725    def update(self):
1726        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1727        if grok.getSite()['configuration'].maintmode_enabled_by:
1728            grok.getSite()['configuration'].maintmode_enabled_by = None
1729            self.context.logger.info('%s - maintmode disabled' % ob_class)
1730            self.flash(_('Maintenance mode disabled.'))
1731            self.redirect(self.url(self.context))
1732        else:
1733            grok.getSite()['configuration'].maintmode_enabled_by = unicode(
1734                self.request.principal.id)
1735            self.context.logger.info('%s - maintmode enabled' % ob_class)
1736            self.flash(_('Maintenance mode enabled.'), type='warning')
1737            self.redirect(self.url(self.context, 'import1'))
1738        return
1739
1740    def render(self):
1741        return
1742
1743
1744class ChangePasswordRequestPage(IkobaFormPage):
1745    """Captcha'd page for all kind of users to request a password change.
1746    """
1747    grok.context(ICompany)
1748    grok.name('changepw')
1749    grok.require('waeup.Anonymous')
1750    grok.template('changepw')
1751    label = _('Send me a new password')
1752    form_fields = grok.AutoFields(IChangePassword)
1753
1754    def update(self):
1755        # Handle captcha
1756        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1757        self.captcha_result = self.captcha.verify(self.request)
1758        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1759        return
1760
1761    def _searchUser(self, email):
1762        # Search customer
1763        cat = queryUtility(ICatalog, name='customers_catalog')
1764        results = list(cat.searchResults(email=(email,email)))
1765        if len(results) == 1:
1766            return results[0]
1767        # Search portal user
1768        users = grok.getSite()['users'].values()
1769        results = []
1770        for user in users:
1771            if user.email == email:
1772                results.append(user)
1773            if len(results) == 1:
1774                return results[0]
1775        return None
1776
1777    @action(_('Send login credentials to email address'), style='primary')
1778    def request(self, **data):
1779        if not self.captcha_result.is_valid:
1780            # Captcha will display error messages automatically.
1781            # No need to flash something.
1782            return
1783        # Search customer
1784        email = data['email']
1785        user = self._searchUser(email)
1786        if user is None:
1787            self.flash(_('No record found.'), type='warning')
1788            return
1789        # Change password
1790        ikoba_utils = getUtility(IIkobaUtils)
1791        password = ikoba_utils.genPassword()
1792        mandate = PasswordMandate()
1793        mandate.params['password'] = password
1794        mandate.params['user'] = user
1795        site = grok.getSite()
1796        site['mandates'].addMandate(mandate)
1797        # Send email with credentials
1798        args = {'mandate_id':mandate.mandate_id}
1799        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1800        url_info = u'Confirmation link: %s' % mandate_url
1801        msg = _('You have successfully requested a password for the')
1802        success = ikoba_utils.sendCredentials(
1803            IUserAccount(user),password,url_info,msg)
1804        if success:
1805            self.flash(_('An email with your user name and password ' +
1806                'has been sent to ${a}.', mapping = {'a':email}))
1807        else:
1808            self.flash(_('An smtp server error occurred.'), type='danger')
1809        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1810        self.context.logger.info(
1811            '%s - %s - %s' % (ob_class, IUserAccount(user).name, data['email']))
1812        return
Note: See TracBrowser for help on using the repository browser.