diff options
author | Christopher Allan Webber <cwebber@dustycloud.org> | 2013-05-15 11:40:51 -0500 |
---|---|---|
committer | Christopher Allan Webber <cwebber@dustycloud.org> | 2013-05-15 11:40:51 -0500 |
commit | ff97bbe94450c9587b146ab67feadd0b01817acb (patch) | |
tree | 533e6d76caa5bf34b0013ff684c2c92369230e21 | |
parent | 8154ecaf3a1a91ff82e88fc30db1d656c22e5a7f (diff) | |
parent | a1099bba79077c1258e586e2e2cbfe0094f10118 (diff) | |
download | mediagoblin-ff97bbe94450c9587b146ab67feadd0b01817acb.tar.lz mediagoblin-ff97bbe94450c9587b146ab67feadd0b01817acb.tar.xz mediagoblin-ff97bbe94450c9587b146ab67feadd0b01817acb.zip |
Merge branch '623_context_hooks'
-rw-r--r-- | docs/source/pluginwriter/api.rst | 77 | ||||
-rw-r--r-- | mediagoblin/app.py | 4 | ||||
-rw-r--r-- | mediagoblin/tests/appconfig_context_modified.ini | 26 | ||||
-rw-r--r-- | mediagoblin/tests/test_pluginapi.py | 35 | ||||
-rw-r--r-- | mediagoblin/tests/testplugins/modify_context/__init__.py | 55 | ||||
-rw-r--r-- | mediagoblin/tests/testplugins/modify_context/templates/contextplugin/general.html | 5 | ||||
-rw-r--r-- | mediagoblin/tests/testplugins/modify_context/templates/contextplugin/specific.html | 6 | ||||
-rw-r--r-- | mediagoblin/tests/testplugins/modify_context/views.py | 33 | ||||
-rw-r--r-- | mediagoblin/tools/template.py | 19 |
9 files changed, 259 insertions, 1 deletions
diff --git a/docs/source/pluginwriter/api.rst b/docs/source/pluginwriter/api.rst index df933511..5e0568fd 100644 --- a/docs/source/pluginwriter/api.rst +++ b/docs/source/pluginwriter/api.rst @@ -48,3 +48,80 @@ example might look like:: This means that when people enable your plugin in their config you'll be able to provide defaults as well as type validation. + +Context Hooks +------------- + +View specific hooks ++++++++++++++++++++ + +You can hook up to almost any template called by any specific view +fairly easily. As long as the view directly or indirectly uses the +method ``render_to_response`` you can access the context via a hook +that has a key in the format of the tuple:: + + (view_symbolic_name, view_template_path) + +Where the "view symbolic name" is the same parameter used in +``request.urlgen()`` to look up the view. So say we're wanting to add +something to the context of the user's homepage. We look in +mediagoblin/user_pages/routing.py and see:: + + add_route('mediagoblin.user_pages.user_home', + '/u/<string:user>/', + 'mediagoblin.user_pages.views:user_home') + +Aha! That means that the name is ``mediagoblin.user_pages.user_home``. +Okay, so then we look at the view at the +``mediagoblin.user_pages.user_home`` method:: + + @uses_pagination + def user_home(request, page): + # [...] whole bunch of stuff here + return render_to_response( + request, + 'mediagoblin/user_pages/user.html', + {'user': user, + 'user_gallery_url': user_gallery_url, + 'media_entries': media_entries, + 'pagination': pagination}) + +Nice! So the template appears to be +``mediagoblin/user_pages/user.html``. Cool, that means that the key +is:: + + ("mediagoblin.user_pages.user_home", + "mediagoblin/user_pages/user.html") + +The context hook uses ``hook_transform()`` so that means that if we're +hooking into it, our hook will both accept one argument, ``context``, +and should return that modified object, like so:: + + def add_to_user_home_context(context): + context['foo'] = 'bar' + return context + + hooks = { + ("mediagoblin.user_pages.user_home", + "mediagoblin/user_pages/user.html"): add_to_user_home_context} + + +Global context hooks +++++++++++++++++++++ + +If you need to add something to the context of *every* view, it is not +hard; there are two hooks hook that also uses hook_transform (like the +above) but make available what you are providing to *every* view. + +Note that there is a slight, but critical, difference between the two. + +The most general one is the ``'template_global_context'`` hook. This +one is run only once, and is read into the global context... all views +will get access to what are in this dict. + +The slightly more expensive but more powerful one is +``'template_context_prerender'``. This one is not added to the global +context... it is added to the actual context of each individual +template render right before it is run! Because of this you also can +do some powerful and crazy things, such as checking the request object +or other parts of the context before passing them on. diff --git a/mediagoblin/app.py b/mediagoblin/app.py index bf0e0f13..1984ce77 100644 --- a/mediagoblin/app.py +++ b/mediagoblin/app.py @@ -188,6 +188,7 @@ class MediaGoblinApp(object): mg_request.setup_user_in_request(request) + request.controller_name = None try: found_rule, url_values = map_adapter.match(return_rule=True) request.matchdict = url_values @@ -201,6 +202,9 @@ class MediaGoblinApp(object): exc.get_description(environ))(environ, start_response) controller = endpoint_to_controller(found_rule) + # Make a reference to the controller's symbolic name on the request... + # used for lazy context modification + request.controller_name = found_rule.endpoint # pass the request through our meddleware classes try: diff --git a/mediagoblin/tests/appconfig_context_modified.ini b/mediagoblin/tests/appconfig_context_modified.ini new file mode 100644 index 00000000..e93797df --- /dev/null +++ b/mediagoblin/tests/appconfig_context_modified.ini @@ -0,0 +1,26 @@ +[mediagoblin] +direct_remote_path = /test_static/ +email_sender_address = "notice@mediagoblin.example.org" +email_debug_mode = true + +# TODO: Switch to using an in-memory database +sql_engine = "sqlite:///%(here)s/test_user_dev/mediagoblin.db" + +# Celery shouldn't be set up by the application as it's setup via +# mediagoblin.init.celery.from_celery +celery_setup_elsewhere = true + +[storage:publicstore] +base_dir = %(here)s/test_user_dev/media/public +base_url = /mgoblin_media/ + +[storage:queuestore] +base_dir = %(here)s/test_user_dev/media/queue + +[celery] +CELERY_ALWAYS_EAGER = true +CELERY_RESULT_DBURI = "sqlite:///%(here)s/test_user_dev/celery.db" +BROKER_HOST = "sqlite:///%(here)s/test_user_dev/kombu.db" + +[plugins] +[[mediagoblin.tests.testplugins.modify_context]] diff --git a/mediagoblin/tests/test_pluginapi.py b/mediagoblin/tests/test_pluginapi.py index 809b5ce9..73ad235e 100644 --- a/mediagoblin/tests/test_pluginapi.py +++ b/mediagoblin/tests/test_pluginapi.py @@ -25,6 +25,7 @@ from mediagoblin import mg_globals from mediagoblin.init.plugins import setup_plugins from mediagoblin.init.config import read_mediagoblin_config from mediagoblin.tools import pluginapi +from mediagoblin.tests.tools import get_app def with_cleanup(*modules_to_delete): @@ -323,3 +324,37 @@ def test_plugin_config(): # the callables thing shouldn't really have anything though. assert len(config['plugins'][ 'mediagoblin.tests.testplugins.callables1']) == 0 + + +@pytest.fixture() +def context_modified_app(request): + """ + Get a MediaGoblin app fixture using appconfig_context_modified.ini + """ + return get_app( + request, + mgoblin_config=pkg_resources.resource_filename( + 'mediagoblin.tests', 'appconfig_context_modified.ini')) + + +def test_modify_context(context_modified_app): + """ + Test that we can modify both the view/template specific and + global contexts for templates. + """ + # Specific thing passed into a page + result = context_modified_app.get("/modify_context/specific/") + assert result.body.strip() == """Specific page! + +specific thing: in yer specificpage +global thing: globally appended! +something: orother +doubleme: happyhappy""" + + # General test, should have global context variable only + result = context_modified_app.get("/modify_context/") + assert result.body.strip() == """General page! + +global thing: globally appended! +lol: cats +doubleme: joyjoy""" diff --git a/mediagoblin/tests/testplugins/modify_context/__init__.py b/mediagoblin/tests/testplugins/modify_context/__init__.py new file mode 100644 index 00000000..164e66c1 --- /dev/null +++ b/mediagoblin/tests/testplugins/modify_context/__init__.py @@ -0,0 +1,55 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tools import pluginapi +import pkg_resources + + +def append_to_specific_context(context): + context['specific_page_append'] = 'in yer specificpage' + return context + +def append_to_global_context(context): + context['global_append'] = 'globally appended!' + return context + +def double_doubleme(context): + if 'doubleme' in context: + context['doubleme'] = context['doubleme'] * 2 + return context + + +def setup_plugin(): + routes = [ + ('modify_context.specific_page', + '/modify_context/specific/', + 'mediagoblin.tests.testplugins.modify_context.views:specific'), + ('modify_context.general_page', + '/modify_context/', + 'mediagoblin.tests.testplugins.modify_context.views:general')] + + pluginapi.register_routes(routes) + pluginapi.register_template_path( + pkg_resources.resource_filename( + 'mediagoblin.tests.testplugins.modify_context', 'templates')) + + +hooks = { + 'setup': setup_plugin, + ('modify_context.specific_page', + 'contextplugin/specific.html'): append_to_specific_context, + 'template_global_context': append_to_global_context, + 'template_context_prerender': double_doubleme} diff --git a/mediagoblin/tests/testplugins/modify_context/templates/contextplugin/general.html b/mediagoblin/tests/testplugins/modify_context/templates/contextplugin/general.html new file mode 100644 index 00000000..9cf96d3e --- /dev/null +++ b/mediagoblin/tests/testplugins/modify_context/templates/contextplugin/general.html @@ -0,0 +1,5 @@ +General page! + +global thing: {{ global_append }} +lol: {{ lol }} +doubleme: {{ doubleme }} diff --git a/mediagoblin/tests/testplugins/modify_context/templates/contextplugin/specific.html b/mediagoblin/tests/testplugins/modify_context/templates/contextplugin/specific.html new file mode 100644 index 00000000..5b1b4c4a --- /dev/null +++ b/mediagoblin/tests/testplugins/modify_context/templates/contextplugin/specific.html @@ -0,0 +1,6 @@ +Specific page! + +specific thing: {{ specific_page_append }} +global thing: {{ global_append }} +something: {{ something }} +doubleme: {{ doubleme }} diff --git a/mediagoblin/tests/testplugins/modify_context/views.py b/mediagoblin/tests/testplugins/modify_context/views.py new file mode 100644 index 00000000..701ec6f9 --- /dev/null +++ b/mediagoblin/tests/testplugins/modify_context/views.py @@ -0,0 +1,33 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from mediagoblin.tools.response import render_to_response + + +def specific(request): + return render_to_response( + request, + 'contextplugin/specific.html', + {"something": "orother", + "doubleme": "happy"}) + + +def general(request): + return render_to_response( + request, + 'contextplugin/general.html', + {"lol": "cats", + "doubleme": "joy"}) diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py index 54aeac92..3d651a6e 100644 --- a/mediagoblin/tools/template.py +++ b/mediagoblin/tools/template.py @@ -27,7 +27,7 @@ from mediagoblin import messages from mediagoblin import _version from mediagoblin.tools import common from mediagoblin.tools.translate import set_thread_locale -from mediagoblin.tools.pluginapi import get_hook_templates +from mediagoblin.tools.pluginapi import get_hook_templates, hook_transform from mediagoblin.tools.timesince import timesince from mediagoblin.meddleware.csrf import render_csrf_form_token @@ -80,6 +80,9 @@ def get_jinja_env(template_loader, locale): # allow for hooking up plugin templates template_env.globals['get_hook_templates'] = get_hook_templates + template_env.globals = hook_transform( + 'template_global_context', template_env.globals) + if exists(locale): SETUP_JINJA_ENVS[locale] = template_env @@ -103,6 +106,20 @@ def render_template(request, template_path, context): rendered_csrf_token = render_csrf_form_token(request) if rendered_csrf_token is not None: context['csrf_token'] = render_csrf_form_token(request) + + # allow plugins to do things to the context + if request.controller_name: + context = hook_transform( + (request.controller_name, template_path), + context) + + # More evil: allow plugins to possibly do something to the context + # in every request ever with access to the request and other + # variables. Note: this is slower than using + # template_global_context + context = hook_transform( + 'template_context_prerender', context) + rendered = template.render(context) if common.TESTS_ENABLED: |