Skip to content

Commit 6a76e6d

Browse files
frankduncanfrjo
andauthored
Add django_hooks for template hooks (#4395)
This is pulled from https://github.com/nitely/django-hooks with everything except the template hooks removed. This is the first of three PRs, adding the infrastructure needed to add django style hooks inside templates. The hooks in templates look like: ``` {% hook 'hook_name' %} ``` where `hook_name` is defined somewhere else via: ``` from django.template.loader import render_to_string # Code that loads a template def my_extension_for_hook_name(context, *args, **kwargs): return render_to_string( "my_extension_for_hook_name.html", context.flatten(), ) class MyApp(AppConfig): def ready(self): from extensions.django_hooks.templatehook import hook hook.register("hypha_extension_head", my_extension_for_hook_name) ``` --------- Co-authored-by: Fredrik Jonsson <[email protected]>
1 parent fa3bfff commit 6a76e6d

File tree

3 files changed

+305
-0
lines changed

3 files changed

+305
-0
lines changed

docs/references/template-hooks.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# TemplateHook
2+
3+
Adding a hook-point in `main_app`'s template:
4+
5+
6+
# my_main_app/templates/_base.html
7+
8+
{ % load hooks_tags %}
9+
10+
<!DOCTYPE html>
11+
<html>
12+
<head>
13+
14+
15+
{ % hook 'within_head' %}
16+
17+
18+
</head>
19+
</html>
20+
21+
**Tip**
22+
23+
Here we are adding a *hook-point* called `within_head` where
24+
*third-party* apps will be able to insert their code.
25+
26+
Creating a hook listener in a `third_party_app`:
27+
28+
# third_party_app/template_hooks.py
29+
30+
from django.template.loader import render_to_string
31+
from django.utils.html import mark_safe, format_html
32+
33+
34+
# Example 1
35+
def css_resources(context, *args, **kwargs):
36+
return mark_safe(u'<link rel="stylesheet" href="%s/app_hook/styles.css">' % settings.STATIC_URL)
37+
38+
39+
# Example 2
40+
def user_about_info(context, *args, **kwargs):
41+
user = context['request'].user
42+
return format_html(
43+
"<b>{name}</b> {last_name}: {about}",
44+
name=user.first_name,
45+
last_name=user.last_name,
46+
about=mark_safe(user.profile.about_html_field) # Some safe (sanitized) html data.
47+
)
48+
49+
50+
# Example 3
51+
def a_more_complex_hook(context, *args, **kwargs):
52+
# If you are doing this a lot, make sure to keep your templates in memory (google: django.template.loaders.cached.Loader)
53+
return render_to_string(
54+
template_name='templates/app_hook/head_resources.html',
55+
context_instance=context
56+
)
57+
58+
59+
# Example 4
60+
def an_even_more_complex_hook(context, *args, **kwargs):
61+
articles = Article.objects.all()
62+
return render_to_string(
63+
template_name='templates/app_hook/my_articles.html',
64+
dictionary={'articles': articles, },
65+
context_instance=context
66+
)
67+
68+
Registering a hook listener in a `third_party_app`:
69+
70+
# third_party_app/apps.py
71+
72+
from django.apps import AppConfig
73+
74+
75+
class MyAppConfig(AppConfig):
76+
77+
name = 'myapp'
78+
verbose_name = 'My App'
79+
80+
def ready(self):
81+
from hooks.templatehook import hook
82+
from third_party_app.template_hooks import css_resources
83+
84+
hook.register("within_head", css_resources)
85+
86+
**Tip**
87+
88+
Where to register your hooks:
89+
90+
Use `AppConfig.ready()`:
91+
[docs](https://docs.djangoproject.com/en/1.8/ref/applications/#django.apps.AppConfig.ready)
92+
and
93+
[example](http://chriskief.com/2014/02/28/django-1-7-signals-appconfig/)

hypha/core/templatehook.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Adds the ability to have hooks injected into templates, via the `hooks_tags.hook` template tag.
2+
#
3+
# Originally from https://github.com/nitely/django-hooks but after culling everything except
4+
# the template hook. See /docs/templatehook.rst for more information
5+
#
6+
# The reason this was forked, rather than used as a module, is that the last commit from the
7+
# other project was made in 2015, and can no longer be imported into a modern django project.
8+
# The template hook continues to work correctly, but the other hooks most likely do not, and
9+
# so were removed.
10+
11+
12+
class TemplateHook(object):
13+
"""
14+
A hook for templates. This can be used directly or\
15+
through the :py:class:`Hook` dispatcher
16+
17+
:param list providing_args: A list of the arguments\
18+
this hook can pass along in a :py:func:`.__call__`
19+
"""
20+
21+
def __init__(self, providing_args=None):
22+
self.providing_args = providing_args or []
23+
self._registry = []
24+
25+
def __call__(self, *args, **kwargs):
26+
"""
27+
Collect all callbacks responses for this template hook
28+
29+
:return: Responses by registered callbacks,\
30+
this is usually a list of HTML strings
31+
:rtype: list
32+
"""
33+
return [func(*args, **kwargs) for func in self._registry]
34+
35+
def register(self, func):
36+
"""
37+
Register a new callback
38+
39+
:param callable func: A function reference used as a callback
40+
"""
41+
assert callable(func), "Callback func must be a callable"
42+
43+
self._registry.append(func)
44+
45+
def unregister(self, func):
46+
"""
47+
Remove a previously registered callback
48+
49+
:param callable func: A function reference\
50+
that was registered previously
51+
"""
52+
try:
53+
self._registry.remove(func)
54+
except ValueError:
55+
pass
56+
57+
def unregister_all(self):
58+
"""
59+
Remove all callbacks
60+
"""
61+
del self._registry[:]
62+
63+
64+
class Hook(object):
65+
"""
66+
Dynamic dispatcher (proxy) for :py:class:`TemplateHook`
67+
"""
68+
69+
def __init__(self):
70+
self._registry = {}
71+
72+
def __call__(self, name, *args, **kwargs):
73+
"""
74+
Collect all callbacks responses for this template hook.\
75+
The hook (name) does not need to be pre-created,\
76+
it may not exist at call time
77+
78+
:param str name: Hook name, it must be unique,\
79+
prefixing it with the app label is a good idea
80+
:return: Responses by registered callbacks
81+
:rtype: list
82+
"""
83+
try:
84+
templatehook = self._registry[name]
85+
except KeyError:
86+
return []
87+
88+
return templatehook(*args, **kwargs)
89+
90+
def _register(self, name):
91+
"""
92+
@Api private
93+
Add new :py:class:`TemplateHook` into the registry
94+
95+
:param str name: Hook name
96+
:return: Instance of :py:class:`TemplateHook`
97+
:rtype: :py:class:`TemplateHook`
98+
"""
99+
templatehook = TemplateHook()
100+
self._registry[name] = templatehook
101+
return templatehook
102+
103+
def register(self, name, func):
104+
"""
105+
Register a new callback.\
106+
When the name/id is not found\
107+
a new hook is created under its name,\
108+
meaning the hook is usually created by\
109+
the first registered callback
110+
111+
:param str name: Hook name
112+
:param callable func: A func reference (callback)
113+
"""
114+
try:
115+
templatehook = self._registry[name]
116+
except KeyError:
117+
templatehook = self._register(name)
118+
119+
templatehook.register(func)
120+
121+
def unregister(self, name, func):
122+
"""
123+
Remove a previously registered callback
124+
125+
:param str name: Hook name
126+
:param callable func: A function reference\
127+
that was registered previously
128+
"""
129+
try:
130+
templatehook = self._registry[name]
131+
except KeyError:
132+
return
133+
134+
templatehook.unregister(func)
135+
136+
def unregister_all(self, name):
137+
"""
138+
Remove all callbacks
139+
140+
:param str name: Hook name
141+
"""
142+
try:
143+
templatehook = self._registry[name]
144+
except KeyError:
145+
return
146+
147+
templatehook.unregister_all()
148+
149+
150+
hook = Hook()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from django import template
2+
from django.utils.html import format_html_join
3+
4+
from hypha.core.templatehook import hook
5+
6+
register = template.Library()
7+
8+
9+
@register.simple_tag(name="hook", takes_context=True)
10+
def hook_tag(context, name, *args, **kwargs):
11+
r"""
12+
Hook tag to call within templates
13+
14+
:param dict context: This is automatically passed,\
15+
contains the template state/variables
16+
:param str name: The hook which will be dispatched
17+
:param \*args: Positional arguments, will be passed to hook callbacks
18+
:param \*\*kwargs: Keyword arguments, will be passed to hook callbacks
19+
:return: A concatenation of all callbacks\
20+
responses marked as safe (conditionally)
21+
:rtype: str
22+
"""
23+
return format_html_join(
24+
sep="\n",
25+
format_string="{}",
26+
args_generator=(
27+
(response,) for response in hook(name, context, *args, **kwargs)
28+
),
29+
)
30+
31+
32+
def template_hook_collect(module, hook_name, *args, **kwargs):
33+
r"""
34+
Helper to include in your own templatetag, for static TemplateHooks
35+
36+
Example::
37+
38+
import myhooks
39+
from hooks.templatetags import template_hook_collect
40+
41+
@register.simple_tag(takes_context=True)
42+
def hook(context, name, *args, **kwargs):
43+
return template_hook_collect(myhooks, name, context, *args, **kwargs)
44+
45+
:param module module: Module containing the template hook definitions
46+
:param str hook_name: The hook name to be dispatched
47+
:param \*args: Positional arguments, will be passed to hook callbacks
48+
:param \*\*kwargs: Keyword arguments, will be passed to hook callbacks
49+
:return: A concatenation of all callbacks\
50+
responses marked as safe (conditionally)
51+
:rtype: str
52+
"""
53+
try:
54+
templatehook = getattr(module, hook_name)
55+
except AttributeError:
56+
return ""
57+
58+
return format_html_join(
59+
sep="\n",
60+
format_string="{}",
61+
args_generator=((response,) for response in templatehook(*args, **kwargs)),
62+
)

0 commit comments

Comments
 (0)