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

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

Show only processors which are in BATCH_PROCESSOR_NAMES.

  • Property svn:keywords set to Id
File size: 65.9 KB
Line 
1## $Id: pages.py 14183 2016-09-23 16:28:55Z 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.context(IConfigurationContainer)
942    pnav = 0
943    label = _(u'Edit portal configuration')
944    form_fields = grok.AutoFields(IConfigurationContainer).omit(
945        'frontpage_dict')
946    form_fields['maintmode_enabled_by'].for_display = True
947
948    @action(_('Save'), style='primary')
949    def save(self, **data):
950        msave(self, **data)
951        frontpage = getattr(self.context, 'frontpage', None)
952        portal_language = getUtility(IIkobaUtils).PORTAL_LANGUAGE
953        self.context.frontpage_dict = html2dict(frontpage, portal_language)
954        return
955
956    @action(_('Update plugins'),
957              tooltip=_('For experts only!'),
958              warning=_('Plugins may only be updated after software upgrades. '
959                        'Are you really sure?'),
960              validator=NullValidator)
961    def updatePlugins(self, **data):
962        grok.getSite().updatePlugins()
963        self.flash(_('Plugins were updated. See log file for details.'))
964        return
965
966#
967# Datacenter pages...
968#
969
970class DatacenterPage(IkobaEditFormPage):
971    grok.context(IDataCenter)
972    grok.name('index')
973    grok.require('waeup.manageDataCenter')
974    label = _(u'Data Center')
975    pnav = 0
976
977    @jsaction(_('Remove selected'))
978    def delFiles(self, **data):
979        form = self.request.form
980        if 'val_id' in form:
981            child_id = form['val_id']
982        else:
983            self.flash(_('No item selected.'), type='danger')
984            return
985        if not isinstance(child_id, list):
986            child_id = [child_id]
987        deleted = []
988        for id in child_id:
989            fullpath = os.path.join(self.context.storage, id)
990            try:
991                os.remove(fullpath)
992                deleted.append(id)
993            except OSError:
994                self.flash(_('OSError: The file could not be deleted.'),
995                           type='danger')
996                return
997        if len(deleted):
998            self.flash(_('Successfully deleted: ${a}',
999                mapping = {'a': ', '.join(deleted)}))
1000            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1001            self.context.logger.info(
1002                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
1003        return
1004
1005class DatacenterFinishedPage(IkobaEditFormPage):
1006    grok.context(IDataCenter)
1007    grok.name('processed')
1008    grok.require('waeup.manageDataCenter')
1009    label = _(u'Processed Files')
1010    pnav = 0
1011    cancel_button =_('Back to Data Center')
1012
1013    def update(self, CANCEL=None):
1014        if CANCEL is not None:
1015            self.redirect(self.url(self.context))
1016            return
1017        return super(DatacenterFinishedPage, self).update()
1018
1019class DatacenterUploadPage(IkobaPage):
1020    grok.context(IDataCenter)
1021    grok.name('upload')
1022    grok.require('waeup.manageDataCenter')
1023    label = _(u'Upload portal data as CSV file')
1024    pnav = 0
1025    max_files = 20
1026    upload_button =_(u'Upload')
1027    cancel_button =_(u'Back to Data Center')
1028
1029    def getPreviewHeader(self):
1030        """Get the header fields of uploaded CSV file.
1031        """
1032        reader = csv.reader(open(self.fullpath, 'rb'))
1033        return reader.next()
1034
1035    def _notifyImportManagers(self, filename,
1036        normalized_filename, importer, import_mode):
1037        """Send email to Import Managers
1038        """
1039        # Get information about file
1040        self.fullpath = os.path.join(self.context.storage, normalized_filename)
1041        uploadfile = DataCenterFile(self.fullpath)
1042        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1043        table = getPreviewTable(self, 3)
1044        mail_table = ''
1045        for line in table:
1046            header = line[0]
1047            data = str(line[1:]).strip('[').strip(']')
1048            mail_table += '%s: %s ...\n' % (line[0], data)
1049        # Collect all recipient addresses
1050        ikoba_utils = getUtility(IIkobaUtils)
1051        import_managers = get_users_with_role(
1052            'waeup.ImportManager', grok.getSite())
1053        rcpt_addrs = ','.join(
1054            [user['user_email'] for user in import_managers if
1055                user['user_email'] is not None])
1056        if rcpt_addrs:
1057            config = grok.getSite()['configuration']
1058            fullname = self.request.principal.title
1059            try:
1060                email = self.request.principal.email
1061            except AttributeError:
1062                email = config.email_admin
1063            username = self.request.principal.id
1064            usertype = getattr(self.request.principal,
1065                               'user_type', 'system').title()
1066            rcpt_name = _('Import Manager')
1067            subject = translate(
1068                      _('${a}: ${b} uploaded',
1069                      mapping = {'a':config.acronym, 'b':filename}),
1070                      'waeup.ikoba',
1071                      target_language=ikoba_utils.PORTAL_LANGUAGE)
1072            text = _("""File: ${a}
1073Importer: ${b}
1074Import Mode: ${c}
1075Datasets: ${d}
1076
1077${e}
1078
1079Comment by Import Manager:""", mapping = {'a':normalized_filename,
1080                'b':importer,
1081                'c':import_mode,
1082                'd':uploadfile.lines - 1,
1083                'e':mail_table})
1084            success = ikoba_utils.sendContactForm(
1085                    fullname,email,
1086                    rcpt_name,rcpt_addrs,
1087                    username,usertype,config.name,
1088                    text,subject)
1089            if success:
1090                self.flash(
1091                    _('All import managers have been notified by email.'))
1092            else:
1093                self.flash(_('An smtp server error occurred.'), type='danger')
1094            return
1095
1096    def update(self, uploadfile=None, import_mode=None,
1097               importer=None, CANCEL=None, SUBMIT=None):
1098        number_of_pendings = len(self.context.getPendingFiles())
1099        if number_of_pendings > self.max_files:
1100            self.flash(
1101                _('Maximum number of files in the data center exceeded.'),
1102                  type='danger')
1103            self.redirect(self.url(self.context))
1104            return
1105        if CANCEL is not None:
1106            self.redirect(self.url(self.context))
1107            return
1108        if not uploadfile:
1109            return
1110        try:
1111            filename = uploadfile.filename
1112            #if 'pending' in filename:
1113            #    self.flash(_("You can't re-upload pending data files."), type='danger')
1114            #    return
1115            if not filename.endswith('.csv'):
1116                self.flash(_("Only csv files are allowed."), type='danger')
1117                return
1118            normalized_filename = self.getNormalizedFileName(filename)
1119            finished_file = os.path.join(
1120                self.context.storage, 'finished', normalized_filename)
1121            unfinished_file = os.path.join(
1122                self.context.storage, 'unfinished', normalized_filename)
1123            if os.path.exists(finished_file) or os.path.exists(unfinished_file):
1124                self.flash(_("File with same name was uploaded earlier."),
1125                           type='danger')
1126                return
1127            target = os.path.join(self.context.storage, normalized_filename)
1128            filecontent = uploadfile.read()
1129            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1130            logger = self.context.logger
1131
1132            # Forbid certain characters in import files.
1133            invalid_line = check_csv_charset(filecontent.splitlines())
1134            if invalid_line:
1135                self.flash(_(
1136                    "Your file contains forbidden characters or "
1137                    "has invalid CSV format. "
1138                    "First problematic line detected: line %s. "
1139                    "Please replace." % invalid_line), type='danger')
1140                logger.info('%s - invalid file uploaded: %s' %
1141                            (ob_class, target))
1142                return
1143
1144            open(target, 'wb').write(filecontent)
1145            os.chmod(target, 0664)
1146            logger.info('%s - uploaded: %s' % (ob_class, target))
1147            self._notifyImportManagers(filename,
1148                normalized_filename, importer, import_mode)
1149
1150        except IOError:
1151            self.flash('Error while uploading file. Please retry.', type='danger')
1152            self.flash('I/O error: %s' % sys.exc_info()[1], type='danger')
1153            return
1154        self.redirect(self.url(self.context))
1155
1156    def getNormalizedFileName(self, filename):
1157        """Build sane filename.
1158
1159        An uploaded file foo.csv will be stored as foo_USERNAME.csv
1160        where username is the principal id of the currently logged in
1161        user.
1162
1163        Spaces in filename are replaced by underscore.
1164        Pending data filenames remain unchanged.
1165        """
1166        if filename.endswith('.pending.csv'):
1167            return filename
1168        username = self.request.principal.id
1169        filename = filename.replace(' ', '_')
1170        # Only accept typical filname chars...
1171        filtered_username = ''.join(re.findall('[a-zA-Z0-9_\.\-]', username))
1172        base, ext = os.path.splitext(filename)
1173        return '%s_%s%s' % (base, filtered_username, ext.lower())
1174
1175    def getImporters(self):
1176        return getImporters(self.context)
1177
1178class FileDownloadView(UtilityView, grok.View):
1179    grok.context(IDataCenter)
1180    grok.name('download')
1181    grok.require('waeup.manageDataCenter')
1182
1183    def update(self, filename=None):
1184        self.filename = self.request.form['filename']
1185        return
1186
1187    def render(self):
1188        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1189        self.context.logger.info(
1190            '%s - downloaded: %s' % (ob_class, self.filename))
1191        self.response.setHeader(
1192            'Content-Type', 'text/csv; charset=UTF-8')
1193        self.response.setHeader(
1194            'Content-Disposition:', 'attachment; filename="%s' %
1195            self.filename.replace('finished/',''))
1196        fullpath = os.path.join(self.context.storage, self.filename)
1197        return open(fullpath, 'rb').read()
1198
1199class DatacenterImportStep1(IkobaPage):
1200    """Manual import step 1: choose file
1201    """
1202    grok.context(IDataCenter)
1203    grok.name('import1')
1204    grok.template('datacenterimport1page')
1205    grok.require('waeup.manageDataCenter')
1206    label = _(u'Process CSV file')
1207    pnav = 0
1208    cancel_button =_(u'Back to Data Center')
1209
1210    def getFiles(self):
1211        files = self.context.getPendingFiles(sort='date')
1212        for file in files:
1213            name = file.name
1214            if not name.endswith('.csv') and not name.endswith('.pending'):
1215                continue
1216            yield file
1217
1218    def update(self, filename=None, select=None, cancel=None):
1219        if not grok.getSite()['configuration'].maintmode_enabled_by and \
1220            not self.request.principal.id == 'admin':
1221            self.flash(
1222                _('Portal must be in maintenance mode for data import.'),
1223                type='warning')
1224            self.redirect(self.url(self.context))
1225            return
1226        if cancel is not None:
1227            self.redirect(self.url(self.context))
1228            return
1229        if select is not None:
1230            # A filename was selected
1231            session = ISession(self.request)['waeup.ikoba']
1232            session['import_filename'] = select
1233            self.redirect(self.url(self.context, '@@import2'))
1234        return
1235
1236class DatacenterImportStep2(IkobaPage):
1237    """Manual import step 2: choose processor
1238    """
1239    grok.context(IDataCenter)
1240    grok.name('import2')
1241    grok.template('datacenterimport2page')
1242    grok.require('waeup.manageDataCenter')
1243    label = _(u'Process CSV file')
1244    pnav = 0
1245    cancel_button =_(u'Cancel')
1246    back_button =_(u'Back to step 1')
1247    proceed_button =_(u'Proceed to step 3')
1248
1249    filename = None
1250    mode = 'create'
1251    importer = None
1252    mode_locked = False
1253
1254    def getPreviewHeader(self):
1255        """Get the header fields of attached CSV file.
1256        """
1257        reader = csv.reader(open(self.fullpath, 'rb'))
1258        return reader.next()
1259
1260    def getPreviewTable(self):
1261        return getPreviewTable(self, 3)
1262
1263    def getImporters(self):
1264        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
1265        ikoba_utils = getUtility(IIkobaUtils)
1266        importers = sorted(
1267            [dict(title=x.name, name=x.util_name) for x in importers
1268            if x.util_name in ikoba_utils.BATCH_PROCESSOR_NAMES])
1269        return importers
1270
1271    def getModeFromFilename(self, filename):
1272        """Lookup filename or path and return included mode name or None.
1273        """
1274        if not filename.endswith('.pending.csv'):
1275            return None
1276        base = os.path.basename(filename)
1277        parts = base.rsplit('.', 3)
1278        if len(parts) != 4:
1279            return None
1280        if parts[1] not in ['create', 'update', 'remove']:
1281            return None
1282        return parts[1]
1283
1284    def getWarnings(self):
1285        import sys
1286        result = []
1287        try:
1288            headerfields = self.getPreviewHeader()
1289            headerfields_clean = list(set(headerfields))
1290            if len(headerfields) > len(headerfields_clean):
1291                result.append(
1292                    _("Double headers: each column name may only appear once. "))
1293        except:
1294            fatal = '%s' % sys.exc_info()[1]
1295            result.append(fatal)
1296        if result:
1297            warnings = ""
1298            for line in result:
1299                warnings += line + '<br />'
1300            warnings += _('Replace imported file!')
1301            return warnings
1302        return False
1303
1304    def update(self, mode=None, importer=None,
1305               back1=None, cancel=None, proceed=None):
1306        session = ISession(self.request)['waeup.ikoba']
1307        self.filename = session.get('import_filename', None)
1308
1309        if self.filename is None or back1 is not None:
1310            self.redirect(self.url(self.context, '@@import1'))
1311            return
1312        if cancel is not None:
1313            self.flash(_('Import aborted.'), type='warning')
1314            self.redirect(self.url(self.context))
1315            return
1316        self.mode = mode or session.get('import_mode', self.mode)
1317        filename_mode = self.getModeFromFilename(self.filename)
1318        if filename_mode is not None:
1319            self.mode = filename_mode
1320            self.mode_locked = True
1321        self.importer = importer or session.get('import_importer', None)
1322        session['import_importer'] = self.importer
1323        if self.importer and 'update' in self.importer:
1324            if self.mode != 'update':
1325                self.flash(_('Update mode only!'), type='warning')
1326                self.mode_locked = True
1327                self.mode = 'update'
1328                proceed = None
1329        session['import_mode'] = self.mode
1330        if proceed is not None:
1331            self.redirect(self.url(self.context, '@@import3'))
1332            return
1333        self.fullpath = os.path.join(self.context.storage, self.filename)
1334        warnings = self.getWarnings()
1335        if not warnings:
1336            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1337        else:
1338            self.reader = ()
1339            self.flash(warnings, type='warning')
1340
1341class DatacenterImportStep3(IkobaPage):
1342    """Manual import step 3: modify header
1343    """
1344    grok.context(IDataCenter)
1345    grok.name('import3')
1346    grok.template('datacenterimport3page')
1347    grok.require('waeup.manageDataCenter')
1348    label = _(u'Process CSV file')
1349    pnav = 0
1350    cancel_button =_(u'Cancel')
1351    reset_button =_(u'Reset')
1352    update_button =_(u'Set headerfields')
1353    back_button =_(u'Back to step 2')
1354    proceed_button =_(u'Perform import')
1355
1356    filename = None
1357    mode = None
1358    importername = None
1359
1360    @property
1361    def nextstep(self):
1362        return self.url(self.context, '@@import4')
1363
1364    def getPreviewHeader(self):
1365        """Get the header fields of attached CSV file.
1366        """
1367        reader = csv.reader(open(self.fullpath, 'rb'))
1368        return reader.next()
1369
1370    def getPreviewTable(self):
1371        """Get transposed table with 1 sample record.
1372
1373        The first column contains the headers.
1374        """
1375        if not self.reader:
1376            return
1377        headers = self.getPreviewHeader()
1378        num = 0
1379        data = []
1380        for line in self.reader:
1381            if num > 0:
1382                break
1383            num += 1
1384            data.append(line)
1385        result = []
1386        field_num = 0
1387        for name in headers:
1388            result_line = []
1389            result_line.append(field_num)
1390            field_num += 1
1391            for d in data:
1392                result_line.append(d[name])
1393            result.append(result_line)
1394        return result
1395
1396    def getPossibleHeaders(self):
1397        """Get the possible headers.
1398
1399        The headers are described as dicts {value:internal_name,
1400        title:displayed_name}
1401        """
1402        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
1403        headers = self.importer.getHeaders()
1404        result.extend([dict(title=x, value=x) for x in headers])
1405        return result
1406
1407    def getWarnings(self):
1408        import sys
1409        result = []
1410        try:
1411            self.importer.checkHeaders(self.headerfields, mode=self.mode)
1412        except:
1413            fatal = '%s' % sys.exc_info()[1]
1414            result.append(fatal)
1415        if result:
1416            warnings = ""
1417            for line in result:
1418                warnings += line + '<br />'
1419            warnings += _('Edit headers or replace imported file!')
1420            return warnings
1421        return False
1422
1423    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
1424        session = ISession(self.request)['waeup.ikoba']
1425        self.filename = session.get('import_filename', None)
1426        self.mode = session.get('import_mode', None)
1427        self.importername = session.get('import_importer', None)
1428
1429        if None in (self.filename, self.mode, self.importername):
1430            self.redirect(self.url(self.context, '@@import2'))
1431            return
1432        if back2 is not None:
1433            self.redirect(self.url(self.context ,'@@import2'))
1434            return
1435        if cancel is not None:
1436            self.flash(_('Import aborted.'), type='warning')
1437            self.redirect(self.url(self.context))
1438            return
1439
1440        self.fullpath = os.path.join(self.context.storage, self.filename)
1441        self.headerfields = headerfield or self.getPreviewHeader()
1442        session['import_headerfields'] = self.headerfields
1443
1444        if proceed is not None:
1445            self.redirect(self.url(self.context, '@@import4'))
1446            return
1447        self.importer = getUtility(IBatchProcessor, name=self.importername)
1448        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1449        warnings = self.getWarnings()
1450        if warnings:
1451            self.flash(warnings, type='warning')
1452
1453class DatacenterImportStep4(IkobaPage):
1454    """Manual import step 4: do actual import
1455    """
1456    grok.context(IDataCenter)
1457    grok.name('import4')
1458    grok.template('datacenterimport4page')
1459    grok.require('waeup.importData')
1460    label = _(u'Process CSV file')
1461    pnav = 0
1462    back_button =_(u'Process next')
1463
1464    filename = None
1465    mode = None
1466    importername = None
1467    headerfields = None
1468    warnnum = None
1469
1470    def update(self, back=None, finish=None, showlog=None):
1471        if finish is not None:
1472            self.redirect(self.url(self.context, '@@import1'))
1473            return
1474        if not grok.getSite()['configuration'].maintmode_enabled_by and \
1475            not self.request.principal.id == 'admin':
1476            self.flash(
1477                _('File has not been imported. '
1478                  'Portal must be in maintenance mode for data import.'),
1479                type='danger')
1480            self.redirect(self.url(self.context))
1481            return
1482        session = ISession(self.request)['waeup.ikoba']
1483        self.filename = session.get('import_filename', None)
1484        self.mode = session.get('import_mode', None)
1485        self.importername = session.get('import_importer', None)
1486        # If the import file contains only one column
1487        # the import_headerfields attribute is a string.
1488        ihf = session.get('import_headerfields', None)
1489        if not isinstance(ihf, list):
1490            self.headerfields = ihf.split()
1491        else:
1492            self.headerfields = ihf
1493
1494        if None in (self.filename, self.mode, self.importername,
1495                    self.headerfields):
1496            self.redirect(self.url(self.context, '@@import3'))
1497            return
1498
1499        if showlog is not None:
1500            logfilename = "datacenter.log"
1501            session['logname'] = logfilename
1502            self.redirect(self.url(self.context, '@@show'))
1503            return
1504
1505        self.fullpath = os.path.join(self.context.storage, self.filename)
1506        self.importer = getUtility(IBatchProcessor, name=self.importername)
1507
1508        # Perform batch processing...
1509        # XXX: This might be better placed in datacenter module.
1510        (linenum, self.warn_num,
1511         fin_path, pending_path) = self.importer.doImport(
1512            self.fullpath, self.headerfields, self.mode,
1513            self.request.principal.id, logger=self.context.logger)
1514        # Put result files in desired locations...
1515        self.context.distProcessedFiles(
1516            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1517            self.mode)
1518        # Disable maintenance mode if set.
1519        if grok.getSite()['configuration'].maintmode_enabled_by:
1520            grok.getSite()['configuration'].maintmode_enabled_by = None
1521            self.flash(_('Maintenance mode disabled.'))
1522
1523        if self.warn_num:
1524            self.flash(_('Processing of ${a} rows failed.',
1525                mapping = {'a':self.warn_num}), type='warning')
1526        self.flash(_('Successfully processed ${a} rows.',
1527            mapping = {'a':linenum - self.warn_num}))
1528
1529class DatacenterLogsOverview(IkobaPage):
1530    grok.context(IDataCenter)
1531    grok.name('logs')
1532    grok.template('datacenterlogspage')
1533    grok.require('waeup.manageDataCenter')
1534    label = _(u'Show logfiles')
1535    pnav = 0
1536    back_button = _(u'Back to Data Center')
1537    show_button = _(u'Show')
1538
1539    def update(self, back=None):
1540        if back is not None:
1541            self.redirect(self.url(self.context))
1542            return
1543        self.files = self.context.getLogFiles()
1544
1545class DatacenterLogsFileview(IkobaPage):
1546    grok.context(IDataCenter)
1547    grok.name('show')
1548    grok.template('datacenterlogsshowfilepage')
1549    grok.require('waeup.manageDataCenter')
1550    title = _(u'Data Center')
1551    pnav = 0
1552    search_button = _('Search')
1553    back_button = _('Back to Data Center')
1554    placeholder = _('Enter a regular expression here...')
1555
1556    def label(self):
1557        return "Logfile %s" % self.filename
1558
1559    def update(self, back=None, query=None, logname=None):
1560        if os.name != 'posix':
1561            self.flash(
1562                _('Log files can only be searched ' +
1563                  'on Unix-based operating systems.'), type='danger')
1564            self.redirect(self.url(self.context, '@@logs'))
1565            return
1566        if back is not None or logname is None:
1567            self.redirect(self.url(self.context, '@@logs'))
1568            return
1569        self.filename = logname
1570        self.query = query
1571        if not query:
1572            return
1573        try:
1574            self.result = cgi.escape(
1575                ''.join(self.context.queryLogfiles(logname, query)))
1576        except ValueError:
1577            self.flash(_('Invalid search expression.'), type='danger')
1578            return
1579        if not self.result:
1580            self.flash(_('No search results found.'), type='warning')
1581        return
1582
1583class DatacenterSettings(IkobaPage):
1584    grok.context(IDataCenter)
1585    grok.name('manage')
1586    grok.template('datacentermanagepage')
1587    grok.require('waeup.managePortal')
1588    label = _('Edit data center settings')
1589    pnav = 0
1590    save_button =_(u'Save')
1591    reset_button =_(u'Reset')
1592    cancel_button =_(u'Back to Data Center')
1593
1594    def update(self, newpath=None, move=False, overwrite=False,
1595               save=None, cancel=None):
1596        if move:
1597            move = True
1598        if overwrite:
1599            overwrite = True
1600        if newpath is None:
1601            return
1602        if cancel is not None:
1603            self.redirect(self.url(self.context))
1604            return
1605        try:
1606            not_copied = self.context.setStoragePath(newpath, move=move)
1607            for name in not_copied:
1608                self.flash(_('File already existed (not copied): ${a}',
1609                    mapping = {'a':name}), type='danger')
1610        except:
1611            self.flash(_('Given storage path cannot be used: ${a}',
1612                        mapping = {'a':sys.exc_info()[1]}), type='danger')
1613            return
1614        if newpath:
1615            self.flash(_('New storage path succefully set.'))
1616            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1617            self.context.logger.info(
1618                '%s - storage path set: %s' % (ob_class, newpath))
1619            self.redirect(self.url(self.context))
1620        return
1621
1622class ExportCSVPage(IkobaPage):
1623    grok.context(IDataCenter)
1624    grok.name('export')
1625    grok.template('datacenterexportpage')
1626    grok.require('waeup.exportData')
1627    label = _('Download portal data as CSV file')
1628    pnav = 0
1629    export_button = _(u'Create CSV file')
1630    cancel_button =_(u'Back to Data Center')
1631
1632    def getExporters(self):
1633        exporter_utils = getUtilitiesFor(ICSVExporter)
1634        ikoba_utils = getUtility(IIkobaUtils)
1635        title_name_tuples = [
1636            (util.title, name) for name, util in exporter_utils
1637            if name in ikoba_utils.EXPORTER_NAMES]
1638        return sorted(title_name_tuples)
1639
1640    def update(self, CREATE=None, DISCARD=None, PURGE=None, exporter=None,
1641               job_id=None, CANCEL=None):
1642        if CANCEL is not None:
1643            self.redirect(self.url(self.context))
1644            return
1645        if CREATE:
1646            utils = queryUtility(IIkobaUtils)
1647            if utils.export_disabled_message():
1648                self.flash(utils.export_disabled_message(), type='danger')
1649                self.entries = doll_up(self, user=None)
1650                return
1651            if running_jobs(self):
1652                self.flash(_(
1653                    "Sorry, another export job is running. "
1654                    "Please try again later."), type='danger')
1655                self.entries = doll_up(self, user=None)
1656                return
1657            job_id = self.context.start_export_job(
1658                exporter, self.request.principal.id)
1659            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1660            self.context.logger.info(
1661                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
1662        if DISCARD and job_id:
1663            entry = self.context.entry_from_job_id(job_id)
1664            self.context.delete_export_entry(entry)
1665            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1666            self.context.logger.info(
1667                '%s - discarded: job_id=%s' % (ob_class, job_id))
1668            self.flash(_('Discarded export') + ' %s' % job_id)
1669        self.entries = doll_up(self, user=None, purge=PURGE)
1670        if PURGE:
1671            self.flash(_('Exports successfully purged.'))
1672        return
1673
1674class ExportCSVView(grok.View):
1675    grok.context(IDataCenter)
1676    grok.name('download_export')
1677    grok.require('waeup.exportData')
1678
1679    def render(self, job_id=None):
1680        manager = getUtility(IJobManager)
1681        job = manager.get(job_id)
1682        if job is None:
1683            return
1684        if hasattr(job.result, 'traceback'):
1685            # XXX: Some error happened. Do something more approriate here...
1686            return
1687        path = job.result
1688        if not os.path.exists(path):
1689            # XXX: Do something more appropriate here...
1690            return
1691        result = open(path, 'rb').read()
1692        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
1693        filename = "%s_%s" % (acronym, os.path.basename(path))
1694        filename = filename.replace('.csv', '_%s.csv' % job_id)
1695        self.response.setHeader(
1696            'Content-Type', 'text/csv; charset=UTF-8')
1697        self.response.setHeader(
1698            'Content-Disposition', 'attachment; filename="%s' % filename)
1699        # remove job and running_exports entry from context
1700        #self.context.delete_export_entry(
1701        #    self.context.entry_from_job_id(job_id))
1702        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1703        self.context.logger.info(
1704            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
1705        return result
1706
1707class SwitchMaintModePage(UtilityView, grok.View):
1708    """Import managers must disable authentication for all other users
1709    before starting an import. This pages switches maintenance mode
1710    on and off.
1711    """
1712    grok.context(IDataCenter)
1713    grok.name('switchmaintmode')
1714    grok.require('waeup.importData')
1715
1716    def update(self):
1717        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1718        if grok.getSite()['configuration'].maintmode_enabled_by:
1719            grok.getSite()['configuration'].maintmode_enabled_by = None
1720            self.context.logger.info('%s - maintmode disabled' % ob_class)
1721            self.flash(_('Maintenance mode disabled.'))
1722            self.redirect(self.url(self.context))
1723        else:
1724            grok.getSite()['configuration'].maintmode_enabled_by = unicode(
1725                self.request.principal.id)
1726            self.context.logger.info('%s - maintmode enabled' % ob_class)
1727            self.flash(_('Maintenance mode enabled.'), type='warning')
1728            self.redirect(self.url(self.context, 'import1'))
1729        return
1730
1731    def render(self):
1732        return
1733
1734
1735class ChangePasswordRequestPage(IkobaFormPage):
1736    """Captcha'd page for all kind of users to request a password change.
1737    """
1738    grok.context(ICompany)
1739    grok.name('changepw')
1740    grok.require('waeup.Anonymous')
1741    grok.template('changepw')
1742    label = _('Send me a new password')
1743    form_fields = grok.AutoFields(IChangePassword)
1744
1745    def update(self):
1746        # Handle captcha
1747        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1748        self.captcha_result = self.captcha.verify(self.request)
1749        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1750        return
1751
1752    def _searchUser(self, email):
1753        # Search customer
1754        cat = queryUtility(ICatalog, name='customers_catalog')
1755        results = list(cat.searchResults(email=(email,email)))
1756        if len(results) == 1:
1757            return results[0]
1758        # Search portal user
1759        users = grok.getSite()['users'].values()
1760        results = []
1761        for user in users:
1762            if user.email == email:
1763                results.append(user)
1764            if len(results) == 1:
1765                return results[0]
1766        return None
1767
1768    @action(_('Send login credentials to email address'), style='primary')
1769    def request(self, **data):
1770        if not self.captcha_result.is_valid:
1771            # Captcha will display error messages automatically.
1772            # No need to flash something.
1773            return
1774        # Search customer
1775        email = data['email']
1776        user = self._searchUser(email)
1777        if user is None:
1778            self.flash(_('No record found.'), type='warning')
1779            return
1780        # Change password
1781        ikoba_utils = getUtility(IIkobaUtils)
1782        password = ikoba_utils.genPassword()
1783        mandate = PasswordMandate()
1784        mandate.params['password'] = password
1785        mandate.params['user'] = user
1786        site = grok.getSite()
1787        site['mandates'].addMandate(mandate)
1788        # Send email with credentials
1789        args = {'mandate_id':mandate.mandate_id}
1790        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
1791        url_info = u'Confirmation link: %s' % mandate_url
1792        msg = _('You have successfully requested a password for the')
1793        success = ikoba_utils.sendCredentials(
1794            IUserAccount(user),password,url_info,msg)
1795        if success:
1796            self.flash(_('An email with your user name and password ' +
1797                'has been sent to ${a}.', mapping = {'a':email}))
1798        else:
1799            self.flash(_('An smtp server error occurred.'), type='danger')
1800        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
1801        self.context.logger.info(
1802            '%s - %s - %s' % (ob_class, IUserAccount(user).name, data['email']))
1803        return
Note: See TracBrowser for help on using the repository browser.