Skip to content

Commit

Permalink
Proxy beaker session for universal support.
Browse files Browse the repository at this point in the history
Both Flask and Pylons can share the same beaker session objects, with a
session proxy in common.py.

This commit includes tests for cross app session access:
- flask view populates flash message in session, rendered by flask view
- flask view populates flash message in session, rendered by pylons
  action
- pylons action populates flash message in session, rendered by flask
  view
  • Loading branch information
brew committed May 26, 2016
1 parent ac4ee76 commit 28bc9c4
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 7 deletions.
21 changes: 15 additions & 6 deletions ckan/config/middleware.py
Expand Up @@ -261,6 +261,11 @@ def make_flask_stack(conf, **app_conf):

# Do all the Flask-specific stuff before adding other middlewares

# secret key needed for flask-debug-toolbar
app.config['SECRET_KEY'] = '<replace with a secret key>'
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
DebugToolbarExtension(app)

# Use Beaker as the Flask session interface
class BeakerSessionInterface(SessionInterface):
def open_session(self, app, request):
Expand All @@ -270,14 +275,18 @@ def open_session(self, app, request):
def save_session(self, app, session, response):
session.save()

app.wsgi_app = SessionMiddleware(app.wsgi_app)
cache_dir = app_conf.get('cache_dir') or app_conf.get('cache.dir')
session_opts = {
'session.data_dir': '{data_dir}/sessions'.format(
data_dir=cache_dir),
'session.key': app_conf.get('beaker.session.key'),
'session.cookie_expires':
app_conf.get('beaker.session.cookie_expires'),
'session.secret': app_conf.get('beaker.session.secret')
}
app.wsgi_app = SessionMiddleware(app.wsgi_app, session_opts)
app.session_interface = BeakerSessionInterface()

# secret key needed for flask-debug-toolbar
app.config['SECRET_KEY'] = '<replace with a secret key>'
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
DebugToolbarExtension(app)

# Add jinja2 extensions and filters
extensions = [
'jinja2.ext.do', 'jinja2.ext.with_',
Expand Down
2 changes: 1 addition & 1 deletion ckan/templates/base.html
Expand Up @@ -86,7 +86,7 @@
</head>

{# Allows custom attributes to be added to the <body> tag #}
<body{% block bodytag %} data-site-root="{{ h.url('/', locale='default', qualified=true) }}" data-locale-root="{{ h.url('/', qualified=true) }}" {% endblock %}>
<body{% block bodytag %} {% endblock %}>

{#
The page block allows you to add content to the page. Most of the time it is
Expand Down
14 changes: 14 additions & 0 deletions ckan/templates/tests/flash_messages.html
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
</head>
<body>
Flash messages:
{% for message in h.flash.pop_messages() | list %}
<div>
{{ message.category }}: {{ h.literal(message) }}
</div>
{% endfor %}
</body>
</html>
126 changes: 126 additions & 0 deletions ckan/tests/config/test_sessions.py
@@ -0,0 +1,126 @@
from nose.tools import ok_

from flask import Blueprint
from flask import render_template
from flask import redirect as flask_redirect
from flask import url_for
from ckan.lib.base import redirect as pylons_redirect
from ckan.lib.base import render as pylons_render

import ckan.plugins as p
import ckan.tests.helpers as helpers
import ckan.lib.helpers as h


class TestCrossFlaskPylonsFlashMessages(helpers.FunctionalTestBase):
'''
Test that flash message set in the Pylons controller can be accessed by
Flask views, and visa versa.
'''

def setup(self):
self.app = helpers._get_test_app()
self.flask_app = helpers.find_flask_app(self.app)

# Install plugin and register its blueprint
if not p.plugin_loaded('test_flash_plugin'):
p.load('test_flash_plugin')
plugin = p.get_plugin('test_flash_plugin')
self.flask_app.register_blueprint(plugin.get_blueprint(),
prioritise_rules=True)

def test_flash_populated_by_flask_redirect_to_flask(self):
'''
Flash store is populated by Flask view is accessible by another Flask
view.
'''
res = self.app.get(
'/flask_add_flash_message_redirect_to_flask').follow()

ok_("This is a success message populate by Flask" in res.body)

def test_flash_populated_in_pylons_action_redirect_to_flask(self):
'''
Flash store is populated by pylons action is accessible by Flask view.
'''
res = self.app.get('/pylons_add_flash_message_redirect_view').follow()

ok_("This is a success message populate by Pylons" in res.body)

def test_flash_populated_in_flask_view_redirect_to_pylons(self):
'''
Flash store is populated by flask view is accessible by pylons action.
'''
res = self.app.get('/flask_add_flash_message_redirect_pylons').follow()

ok_("This is a success message populate by Flask" in res.body)


class FlashMessagePlugin(p.SingletonPlugin):
'''
A Flask and Pylons compatible IRoutes plugin to add Flask views and Pylons
actions to display flash messages.
'''

p.implements(p.IRoutes, inherit=True)

def flash_message_view(self):
'''Flask view that renders the flash message html template.'''
return render_template('tests/flash_messages.html')

def add_flash_message_view_redirect_to_flask(self):
'''Add flash message, then redirect to Flask view to render it.'''
h.flash_success("This is a success message populate by Flask")
return flask_redirect(url_for('test_flash_plugin.flash_message_view'))

def add_flash_message_view_redirect_to_pylons(self):
'''Add flash message, then redirect to view that renders it'''
h.flash_success("This is a success message populate by Flask")
return flask_redirect('/pylons_view_flash_message')

def get_blueprint(self):
'''Return Flask Blueprint object to be registered by the Flask app.'''

# Create Blueprint for plugin
blueprint = Blueprint(self.name, self.__module__)
blueprint.template_folder = 'templates'
# Add plugin url rules to Blueprint object
rules = [
('/flask_add_flash_message_redirect_to_flask', 'add_flash_message',
self.add_flash_message_view_redirect_to_flask),
('/flask_add_flash_message_redirect_pylons',
'add_flash_message_view_redirect_to_pylons',
self.add_flash_message_view_redirect_to_pylons),
('/flask_view_flash_message', 'flash_message_view',
self.flash_message_view),
]
for rule in rules:
blueprint.add_url_rule(*rule)

return blueprint

controller = \
'ckan.tests.config.test_sessions:PylonsAddFlashMessageController'

def before_map(self, _map):
'''Update the pylons route map to be used by the Pylons app.'''
_map.connect('/pylons_add_flash_message_redirect_view',
controller=self.controller,
action='add_flash_message_redirect')

_map.connect('/pylons_view_flash_message',
controller=self.controller,
action='flash_message_action')
return _map


class PylonsAddFlashMessageController(p.toolkit.BaseController):

def flash_message_action(self):
'''Pylons view to render flash messages in a template.'''
return pylons_render('tests/flash_messages.html')

def add_flash_message_redirect(self):
# Adds a flash message and redirects to flask view
h.flash_success('This is a success message populate by Pylons')
return pylons_redirect('/flask_view_flash_message')
10 changes: 10 additions & 0 deletions ckanext/example_flask_iroutes/plugin.py
Expand Up @@ -14,6 +14,14 @@ def override_pylons_about():
return render_template('about.html')


def override_pylons_about_with_core_template():
'''
Override the pylons about controller to render the core about page
template.
'''
return render_template('home/about.html')


def override_flask_hello():
'''A simple replacement for the flash Hello view function.'''
html = '''<!DOCTYPE html>
Expand Down Expand Up @@ -75,6 +83,8 @@ def get_blueprint(self):
rules = [
('/hello_plugin', 'hello_plugin', hello_plugin),
('/about', 'about', override_pylons_about),
('/about_core', 'about_core',
override_pylons_about_with_core_template),
('/hello', 'hello', override_flask_hello),
('/helper_not_here', 'helper_not_here', helper_not_here),
('/helper', 'helper_here', helper_here),
Expand Down
5 changes: 5 additions & 0 deletions ckanext/example_flask_iroutes/templates/about_base.html
@@ -0,0 +1,5 @@
{% extends "page.html" %}

{% block page %}
This about page extends from page.html
{% endblock page %}
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -160,6 +160,7 @@
'test_datastore_view = ckan.tests.lib.test_datapreview:MockDatastoreBasedResourceView',
'test_datapusher_plugin = ckanext.datapusher.tests.test_interfaces:FakeDataPusherPlugin',
'test_routing_plugin = ckan.tests.config.test_middleware:MockRoutingPlugin',
'test_flash_plugin = ckan.tests.config.test_sessions:FlashMessagePlugin',
'test_helpers_plugin = ckan.tests.lib.test_helpers:TestHelpersPlugin',
],
'babel.extractors': [
Expand Down

0 comments on commit 28bc9c4

Please sign in to comment.