Merge branch '623_context_hooks'

This commit is contained in:
Christopher Allan Webber 2013-05-15 11:40:51 -05:00
commit ff97bbe944
9 changed files with 259 additions and 1 deletions

View File

@ -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.

View File

@ -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:

View File

@ -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]]

View File

@ -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"""

View File

@ -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}

View File

@ -0,0 +1,5 @@
General page!
global thing: {{ global_append }}
lol: {{ lol }}
doubleme: {{ doubleme }}

View File

@ -0,0 +1,6 @@
Specific page!
specific thing: {{ specific_page_append }}
global thing: {{ global_append }}
something: {{ something }}
doubleme: {{ doubleme }}

View File

@ -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"})

View File

@ -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: