From 079ccafc75c124010b8998a4c28d762f391670fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Tue, 4 Jul 2017 08:34:26 +0200 Subject: [PATCH] monkeypatch quixote with thread-aware functions (#6735) --- tests/test_workflows.py | 2 +- tests/utilities.py | 16 +++---- wcs/__init__.py | 3 ++ wcs/backoffice/management.py | 4 +- wcs/compat.py | 18 +------- wcs/middleware.py | 11 ++++- wcs/monkeypatch.py | 90 ++++++++++++++++++++++++++++++++++++ wcs/qommon/__init__.py | 28 ++++++++++- wcs/qommon/errors.py | 4 +- wcs/qommon/ident/password.py | 2 +- wcs/qommon/publisher.py | 17 +++++-- wcs/qommon/substitution.py | 2 +- wcs/qommon/template.py | 2 +- wcs/wscalls.py | 8 ++-- 14 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 wcs/monkeypatch.py diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 456d209ef..de9391d6c 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -1361,7 +1361,7 @@ def test_sms_with_passerelle(pub): item.body = 'my message' with mock.patch('wcs.wscalls.get_secret_and_orig') as mocked_secret_and_orig: mocked_secret_and_orig.return_value = ('secret', 'localhost') - with mock.patch('wcs.wscalls.http_post_request') as mocked_http_post: + with mock.patch('qommon.misc.http_post_request') as mocked_http_post: mocked_http_post.return_value = ('response', '200', 'data', 'headers') item.perform(formdata) url, payload = mocked_http_post.call_args[0] diff --git a/tests/utilities.py b/tests/utilities.py index 8ab5ffb19..488d8a138 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -7,6 +7,7 @@ import random import psycopg2 import pytest import shutil +import sys import threading import urlparse @@ -22,6 +23,7 @@ from wcs import publisher, compat from wcs.qommon.http_request import HTTPRequest from wcs.users import User from wcs.tracking_code import TrackingCode +import wcs.qommon.emails import wcs.qommon.sms import qommon.sms from qommon.errors import ConnectionError @@ -195,21 +197,19 @@ class EmailsMocking(object): return len(self.emails) def __enter__(self): - import wcs.qommon.emails - import qommon.emails - self.wcs_create_smtp_server = wcs.qommon.emails.create_smtp_server - self.qommon_create_smtp_server = qommon.emails.create_smtp_server + self.wcs_create_smtp_server = sys.modules['wcs.qommon.emails'].create_smtp_server + self.qommon_create_smtp_server = sys.modules['qommon.emails'].create_smtp_server - wcs.qommon.emails.create_smtp_server = self.create_smtp_server - qommon.emails.create_smtp_server = self.create_smtp_server + sys.modules['wcs.qommon.emails'].create_smtp_server = self.create_smtp_server + sys.modules['qommon.emails'].create_smtp_server = self.create_smtp_server self.emails = {} return self def __exit__(self, exc_type, exc_value, tb): del self.emails - wcs.qommon.emails.create_smtp_server = self.wcs_create_smtp_server - qommon.emails.create_smtp_server = self.qommon_create_smtp_server + sys.modules['wcs.qommon.emails'].create_smtp_server = self.wcs_create_smtp_server + sys.modules['qommon.emails'].create_smtp_server = self.qommon_create_smtp_server class MockSubstitutionVariables(object): diff --git a/wcs/__init__.py b/wcs/__init__.py index 2ae0be35a..a9923b73c 100644 --- a/wcs/__init__.py +++ b/wcs/__init__.py @@ -18,7 +18,10 @@ import sys import os sys.path.insert(0, os.path.dirname(__file__)) +import monkeypatch + import qommon +sys.modules['qommon'] = sys.modules['wcs.qommon'] import qommon.form sys.modules['form'] = qommon.form diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py index 71dff5627..564cad1ff 100644 --- a/wcs/backoffice/management.py +++ b/wcs/backoffice/management.py @@ -41,11 +41,11 @@ from qommon.evalutils import make_datetime from qommon.misc import C_, ellipsize from qommon.afterjobs import AfterJob from qommon import emails +import qommon.sms from qommon import errors from qommon import ezt from qommon import ods from qommon.form import * -from qommon.sms import SMS from qommon.storage import (Equal, NotEqual, LessOrEqual, GreaterOrEqual, Or, Intersects, ILike, FtsMatch, Contains, Null) @@ -131,7 +131,7 @@ class SendCodeFormdefDirectory(Directory): if get_publisher().use_sms_feature: sms_cfg = get_cfg('sms', {}) mode = sms_cfg.get('mode', 'none') - sms_class = SMS.get_sms_class(mode) + sms_class = qommon.sms.SMS.get_sms_class(mode) if sms_class: form.add(StringWidget, 'sms', title=_('SMS Number'), required=False) form.add(RadiobuttonsWidget, 'method', diff --git a/wcs/compat.py b/wcs/compat.py index 47d541055..e8af60f81 100644 --- a/wcs/compat.py +++ b/wcs/compat.py @@ -36,21 +36,6 @@ from .publisher import WcsPublisher from .qommon.http_request import HTTPRequest from .qommon.http_response import HTTPResponse -def init_publisher_if_needed(): - if get_publisher() is not None: - return get_publisher() - # initialize publisher in first request - config = ConfigParser.ConfigParser() - if settings.WCS_LEGACY_CONFIG_FILE: - config.read(settings.WCS_LEGACY_CONFIG_FILE) - if hasattr(settings, 'WCS_EXTRA_MODULES') and settings.WCS_EXTRA_MODULES: - if not config.has_section('extra'): - config.add_section('extra') - for i, extra in enumerate(settings.WCS_EXTRA_MODULES): - config.set('extra', 'cmd_line_extra_%d' % i, extra) - CompatWcsPublisher.configure(config) - return CompatWcsPublisher.create_publisher() - class TemplateWithFallbackView(TemplateView): quixote_response = None @@ -192,7 +177,8 @@ class CompatWcsPublisher(WcsPublisher): quixote_lock = Lock() def quixote(request): - with quixote_lock: + #with quixote_lock: + if True: pub = get_publisher() compat_request = CompatHTTPRequest(request) return pub.process_request(compat_request) diff --git a/wcs/middleware.py b/wcs/middleware.py index 11d47be1e..6c57c5398 100644 --- a/wcs/middleware.py +++ b/wcs/middleware.py @@ -14,12 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . -from .compat import init_publisher_if_needed, CompatHTTPRequest +import thread +import threading + +from quixote import get_publisher +from .compat import CompatHTTPRequest, CompatWcsPublisher + class PublisherInitialisationMiddleware(object): '''Initializes the publisher according to the request server name.''' def process_request(self, request): - pub = init_publisher_if_needed() + pub = get_publisher() + if not pub: + pub = CompatWcsPublisher.create_publisher() compat_request = CompatHTTPRequest(request) pub.init_publish(compat_request) pub._set_request(compat_request) diff --git a/wcs/monkeypatch.py b/wcs/monkeypatch.py new file mode 100644 index 000000000..cdde6f342 --- /dev/null +++ b/wcs/monkeypatch.py @@ -0,0 +1,90 @@ +# w.c.s. - web application for online forms +# Copyright (C) 2005-2017 Entr'ouvert +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +import threading +import types +import urlparse + +import quixote +import quixote.publish + +_thread_local = threading.local() + +cleanup_orig = quixote.publish.cleanup +PublisherOrig = quixote.publish.Publisher + +class Publisher(quixote.publish.Publisher): + def __init__(self, root_directory, *args, **kwargs): + try: + PublisherOrig.__init__(self, root_directory, *args, **kwargs) + except RuntimeError: + pass + _thread_local.publisher = self + self.root_directory = root_directory + self._request = None + +def get_publisher(): + return getattr(_thread_local, 'publisher', None) + +def get_request(): + return _thread_local.publisher.get_request() + +def get_response(): + return _thread_local.publisher.get_request().response + +def get_field(name, default=None): + return _thread_local.publisher.get_request().get_field(name, default) + +def get_cookie(name, default=None): + return _thread_local.publisher.get_request().get_cookie(name, default) + +def get_path(n=0): + return _thread_local.publisher.get_request().get_path(n) + +def redirect(location, permanent=False): + """(location : string, permanent : boolean = false) -> string + + Create a redirection response. If the location is relative, then it + will automatically be made absolute. The return value is an HTML + document indicating the new URL (useful if the client browser does + not honor the redirect). + """ + request = _thread_local.publisher.get_request() + location = urlparse.urljoin(request.get_url(), str(location)) + return request.response.redirect(location, permanent) + +def get_session(): + return _thread_local.publisher.get_request().session + +def get_session_manager(): + return _thread_local.publisher.session_manager + +def get_user(): + session = _thread_local.publisher.get_request().session + if session is None: + return None + else: + return session.user + +def cleanup(): + cleanup_orig() + _thread_local.publisher = None + + +for key, value in locals().items(): + if type(value) in (types.FunctionType, types.TypeType, types.ClassType): + setattr(quixote, key, value) + setattr(quixote.publish, key, value) diff --git a/wcs/qommon/__init__.py b/wcs/qommon/__init__.py index 3c7fc6566..ddce2fd35 100644 --- a/wcs/qommon/__init__.py +++ b/wcs/qommon/__init__.py @@ -14,6 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see . +import ConfigParser +import django.apps +from django.conf import settings from quixote import get_publisher try: @@ -33,7 +36,30 @@ def ngettext(*args): return message return unicode(pub.ngettext(*args), 'utf-8').encode(pub.site_charset) -from publisher import get_cfg, get_logger +from publisher import get_cfg, get_logger, get_publisher_class +import publisher +publisher._ = _ + + +class AppConfig(django.apps.AppConfig): + name = 'wcs.qommon' + + def ready(self): + config = ConfigParser.ConfigParser() + if settings.WCS_LEGACY_CONFIG_FILE: + config.read(settings.WCS_LEGACY_CONFIG_FILE) + if hasattr(settings, 'WCS_EXTRA_MODULES') and settings.WCS_EXTRA_MODULES: + if not config.has_section('extra'): + config.add_section('extra') + for i, extra in enumerate(settings.WCS_EXTRA_MODULES): + config.set('extra', 'cmd_line_extra_%d' % i, extra) + + get_publisher_class().configure(config) + get_publisher_class().register_tld_names = True + get_publisher_class().init_publisher_class() + +default_app_config = 'wcs.qommon.AppConfig' + if lasso: if not hasattr(lasso, 'SAML2_SUPPORT'): diff --git a/wcs/qommon/errors.py b/wcs/qommon/errors.py index b2ec8a13b..7572b2a43 100644 --- a/wcs/qommon/errors.py +++ b/wcs/qommon/errors.py @@ -21,7 +21,6 @@ import quixote from quixote.errors import * from quixote.html import TemplateIO, htmltext -from qommon import _ import template @@ -31,6 +30,7 @@ class AccessForbiddenError(AccessError): self.location_hint = location_hint def render(self): + from qommon import _ if self.public_msg: return template.error_page(self.public_msg, _('Access Forbidden'), continue_to = (get_publisher().get_root_url(), _('the homepage')), @@ -63,6 +63,7 @@ class EmailError(Exception): class InternalServerError(object): def render(self): + from qommon import _ template.html_top(_('Oops, the server borked severely')) r = TemplateIO(html=True) @@ -115,6 +116,7 @@ TraversalError.description = N_( def format_publish_error(exc): + from qommon import _ if getattr(exc, 'public_msg', None): return template.error_page(exc.format(), _(exc.title)) else: diff --git a/wcs/qommon/ident/password.py b/wcs/qommon/ident/password.py index ecf22b805..3e56b32f6 100644 --- a/wcs/qommon/ident/password.py +++ b/wcs/qommon/ident/password.py @@ -1187,7 +1187,7 @@ class PasswordAuthMethod(AuthMethod): @classmethod def register(cls): - rdb = get_publisher().backoffice_directory_class + rdb = get_publisher_class().backoffice_directory_class if rdb: rdb.register_directory('accounts', AccountsDirectory()) diff --git a/wcs/qommon/publisher.py b/wcs/qommon/publisher.py index e8d17702c..4356d9e1c 100644 --- a/wcs/qommon/publisher.py +++ b/wcs/qommon/publisher.py @@ -57,8 +57,6 @@ import storage import strftime import urllib -from qommon import _ - class ImmediateRedirectException(Exception): def __init__(self, location): self.location = location @@ -802,16 +800,25 @@ class QommonPublisher(Publisher, object): cls.register_cronjob(CronJob(cls.clean_afterjobs, minutes=[random.randint(0, 59)])) cls.register_cronjob(CronJob(cls.clean_tempfiles, minutes=[random.randint(0, 59)])) + register_cron = False + register_tld_names = False + + _initialized = False @classmethod - def create_publisher(cls, register_cron=True, register_tld_names=True): + def init_publisher_class(cls): + if cls._initialized: + return + cls._initialized = True cls.load_extra_dirs() cls.load_translations() - if register_cron: + if cls.register_cron: cls.register_cronjobs() - if register_tld_names: + if cls.register_tld_names: cls.load_effective_tld_names() + @classmethod + def create_publisher(cls, **kwargs): publisher = cls(cls.root_directory_class(), session_cookie_name = cls.APP_NAME, session_cookie_path = '/', diff --git a/wcs/qommon/substitution.py b/wcs/qommon/substitution.py index f37b438ab..4e6c7e211 100644 --- a/wcs/qommon/substitution.py +++ b/wcs/qommon/substitution.py @@ -15,7 +15,6 @@ # along with this program; if not, see . from quixote.html import htmltext, TemplateIO -from qommon import _ class Substitutions(object): substitutions_dict = {} @@ -61,6 +60,7 @@ class Substitutions(object): @classmethod def get_substitution_html_table(cls): + from qommon import _ r = TemplateIO(html=True) r += htmltext('') r += htmltext('' % ( diff --git a/wcs/qommon/template.py b/wcs/qommon/template.py index db75a3ea9..9a07ecd90 100644 --- a/wcs/qommon/template.py +++ b/wcs/qommon/template.py @@ -24,7 +24,6 @@ from quixote.directory import Directory from quixote.util import StaticDirectory, StaticFile from quixote.html import htmltext, htmlescape, TemplateIO -from qommon import _ import errors import ezt @@ -227,6 +226,7 @@ def html_top(title=None, default_org=None): def error_page(error_message, error_title = None, exception = None, continue_to = None, location_hint = None): + from qommon import _ if not error_title: error_title = _('Error') if exception: diff --git a/wcs/wscalls.py b/wcs/wscalls.py index b6cbbab51..852e88496 100644 --- a/wcs/wscalls.py +++ b/wcs/wscalls.py @@ -24,11 +24,11 @@ import xml.etree.ElementTree as ET from quixote import get_publisher from qommon import _ -from qommon.misc import (simplify, http_get_page, http_post_request, - get_variadic_url, JSONEncoder, json_loads) +from qommon.misc import simplify, get_variadic_url, JSONEncoder, json_loads from qommon.xml_storage import XmlStorableObject from qommon.form import (CompositeWidget, StringWidget, WidgetDict, ComputedExpressionWidget, RadiobuttonsWidget, CheckboxWidget) +import qommon.misc from wcs.api_utils import sign_url, get_secret_and_orig, MissingSecret from wcs.workflows import WorkflowStatusItem @@ -106,10 +106,10 @@ def call_webservice(url, qs_data=None, request_signature_key=None, # increase timeout for huge loads, one second every 65536 # bytes, to match a country 512kbps DSL line. timeout += len(payload) / 65536 - response, status, data, auth_header = http_post_request( + response, status, data, auth_header = qommon.misc.http_post_request( url, payload, headers=headers, timeout=timeout) else: - response, status, data, auth_header = http_get_page( + response, status, data, auth_header = qommon.misc.http_get_page( url, headers=headers, timeout=TIMEOUT) return (response, status, data)
%s%s%s