Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API endpoint for pattern rendering via POST #168

Merged
merged 4 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/recipes/api-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# API rendering

For additional flexibility, django-pattern-library supports rendering patterns via an API endpoint.
This can be useful when implementing a custom UI while still using the pattern library’s Django rendering features.

The API endpoint is available at `api/v1/render-pattern`. It accepts POST requests with a JSON payload containing the following fields:

- `template_name` – the path of the template to render
- `config` – the configuration for the template, with the same data structure as the configuration files (`context` and `tags`).

Here is an example, with curl:

```bash
echo '{"template_name": "patterns/molecules/button/button.html", "config": {"context": {"target_page": {"title": "API"}}, "tags": {"pageurl":{"target_page":{"raw": "/hello-api"}}}}}' | curl -d @- http://localhost:8000/api/v1/render-pattern
```

The response will be the pattern’s rendered HTML:

```html
<a href="/hello-api" class="button">
API
</a>
```

Note compared to iframe rendering, this API always renders the pattern’s HTML standalone, never within a base template.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ nav:
- 'Inclusion tags': 'recipes/inclusion-tags.md'
- 'Looping for tags': 'recipes/looping-for-tags.md'
- 'Pagination': 'recipes/pagination.md'
- 'API rendering': 'recipes/api-rendering.md'
- 'Reference':
- 'API and settings': 'reference/api.md'
- 'Concepts': 'reference/concepts.md'
Expand Down
15 changes: 5 additions & 10 deletions pattern_library/monkey_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

from django.template.library import SimpleNode

from pattern_library.utils import (
get_pattern_config,
is_pattern_library_context,
render_pattern,
)
from pattern_library.utils import is_pattern_library_context, render_pattern

logger = logging.getLogger(__name__)
UNSPECIFIED = object()
Expand All @@ -31,17 +27,16 @@ def node_render(context):
tag_overridden = False
result = ""

# Load pattern's config
current_template_name = parser.origin.template_name
pattern_config = get_pattern_config(current_template_name)
# Get overriden tag config.
tag_overrides = context.get("__pattern_library_tag_overrides", {})

# Extract values for lookup from the token
bits = token.split_contents()
tag_name = bits[0]
arguments = " ".join(bits[1:]).strip()

# Get config for a specific tag
tag_config = pattern_config.get("tags", {}).get(tag_name, {})
tag_config = tag_overrides.get(tag_name, {})
if tag_config:
# Get config for specific arguments
tag_config = tag_config.get(arguments, {})
Expand Down Expand Up @@ -88,7 +83,7 @@ def node_render(context):
logger.warning(
'No default or stub data defined for the "%s" tag in the "%s" template',
tag_name,
current_template_name,
parser.origin.template_name,
)

return original_node_render(context)
Expand Down
4 changes: 3 additions & 1 deletion pattern_library/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.urls import re_path
from django.urls import path, re_path

from pattern_library import get_pattern_template_suffix, views

Expand All @@ -19,4 +19,6 @@
views.RenderPatternView.as_view(),
name="render_pattern",
),
# API rendering
path("api/v1/render-pattern", views.render_pattern_api, name="render_pattern_api"),
]
10 changes: 8 additions & 2 deletions pattern_library/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,18 @@ def get_pattern_markdown(template_name):
return markdown.markdown(f.read())


def render_pattern(request, template_name, allow_non_patterns=False):
def render_pattern(request, template_name, allow_non_patterns=False, config=None):
if not allow_non_patterns and not is_pattern(template_name):
raise TemplateIsNotPattern

context = get_pattern_context(template_name)
if not config:
config = get_pattern_config(template_name)

context = config.get("context", {})
tags = config.get("tags", {})
mark_context_strings_safe(context)
context[get_pattern_context_var_name()] = True
context["__pattern_library_tag_overrides"] = tags
for modifier in registry.get_for_template(template_name):
modifier(context=context, request=request)
return render_to_string(template_name, request=request, context=context)
Expand Down
19 changes: 19 additions & 0 deletions pattern_library/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import json

from django.http import Http404, HttpResponse
from django.template.loader import get_template
from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import TemplateView

from pattern_library import get_base_template_names, get_pattern_base_template_name
Expand Down Expand Up @@ -99,3 +102,19 @@ def get(self, request, pattern_template_name=None):
return self.render_to_response(context)

return HttpResponse(rendered_pattern)


@csrf_exempt
def render_pattern_api(request):
data = json.loads(request.body.decode("utf-8"))
template_name = data["template_name"]
config = data["config"]
bcdickinson marked this conversation as resolved.
Show resolved Hide resolved

try:
rendered_pattern = render_pattern(
request, template_name, allow_non_patterns=False, config=config
)
except TemplateIsNotPattern:
raise Http404

return HttpResponse(rendered_pattern)
4 changes: 4 additions & 0 deletions tests/templates/patterns/molecules/button/button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load test_tags %}
<a href="{% if target_url %}{{ target_url }}{% else %}{% pageurl target_page %}{% endif %}" class="button">
{% if label %}{{ label }}{% else %}{{ target_page.title }}{% endif %}
</a>
7 changes: 7 additions & 0 deletions tests/templates/patterns/molecules/button/button.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
context:
target_page:
title: Get started
tags:
pageurl:
target_page:
raw: /get-started
7 changes: 7 additions & 0 deletions tests/templatetags/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ def default_html_tag_falsey(arg=None):
raise Exception("default_tag raised an exception")


@register.simple_tag()
def pageurl(page):
"""Approximation of wagtail built-in tag for realistic example."""
return "/page/url"


# Get widget type of a field
@register.filter(name="widget_type")
def widget_type(bound_field):
Expand All @@ -36,3 +42,4 @@ def widget_type(bound_field):
override_tag(register, "error_tag")
override_tag(register, "default_html_tag", default_html="https://potato.com")
override_tag(register, "default_html_tag_falsey", default_html=None)
override_tag(register, "pageurl")
1 change: 1 addition & 0 deletions tests/tests/test_context_modifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def test_applied_by_render_pattern(self, render_to_string):
context={
"atom_var": "atom_var value from test_atom.yaml",
"is_pattern_library": True,
"__pattern_library_tag_overrides": {},
"foo": "bar",
"beep": "boop",
},
Expand Down
29 changes: 29 additions & 0 deletions tests/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,32 @@ def test_fragment_extended_from_variable(self):
),
"base content - extended content",
)


class APIViewsTestCase(SimpleTestCase):
def test_renders_with_tag_overrides(self):
api_endpoint = reverse("pattern_library:render_pattern_api")
response = self.client.post(
api_endpoint,
content_type="application/json",
data={
"template_name": "patterns/molecules/button/button.html",
"config": {
"context": {"target_page": {"title": "API"}},
"tags": {"pageurl": {"target_page": {"raw": "/hello-api"}}},
},
},
)
self.assertContains(response, "/hello-api")

def test_404(self):
api_endpoint = reverse("pattern_library:render_pattern_api")
response = self.client.post(
api_endpoint,
content_type="application/json",
data={
"template_name": "doesnotexist.html",
"config": {},
},
)
self.assertEqual(response.status_code, 404)