Skip to content

Commit db555cf

Browse files
author
Ben Dickinson
authored
Merge pull request #124 from bcdickinson/fix-base-template-wrapping
Fix broken page/fragment detection
2 parents 6f12de0 + 6dd45c5 commit db555cf

21 files changed

+250
-34
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ Fixes # (issue)
1313
- [ ] My changes generate no new warnings
1414
- [ ] I have added tests that prove my fix is effective or that my feature works
1515
- [ ] New and existing unit tests pass locally with my changes
16+
- [ ] I have added an appropriate CHANGELOG entry

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
### Fixed
1515

16+
- Pages and fragments are now handled correctly again ([#119](https://github.com/torchbox/django-pattern-library/issues/119))
1617
- PyPI package metadata now uses absolute URLs to GitHub ([#120](https://github.com/torchbox/django-pattern-library/issues/120)).
1718

1819
## [0.2.9] - 2020-07-29
@@ -57,3 +58,8 @@
5758

5859
### Added
5960
- Compatibility with Django 2.2
61+
62+
[0.2.9]: https://github.com/torchbox/django-pattern-library/releases/tag/v0.2.9
63+
[0.2.8]: https://github.com/torchbox/django-pattern-library/releases/tag/v0.2.8
64+
[0.2.5]: https://github.com/torchbox/django-pattern-library/releases/tag/v0.2.5
65+
[0.2.4]: https://github.com/torchbox/django-pattern-library/releases/tag/v0.2.4

README.md

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
[![PyPI](https://img.shields.io/pypi/v/django-pattern-library.svg)](https://pypi.org/project/django-pattern-library/) [![PyPI downloads](https://img.shields.io/pypi/dm/django-pattern-library.svg)](https://pypi.org/project/django-pattern-library/) [![Travis](https://travis-ci.com/torchbox/django-pattern-library.svg?branch=master)](https://travis-ci.com/torchbox/django-pattern-library) [![Total alerts](https://img.shields.io/lgtm/alerts/g/torchbox/django-pattern-library.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/torchbox/django-pattern-library/alerts/)
44

5-
A module for Django that helps you to build pattern libraries and follow the
6-
[Atomic design](http://bradfrost.com/blog/post/atomic-web-design/) methodology.
5+
A module for Django that helps you to build pattern libraries.
76

87
![Screenshot of the pattern library UI, with navigation, pattern rendering, and configuration](https://raw.githubusercontent.com/torchbox/django-pattern-library/master/.github/pattern-library-screenshot.webp)
98

109
## Documentation
1110

12-
Documentation is located in GitHub in [`docs/`](https://github.com/torchbox/django-pattern-library/tree/master/docs).
11+
Documentation is located on GitHub in [`docs/`](https://github.com/torchbox/django-pattern-library/tree/master/docs).
1312

1413
## Objective
1514

@@ -29,9 +28,38 @@ To learn more about how this package can be used, have a look at our Wagtail Spa
2928

3029
[![Reusable UI components: A journey from React to Wagtail](https://raw.githubusercontent.com/torchbox/django-pattern-library/master/.github/pattern-library-talk-youtube.webp)](https://www.youtube.com/watch?v=isrOufI7TKc)
3130

31+
## Concepts
32+
To understand how `django-pattern-library` works, the following concepts are important.
33+
34+
### Patterns
35+
Any template that is displayed by the pattern library is referred to as a pattern. Patterns are divided into two categories: fragments and pages.
36+
37+
### Fragments
38+
A fragment is a pattern whose markup does not include all of the resources (typically CSS and Javascript) for it to be displayed correctly on its own. This is typical for reusable component templates which depend on global stylesheets or Javascript bundles to render and behave correctly.
39+
40+
To enable them to be correctly displayed in the pattern library, `django-pattern-library` will inject the rendered markup of fragments into the **pattern base template** specified by `PATTERN_LIBRARY['PATTERN_BASE_TEMPLATE_NAME']`.
41+
42+
This template should include references to any required static files. The rendered markup of fragments will be available in the `pattern_library_rendered_pattern` context variable (see the tests for [an example](https://github.com/torchbox/django-pattern-library/blob/master/tests/templates/patterns/base.html)).
43+
44+
### Pages
45+
In contrast to fragments, pages are patterns that include everything they need to be displayed correctly in their markup. Pages are defined by `PATTERN_LIBRARY['BASE_TEMPLATE_NAMES']`.
46+
47+
Any template in that list — or that extends a template in that list — is considered a page and will be displayed as-is when rendered in the pattern library.
48+
49+
It is common practice for page templates to extend the pattern base template to avoid duplicate references to stylesheets and Javascript bundles. Again, [an example](https://github.com/torchbox/django-pattern-library/blob/master/tests/templates/patterns/base_page.html) of this can be seen in the tests.
50+
3251
## How to install
3352

34-
In your Django settings, add `pattern_library` into your `INSTALLED_APPS`, and `pattern_library.loader_tags` into the `TEMPLATES` setting. For example:
53+
First install the library:
54+
55+
```sh
56+
pip install django-pattern-library
57+
# ... or...
58+
poetry add django-pattern-library
59+
```
60+
61+
62+
Then, in your Django settings, add `pattern_library` into your `INSTALLED_APPS`, and `pattern_library.loader_tags` to `OPTIONS['builtins']` into the `TEMPLATES` setting. For example:
3563

3664
```python
3765
INSTALLED_APPS = [
@@ -58,18 +86,42 @@ TEMPLATES = [
5886
]
5987
```
6088

61-
Note that this module only supports the Django template backend out of the box.
89+
Note that this module only supports the Django template backend.
6290

63-
Set the `PATTERN_LIBRARY_TEMPLATE_DIR` setting to point to a template directory with your patterns:
91+
### Settings
92+
93+
Next, set the `PATTERN_LIBRARY` setting. Here's an example showing the defaults:
6494

6595
```python
66-
PATTERN_LIBRARY_TEMPLATE_DIR = os.path.join(BASE_DIR, 'project_styleguide', 'templates')
96+
PATTERN_LIBRARY = {
97+
# PATTERN_BASE_TEMPLATE_NAME is the template that fragments will be wrapped with.
98+
# It should include any required CSS and JS and output
99+
# `pattern_library_rendered_pattern` from context.
100+
'PATTERN_BASE_TEMPLATE_NAME': 'patterns/base.html',
101+
# Any template in BASE_TEMPLATE_NAMES or any template that extends a template in
102+
# BASE_TEMPLATE_NAMES is a "page" and will be rendered as-is without being wrapped.
103+
'BASE_TEMPLATE_NAMES': ['patterns/base_page.html'],
104+
'TEMPLATE_SUFFIX': '.html',
105+
# SECTIONS controls the groups of templates that appear in the navigation. The keys
106+
# are the group titles and the values are lists of template name prefixes that will
107+
# be searched to populate the groups.
108+
'SECTIONS': (
109+
('atoms', ['patterns/atoms']),
110+
('molecules', ['patterns/molecules']),
111+
('organisms', ['patterns/organisms']),
112+
('templates', ['patterns/templates']),
113+
('pages', ['patterns/pages']),
114+
),
115+
}
116+
67117
```
68118

69-
Note that `PATTERN_LIBRARY_TEMPLATE_DIR` must be available for
70-
[template loaders](https://docs.djangoproject.com/en/1.11/ref/templates/api/#loader-types).
119+
Note that the templates in your `PATTERN_LIBRARY` settings must be available to your project's
120+
[template loaders](https://docs.djangoproject.com/en/3.1/ref/templates/api/#loader-types).
121+
122+
### URLs
71123

72-
Include `pattern_library.urls` into your `urlpatterns`. Here's an example `urls.py`:
124+
Include `pattern_library.urls` in your `urlpatterns`. Here's an example `urls.py`:
73125

74126
```python
75127
from django.apps import apps

pattern_library/__init__.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
2-
31
default_app_config = 'pattern_library.apps.PatternLibraryAppConfig'
42

53
DEFAULT_SETTINGS = {
6-
'BASE_TEMPLATE_NAME': 'patterns/base.html',
4+
# PATTERN_BASE_TEMPLATE_NAME is the template that fragments will be wrapped with.
5+
# It should include any required CSS and JS and output
6+
# `pattern_library_rendered_pattern` from context.
7+
'PATTERN_BASE_TEMPLATE_NAME': 'patterns/base.html',
8+
# Any template in BASE_TEMPLATE_NAMES or any template that extends a template in
9+
# BASE_TEMPLATE_NAMES is a "page" and will be rendered as-is without being wrapped.
10+
'BASE_TEMPLATE_NAMES': ['patterns/base_page.html'],
711
'TEMPLATE_SUFFIX': '.html',
12+
# SECTIONS controls the groups of templates that appear in the navigation. The keys
13+
# are the group titles and the value are lists of template name prefixes that will
14+
# be searched to populate the groups.
815
'SECTIONS': (
916
('atoms', ['patterns/atoms']),
1017
('molecules', ['patterns/molecules']),
@@ -15,25 +22,27 @@
1522
}
1623

1724

18-
def get_from_settings(attr):
25+
def get_setting(attr):
1926
from django.conf import settings
20-
2127
library_settings = DEFAULT_SETTINGS.copy()
2228
library_settings.update(getattr(settings, 'PATTERN_LIBRARY', {}))
23-
2429
return library_settings.get(attr)
2530

2631

2732
def get_pattern_template_suffix():
28-
return get_from_settings('TEMPLATE_SUFFIX')
33+
return get_setting('TEMPLATE_SUFFIX')
2934

3035

3136
def get_pattern_base_template_name():
32-
return get_from_settings('BASE_TEMPLATE_NAME')
37+
return get_setting('PATTERN_BASE_TEMPLATE_NAME')
38+
39+
40+
def get_base_template_names():
41+
return get_setting('BASE_TEMPLATE_NAMES')
3342

3443

3544
def get_sections():
36-
return get_from_settings('SECTIONS')
45+
return get_setting('SECTIONS')
3746

3847

3948
def get_pattern_context_var_name():

pattern_library/loader_tags.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class ExtendsNode(DjangoExtendsNode):
1717
"""
1818
def render(self, context):
1919
if is_pattern_library_context(context):
20-
parent_context = get_pattern_context(self.parent_name.var)
20+
parent_name = self.parent_name.resolve(context)
21+
parent_context = get_pattern_context(parent_name)
2122
if parent_context:
2223
# We want parent_context to appear later in the lookup process
2324
# than context of the actual template.
@@ -59,7 +60,8 @@ class IncludeNode(DjangoIncludeNode):
5960
"""
6061
def render(self, context):
6162
if is_pattern_library_context(context):
62-
pattern_context = get_pattern_context(self.template.var)
63+
template = self.template.resolve(context)
64+
pattern_context = get_pattern_context(template)
6365
extra_context = {name: var.resolve(context) for name, var in self.extra_context.items()}
6466

6567
if self.isolated_context:
@@ -89,8 +91,8 @@ def render(self, context):
8991
@register.tag('extends')
9092
def do_extends(parser, token):
9193
"""
92-
Copy if Django's built-in {% extends ... %} tag that uses the custom
93-
ExtendsNode to allow us to load dump data for pattern library.
94+
A copy of Django's built-in {% extends ... %} tag that uses our custom
95+
ExtendsNode to allow us to load dummy context for the pattern library.
9496
"""
9597
bits = token.split_contents()
9698
if len(bits) != 2:
@@ -106,8 +108,8 @@ def do_extends(parser, token):
106108
@register.tag('include')
107109
def do_include(parser, token):
108110
"""
109-
Copy if Django's built-in {% include ... %} tag that uses the custom
110-
IncludeNode to allow us to load dump data for pattern library.
111+
A copy of Django's built-in {% include ... %} tag that uses our custom
112+
IncludeNode to allow us to load dummy context for the pattern library.
111113
"""
112114
bits = token.split_contents()
113115
if len(bits) < 2:

pattern_library/utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import re
44

55
from django.template import TemplateDoesNotExist
6+
from django.template.context import Context
67
from django.template.loader import get_template, render_to_string
8+
from django.template.loader_tags import ExtendsNode
79
from django.template.loaders.app_directories import get_app_template_dirs
810
from django.utils.safestring import mark_safe
911

@@ -189,3 +191,27 @@ def render_pattern(request, template_name, allow_non_patterns=False):
189191
context = get_pattern_context(template_name)
190192
context[get_pattern_context_var_name()] = True
191193
return render_to_string(template_name, request=request, context=context)
194+
195+
196+
def get_template_ancestors(template_name, context=None, ancestors=None):
197+
"""
198+
Returns a list of template names, starting with provided name
199+
and followed by the names of any templates that extends until
200+
the most extended template is reached.
201+
"""
202+
if ancestors is None:
203+
ancestors = [template_name]
204+
205+
if context is None:
206+
context = Context()
207+
208+
pattern_template = get_template(template_name)
209+
210+
for node in pattern_template.template.nodelist:
211+
if isinstance(node, ExtendsNode):
212+
parent_template_name = node.parent_name.resolve(context)
213+
ancestors.append(parent_template_name)
214+
get_template_ancestors(parent_template_name, context=context, ancestors=ancestors)
215+
break
216+
217+
return ancestors

pattern_library/views.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
from django.http import Http404
1+
from django.http import Http404, HttpResponse
22
from django.template.loader import get_template
33
from django.utils.decorators import method_decorator
44
from django.utils.html import escape
55
from django.views.decorators.clickjacking import xframe_options_sameorigin
66
from django.views.generic.base import TemplateView
77

8-
from pattern_library import get_pattern_base_template_name
8+
from pattern_library import (
9+
get_base_template_names, get_pattern_base_template_name
10+
)
911
from pattern_library.exceptions import (
1012
PatternLibraryEmpty, TemplateIsNotPattern
1113
)
1214
from pattern_library.utils import (
13-
get_pattern_config, get_pattern_config_str, get_pattern_markdown,
14-
get_pattern_templates, get_sections, is_pattern, render_pattern
15+
get_pattern_config, get_pattern_config_str, get_pattern_context,
16+
get_pattern_markdown, get_pattern_templates, get_sections,
17+
get_template_ancestors, is_pattern, render_pattern
1518
)
1619

1720

@@ -74,12 +77,20 @@ class RenderPatternView(TemplateView):
7477

7578
@method_decorator(xframe_options_sameorigin)
7679
def get(self, request, pattern_template_name=None):
80+
pattern_template_ancestors = get_template_ancestors(
81+
pattern_template_name,
82+
context=get_pattern_context(self.kwargs['pattern_template_name']),
83+
)
84+
pattern_is_fragment = set(pattern_template_ancestors).isdisjoint(set(get_base_template_names()))
85+
7786
try:
7887
rendered_pattern = render_pattern(request, pattern_template_name)
7988
except TemplateIsNotPattern:
8089
raise Http404
8190

82-
context = self.get_context_data()
83-
context['pattern_library_rendered_pattern'] = rendered_pattern
91+
if pattern_is_fragment:
92+
context = self.get_context_data()
93+
context['pattern_library_rendered_pattern'] = rendered_pattern
94+
return self.render_to_response(context)
8495

85-
return self.render_to_response(context)
96+
return HttpResponse(rendered_pattern)

tests/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
'SECTIONS': [
2525
('atoms', ['patterns/atoms']),
2626
('molecules', ['patterns/molecules']),
27+
('pages', ['patterns/pages']),
2728
],
2829
}
2930

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
included content from variable
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{% block content %}base content{% endblock %}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{% extends parent_template_name %}
2+
3+
{% block content %}{{ block.super }} - extended content{% endblock %}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
context:
2+
parent_template_name: patterns/atoms/test_extends/base.html
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{% load test_tags %}
22

33
{% include 'non-patterns/include.html' %}
4+
{% include variable_include %}
45

56
{% error_tag include %}

tests/templates/patterns/atoms/test_includes/test_includes.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
context:
2+
variable_include: non-patterns/variable_include.html
3+
14
tags:
25
error_tag:
36
include:

tests/templates/patterns/base.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
{% block content %}{{ pattern_library_rendered_pattern }}{% endblock %}
1+
<html lang="en-GB">
2+
<head>
3+
<title>{% block title %}Fragment{% endblock %}</title>
4+
</head>
5+
<body>
6+
{% block content %}{{ pattern_library_rendered_pattern }}{% endblock %}
7+
</body>
8+
</html>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{% extends 'patterns/base.html' %}
2+
3+
{% block title %}Page{% endblock %}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{% extends 'patterns/base_page.html' %}
2+
3+
{% block content %}{{ page.body }}{% endblock %}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
context:
2+
page:
3+
body: >
4+
<p>
5+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
6+
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
7+
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
8+
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
9+
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
10+
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
11+
</p>

0 commit comments

Comments
 (0)