diff --git a/README.md b/README.md index f0f6efed..22dd4ca6 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ This package automates the maintenance of UI pattern libraries or styleguides fo - Create reusable patterns by creating Django templates files as usual. - All patterns automatically show up in the pattern library’s interface. - Define data as YAML files for the templates to render with the relevant Django context. -- Override Django templates tags as needed to mock the template’s dependencies. +- Override Django Templates tags as needed to mock the template’s dependencies. - Document your patterns with Markdown. +- Experimental: support for Jinja templates. ## Why you need this diff --git a/docs/community/related-projects.md b/docs/community/related-projects.md index 00b1521b..5199e8c2 100644 --- a/docs/community/related-projects.md +++ b/docs/community/related-projects.md @@ -16,10 +16,9 @@ Here are other projects that are related to django-pattern-library, and may be r - [Storybook](https://storybook.js.org/), and in particular [Storybook for Server](https://github.com/storybookjs/storybook/tree/master/app/server) – Storybook integration with server-rendered UI components. - [Pattern Lab](http://patternlab.io/) – PHP or Node pattern library, from which this project is heavily inspired. - [Astrum](http://astrum.nodividestudio.com/) – Similar to Pattern Lab, Node based. -- [rikki-patterns](https://github.com/springload/rikki-patterns) – Experimental Django-friendly pattern library generator, for Jinja2 and Nunjucks templates +- [rikki-patterns](https://github.com/springload/rikki-patterns) – Experimental Django-friendly pattern library generator, for Jinja and Nunjucks templates - [django-lookbook](https://github.com/rails-inspire-django/django-lookbook) - Empower your Django development with this pluggable app for creating a robust component library. Includes preview system, documentation engine, and parameter editor for building modular UI effortlessly. - ## Pattern libraries based on Django Here are open-source projects that maintain pattern libraries for Django. diff --git a/docs/getting-started.md b/docs/getting-started.md index f5ac2d6f..c47e882d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -19,7 +19,7 @@ We support: - Django 4.2, 5.0, 5.1 - Python 3.9, 3.10, 3.11, 3.12 -- Django Templates only, no Jinja support +- Django Templates and Jinja (experimental) - Modern “evergreen” desktop and mobile browsers ## Configuration @@ -36,7 +36,7 @@ INSTALLED_APPS = [ ] ``` -Also add `pattern_library.loader_tags` to `OPTIONS["builtins"]` into the `TEMPLATES` setting: +For Django Templates, add `pattern_library.loader_tags` to `OPTIONS["builtins"]` into the `TEMPLATES` setting: ```python hl_lines="13 14 15" TEMPLATES = [ @@ -59,6 +59,15 @@ TEMPLATES = [ ] ``` +Experimental: for Jinja support, call `override_jinja_tags` in the file that contains your Jinja environment: + +```python +from pattern_library.monkey_utils import override_jinja_tags + +if apps.is_installed("pattern_library"): + override_jinja_tags() +``` + To see the detailed error pages generated by Django when you have `DEBUG = True` in the pattern library, you'll need to make sure you have `X_FRAME_OPTIONS` set, or your browser will block the response: ```python diff --git a/docs/guides/overriding-template-tags.md b/docs/guides/overriding-template-tags.md index ea412efd..f90013ac 100644 --- a/docs/guides/overriding-template-tags.md +++ b/docs/guides/overriding-template-tags.md @@ -1,18 +1,18 @@ # Overriding template tags -The package overrides the following Django tags: +The package overrides Django’s `extends` and `include` tags, implementing custom behaviour for these tags only when rendering in the pattern library. It falls back to Django's standard behaviour on all other cases. This makes it possible to define fake template contexts once and then have it reused everywhere a template partial is included. -- `{% extends %}` -- `{% include %}` +--- -It's required to allow us to define fake template context and override other template tags in YAML files. -This package uses custom behaviour for these tags only when rendering pattern library and falls back to Django's standard behaviour on all other cases. - -The override process has two parts: +We can also override other template tags in YAML files. The override process has two parts: 1. Override your template tag with a mock implementation 2. Define fake result for your tag in a YAML file +!!! warning "No Jinja support" + + Overriding arbitrary template tags or functions is currently unsupported for Jinja templates. + ## Providing a default value for template tags To provide a default for a template tag, you need to provide a keyword argument default_html when overriding your tag. diff --git a/docs/index.md b/docs/index.md index 4cd9ef93..013cd1b6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,8 +9,9 @@ The [django-pattern-library](https://pypi.org/project/django-pattern-library/) p - Create reusable patterns by creating Django templates files as usual. - All patterns automatically show up in the pattern library’s interface. - Define data as YAML files for the templates to render with the relevant Django context. -- Override Django templates tags as needed to mock the template’s dependencies. +- Override Django Templates tags as needed to mock the template’s dependencies. - Document your patterns with Markdown. +- Experimental: support for Jinja templates. Here is a screenshot of the pattern library in action: @@ -45,4 +46,3 @@ To learn more about how this package can be used, have a look at our talk: [Reusable UI components: A journey from React to Wagtail](https://www.youtube.com/watch?v=isrOufI7TKc) [![Reusable UI components: A journey from React to Wagtail](images/pattern-library-talk-youtube.webp)](https://www.youtube.com/watch?v=isrOufI7TKc) - diff --git a/docs/reference/api.md b/docs/reference/api.md index e20f82e7..167ab91f 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -24,6 +24,7 @@ context: - 2 - 3 # Mapping from tag names to tag overrides. +# Currently unsupported for Jinja templates. tags: error_tag: include: @@ -135,7 +136,7 @@ PATTERN_LIBRARY = { ### `override_tag` -This function tells the pattern library which Django tags to override, and optionally supports providing a default value. See [Overriding template tags](../guides/overriding-template-tags.md) for more information. +This function tells the pattern library which Django Templates tags to override, and optionally supports providing a default value. See [Overriding template tags](../guides/overriding-template-tags.md) for more information. ```python from pattern_library.monkey_utils import override_tag @@ -143,6 +144,16 @@ from pattern_library.monkey_utils import override_tag override_tag(register, 'a_tag_name', default_html="https://example.com/") ``` +### `override_jinja_tags` + +🚧 Experimental. Optionally override `extends` and `include` in Jinja templates, so context for partials can be defined once and reused everywhere. See [Overriding template tags](../guides/overriding-template-tags.md). Call this in your Django settings file or at the top level of the file defining your Jinja environment. + +```python +from pattern_library.monkey_utils import override_jinja_tags + +override_jinja_tags() +``` + ## `register_context_modifier` This decorator makes it possible to override or create additional context data with Django / Python code, rather than being limited to YAML. It has to be called from within a `pattern_contexts` module, which can be at the root of any Django app. See [Modifying template contexts with Python](../guides/defining-template-context.md#modifying-template-contexts-with-python) for more information. diff --git a/docs/reference/known-issues.md b/docs/reference/known-issues.md index 97098125..583b3886 100644 --- a/docs/reference/known-issues.md +++ b/docs/reference/known-issues.md @@ -4,7 +4,7 @@ django-pattern-library has a few known limitations due to its design, which are ## Overriding filters is not supported -See [#114](https://github.com/torchbox/django-pattern-library/issues/114). PRs welcome! +See [#114](https://github.com/torchbox/django-pattern-library/issues/114) for Django Templates. PRs welcome! ## Can’t override context in a child template @@ -62,12 +62,16 @@ See [#138](https://github.com/torchbox/django-pattern-library/issues/138). For e This can’t be mocked for all usage of `include_block`. -## Jinja2 support +## Jinja2 overrides -Or lack thereof! If you’re interested in this, please share your thoughts with us on [#180](https://github.com/torchbox/django-pattern-library/discussions/180). +There is experimental support, excluding overrides of arbitrary tags, functions, and filters. If you’re interested in this, please share your thoughts with us on [#180](https://github.com/torchbox/django-pattern-library/discussions/180). ## Past limitations +### Jinja2 support + +🎉 This is now addressed as of v1.5.0, though with only experimental support, and no capability to override tags, functions, filters (see above). + ### No way to specify objects that have attributes and support iteration 🎉 This is now addressed as of v0.5.0, with the [context modifiers in Python](../guides/defining-template-context.md#modifying-template-contexts-with-python) API. View our [pagination](../recipes/pagination.md) recipe. diff --git a/pattern_library/loader_tags.py b/pattern_library/loader_tags.py index 2efb6cfb..80dd1116 100644 --- a/pattern_library/loader_tags.py +++ b/pattern_library/loader_tags.py @@ -150,3 +150,51 @@ def do_include(parser, token): extra_context=namemap, isolated_context=isolated_context, ) + + +def visit_extends(self, node, frame): + """This method overrides the jinja extends tag + Is called as part of the compiler CodeGenerator + and adds a line to use the template_new_context as + part of the runtime render to pull in the dpl context + Handles visiting extends + """ + from .monkey_utils import jinja_visit_Extends + + jinja_visit_Extends(self, node, frame) + # addition to update the context with dpl context + # calls the template_new_context method below when + # invoked at runtime + self.writeline( + "parent_template.new_context(context.get_all(), True," + f" {self.dump_local_context(frame)})" + ) + + +def template_new_context( + self, + vars=None, + shared=False, + locals=None, +): + """This method overrides the jinja include tag + Is called as part of Template.render by jinja2 and is updated + to pull in the dpl context + Create a new :class:`Context` for this template. The vars + provided will be passed to the template. Per default the globals + are added to the context. If shared is set to `True` the data + is passed as is to the context without adding the globals. + + `locals` can be a dict of local variables for internal usage. + """ + from jinja2.runtime import new_context + + if is_pattern_library_context(vars or {}) and ( + pattern_context := get_pattern_context(self.name) + ): + for k, v in pattern_context.items(): + vars.setdefault(k, v) + + return new_context( + self.environment, self.name, self.blocks, vars, shared, self.globals, locals + ) diff --git a/pattern_library/management/commands/render_patterns.py b/pattern_library/management/commands/render_patterns.py index 482ae902..32268b7a 100644 --- a/pattern_library/management/commands/render_patterns.py +++ b/pattern_library/management/commands/render_patterns.py @@ -5,12 +5,7 @@ from django.test.client import RequestFactory from pattern_library import get_base_template_names, get_pattern_base_template_name -from pattern_library.utils import ( - get_pattern_context, - get_pattern_templates, - get_template_ancestors, - render_pattern, -) +from pattern_library.utils import get_pattern_context, get_renderer, render_pattern class Command(BaseCommand): @@ -44,7 +39,8 @@ def handle(self, **options): self.wrap_fragments = options["wrap_fragments"] self.output_dir = options["output_dir"] - templates = get_pattern_templates() + renderer = get_renderer() + templates = renderer.get_pattern_templates() factory = RequestFactory() request = factory.get("/") @@ -106,7 +102,8 @@ def render_pattern(self, request, pattern_template_name): if not self.wrap_fragments: return rendered_pattern - pattern_template_ancestors = get_template_ancestors( + renderer = get_renderer() + pattern_template_ancestors = renderer.get_template_ancestors( pattern_template_name, context=get_pattern_context(pattern_template_name), ) diff --git a/pattern_library/monkey_utils.py b/pattern_library/monkey_utils.py index 12eeb3e8..6bf962b5 100644 --- a/pattern_library/monkey_utils.py +++ b/pattern_library/monkey_utils.py @@ -117,3 +117,27 @@ def node_render(context): return original_node return tag_func + + +# have to export the original jinja visit Extends +# in the case jinja tags are being overriden +jinja_visit_Extends = None + + +def override_jinja_tags(): + """ + Experimental. Overrides jinja extends and include tags for use in your pattern library. + Call it in your settings to override tags + """ + global jinja_visit_Extends + try: + from jinja2.compiler import CodeGenerator as JinjaCodeGenerator + from jinja2.environment import Template as JinjaTemplate + except ModuleNotFoundError: + ModuleNotFoundError("install jinja2 to override jinja tags") + + from pattern_library.loader_tags import template_new_context, visit_extends + + jinja_visit_Extends = JinjaCodeGenerator.visit_Extends + JinjaTemplate.new_context = template_new_context + JinjaCodeGenerator.visit_Extends = visit_extends diff --git a/pattern_library/utils.py b/pattern_library/utils.py index b1a81b8d..6389f7ad 100644 --- a/pattern_library/utils.py +++ b/pattern_library/utils.py @@ -8,6 +8,7 @@ from django.template.loader import get_template, render_to_string from django.template.loader_tags import ExtendsNode from django.template.loaders.app_directories import get_app_template_dirs +from django.utils.html import escape from django.utils.safestring import mark_safe import markdown @@ -79,77 +80,6 @@ def get_template_dirs(): return template_dirs -def get_pattern_templates(): - templates = base_dict() - template_dirs = get_template_dirs() - - for lookup_dir in template_dirs: - for root, dirs, files in os.walk(lookup_dir, topdown=True): - # Ignore folders without files - if not files: - continue - - base_path = os.path.relpath(root, lookup_dir) - section, path = section_for(base_path) - - # It has no section, ignore it - if not section: - continue - - found_templates = [] - for current_file in files: - pattern_path = os.path.join(root, current_file) - pattern_path = os.path.relpath(pattern_path, lookup_dir) - - if is_pattern(pattern_path): - template = get_template(pattern_path) - pattern_config = get_pattern_config(pattern_path) - pattern_name = pattern_config.get("name") - pattern_filename = os.path.relpath( - template.origin.template_name, - base_path, - ) - if pattern_name: - template.pattern_name = pattern_name - else: - template.pattern_name = pattern_filename - - template.pattern_filename = pattern_filename - - found_templates.append(template) - - if found_templates: - lookup_dir_relpath = os.path.relpath(root, lookup_dir) - sub_folders = os.path.relpath(lookup_dir_relpath, path) - templates_to_store = templates - for folder in [section, *sub_folders.split(os.sep)]: - try: - templates_to_store = templates_to_store["template_groups"][ - folder - ] - except KeyError: - templates_to_store["template_groups"][folder] = base_dict() - templates_to_store = templates_to_store["template_groups"][ - folder - ] - - templates_to_store["templates_stored"].extend(found_templates) - - # Order the templates alphabetically - for templates_objs in templates["template_groups"].values(): - templates_objs["template_groups"] = order_dict( - templates_objs["template_groups"] - ) - - # Order the top level by the sections - section_order = [section for section, _ in get_sections()] - templates["template_groups"] = order_dict( - templates["template_groups"], key_sort=lambda key: section_order.index(key) - ) - - return templates - - def get_pattern_config_str(template_name): replace_pattern = "{}$".format(get_pattern_template_suffix()) context_path = re.sub(replace_pattern, "", template_name) @@ -227,27 +157,163 @@ def render_pattern(request, template_name, allow_non_patterns=False, config=None return render_to_string(template_name, request=request, context=context) -def get_template_ancestors(template_name, context=None, ancestors=None): - """ - Returns a list of template names, starting with provided name - and followed by the names of any templates that extends until - the most extended template is reached. - """ - if ancestors is None: - ancestors = [template_name] +def get_renderer(): + return TemplateRenderer + + +class TemplateRenderer: + @classmethod + def get_pattern_templates(cls): + templates = base_dict() + template_dirs = get_template_dirs() + + for lookup_dir in template_dirs: + for root, dirs, files in os.walk(lookup_dir, topdown=True): + # Ignore folders without files + if not files: + continue + + base_path = os.path.relpath(root, lookup_dir) + section, path = section_for(base_path) + + # It has no section, ignore it + if not section: + continue + + found_templates = [] + for current_file in files: + pattern_path = os.path.join(root, current_file) + pattern_path = os.path.relpath(pattern_path, lookup_dir) + + if is_pattern(pattern_path): + template = get_template(pattern_path) + pattern_config = get_pattern_config(pattern_path) + pattern_name = pattern_config.get("name") + pattern_filename = os.path.relpath( + template.origin.template_name, + base_path, + ) + if pattern_name: + template.pattern_name = pattern_name + else: + template.pattern_name = pattern_filename + + template.pattern_filename = pattern_filename + + found_templates.append(template) + + if found_templates: + lookup_dir_relpath = os.path.relpath(root, lookup_dir) + sub_folders = os.path.relpath(lookup_dir_relpath, path) + templates_to_store = templates + for folder in [section, *sub_folders.split(os.sep)]: + try: + templates_to_store = templates_to_store["template_groups"][ + folder + ] + except KeyError: + templates_to_store["template_groups"][folder] = base_dict() + + templates_to_store = templates_to_store["template_groups"][ + folder + ] + + templates_to_store["templates_stored"].extend(found_templates) + + # Order the templates alphabetically + for templates_objs in templates["template_groups"].values(): + templates_objs["template_groups"] = order_dict( + templates_objs["template_groups"] + ) + + # Order the top level by the sections + section_order = [section for section, _ in get_sections()] + templates["template_groups"] = order_dict( + templates["template_groups"], key_sort=lambda key: section_order.index(key) + ) - if context is None: - context = Context() + return templates - pattern_template = get_template(template_name) + @classmethod + def get_pattern_source(cls, template): + return cls._get_engine(template).get_pattern_source(template) + + @classmethod + def get_template_ancestors(cls, template_name, context=None): + template = get_template(template_name) + return cls._get_engine(template).get_template_ancestors( + template_name, context=context + ) - for node in pattern_template.template.nodelist: - if isinstance(node, ExtendsNode): - parent_template_name = node.parent_name.resolve(context) + @classmethod + def _get_engine(cls, template): + if "jinja" in str(type(template)).lower(): + return JinjaTemplateRenderer + return DTLTemplateRenderer + + +class DTLTemplateRenderer: + @staticmethod + def get_pattern_source(template): + return escape(template.template.source) + + @classmethod + def get_template_ancestors(cls, template_name, context=None, ancestors=None): + """ + Returns a list of template names, starting with provided name + and followed by the names of any templates that extends until + the most extended template is reached. + """ + if ancestors is None: + ancestors = [template_name] + + if context is None: + context = Context() + + pattern_template = get_template(template_name) + + for node in pattern_template.template.nodelist: + if isinstance(node, ExtendsNode): + parent_template_name = node.parent_name.resolve(context) + ancestors.append(parent_template_name) + cls.get_template_ancestors( + parent_template_name, context=context, ancestors=ancestors + ) + break + + return ancestors + + +class JinjaTemplateRenderer: + @staticmethod + def get_pattern_source(template): + with open(template.template.filename) as f: + source = escape(f.read()) + return source + + @classmethod + def get_template_ancestors(cls, template_name, context=None, ancestors=None): + """ + Returns a list of template names, starting with provided name + and followed by the names of any templates that extends until + the most extended template is reached. + """ + from jinja2.nodes import Extends + + if ancestors is None: + ancestors = [template_name] + + if context is None: + context = Context() + + pattern_template = get_template(template_name) + environment = pattern_template.template.environment + nodelist = environment.parse(pattern_template.template.name) + parent_template_name = nodelist.find(Extends) + if parent_template_name: ancestors.append(parent_template_name) - get_template_ancestors( + cls.get_template_ancestors( parent_template_name, context=context, ancestors=ancestors ) - break - return ancestors + return ancestors diff --git a/pattern_library/views.py b/pattern_library/views.py index 4e3ab01d..99d1dccb 100644 --- a/pattern_library/views.py +++ b/pattern_library/views.py @@ -4,6 +4,7 @@ from django.template.loader import get_template from django.utils.decorators import method_decorator from django.utils.html import escape +from django.utils.safestring import mark_safe from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.csrf import csrf_exempt from django.views.generic.base import TemplateView @@ -15,9 +16,8 @@ get_pattern_config_str, get_pattern_context, get_pattern_markdown, - get_pattern_templates, + get_renderer, get_sections, - get_template_ancestors, is_pattern, render_pattern, ) @@ -52,7 +52,8 @@ def get_first_template(self, templates): def get(self, request, pattern_template_name=None): # Get all pattern templates - templates = get_pattern_templates() + renderer = get_renderer() + templates = renderer.get_pattern_templates() if pattern_template_name is None: # Just display the first pattern if a specific one isn't requested @@ -67,7 +68,7 @@ def get(self, request, pattern_template_name=None): context = self.get_context_data() context["pattern_templates"] = templates context["pattern_template_name"] = pattern_template_name - context["pattern_source"] = escape(template.template.source) + context["pattern_source"] = renderer.get_pattern_source(template) context["pattern_config"] = escape( get_pattern_config_str(pattern_template_name) ) @@ -83,7 +84,8 @@ class RenderPatternView(TemplateView): @method_decorator(xframe_options_sameorigin) def get(self, request, pattern_template_name=None): - pattern_template_ancestors = get_template_ancestors( + renderer = get_renderer() + pattern_template_ancestors = renderer.get_template_ancestors( pattern_template_name, context=get_pattern_context(self.kwargs["pattern_template_name"]), ) @@ -98,7 +100,7 @@ def get(self, request, pattern_template_name=None): if pattern_is_fragment: context = self.get_context_data() - context["pattern_library_rendered_pattern"] = rendered_pattern + context["pattern_library_rendered_pattern"] = mark_safe(rendered_pattern) return self.render_to_response(context) return HttpResponse(rendered_pattern) diff --git a/tests/jinja/non-patterns/include.html b/tests/jinja/non-patterns/include.html new file mode 100644 index 00000000..db9fcf7f --- /dev/null +++ b/tests/jinja/non-patterns/include.html @@ -0,0 +1,4 @@ +SHOWME +{% if False %} +HIDEME +{% endif %} diff --git a/tests/jinja/non-patterns/variable_include.html b/tests/jinja/non-patterns/variable_include.html new file mode 100644 index 00000000..be595c6b --- /dev/null +++ b/tests/jinja/non-patterns/variable_include.html @@ -0,0 +1 @@ +included content from variable diff --git a/tests/jinja/patterns_jinja/base_jinja.html b/tests/jinja/patterns_jinja/base_jinja.html new file mode 100644 index 00000000..3b9bd618 --- /dev/null +++ b/tests/jinja/patterns_jinja/base_jinja.html @@ -0,0 +1,16 @@ + + + + + {% block title %}Fragment{% endblock %} + + + + + +
+ {% block content %}{% endblock %} +
+ + + diff --git a/tests/jinja/patterns_jinja/base_page_jinja.html b/tests/jinja/patterns_jinja/base_page_jinja.html new file mode 100644 index 00000000..894070a7 --- /dev/null +++ b/tests/jinja/patterns_jinja/base_page_jinja.html @@ -0,0 +1,3 @@ +{% extends 'patterns_jinja/base_jinja.html' %} + +{% block title %}{{ page.title }}{% endblock %} diff --git a/tests/jinja/patterns_jinja/components/accordion/accordion.html b/tests/jinja/patterns_jinja/components/accordion/accordion.html new file mode 100644 index 00000000..e63f1fbd --- /dev/null +++ b/tests/jinja/patterns_jinja/components/accordion/accordion.html @@ -0,0 +1,17 @@ +
+ {% for accordion in accordions %} +
+

+ +

+
+

{{ accordion.description }}

+
+
+ {% endfor %} +
diff --git a/tests/jinja/patterns_jinja/components/accordion/accordion.md b/tests/jinja/patterns_jinja/components/accordion/accordion.md new file mode 100644 index 00000000..f4d5a57f --- /dev/null +++ b/tests/jinja/patterns_jinja/components/accordion/accordion.md @@ -0,0 +1,3 @@ +# Accordion (Jinja) + +The accordion is a good example of a pattern library component. This one uses [Jinja](https://jinja.palletsprojects.com/). diff --git a/tests/jinja/patterns_jinja/components/accordion/accordion.yaml b/tests/jinja/patterns_jinja/components/accordion/accordion.yaml new file mode 100644 index 00000000..10cb2aca --- /dev/null +++ b/tests/jinja/patterns_jinja/components/accordion/accordion.yaml @@ -0,0 +1,11 @@ +context: + id_prefix: test + accordions: + - title: Title A (Jinja) + description: Description A. Ask for help. + - title: Title B + description: Description B. Ask for help. + - title: Title C + description: Description C. Ask for help. + - title: Title D + description: Description D. Ask for help. diff --git a/tests/jinja/patterns_jinja/components/button/button.html b/tests/jinja/patterns_jinja/components/button/button.html new file mode 100644 index 00000000..a027a3f6 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/button/button.html @@ -0,0 +1,3 @@ + + {% if label %}{{ label }}{% else %}{{ target_page.title }}{% endif %} + diff --git a/tests/jinja/patterns_jinja/components/button/button.yaml b/tests/jinja/patterns_jinja/components/button/button.yaml new file mode 100644 index 00000000..4546360f --- /dev/null +++ b/tests/jinja/patterns_jinja/components/button/button.yaml @@ -0,0 +1,7 @@ +context: + target_page: + title: Get started +tags: + pageurl: + target_page: + raw: /get-started diff --git a/tests/jinja/patterns_jinja/components/icons/icon.html b/tests/jinja/patterns_jinja/components/icons/icon.html new file mode 100644 index 00000000..2875a347 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/icons/icon.html @@ -0,0 +1,3 @@ + diff --git a/tests/jinja/patterns_jinja/components/icons/icon.yaml b/tests/jinja/patterns_jinja/components/icons/icon.yaml new file mode 100644 index 00000000..2ebc4180 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/icons/icon.yaml @@ -0,0 +1,2 @@ +context: + name: close diff --git a/tests/jinja/patterns_jinja/components/sprites/sprites.html b/tests/jinja/patterns_jinja/components/sprites/sprites.html new file mode 100644 index 00000000..06ccb956 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/sprites/sprites.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + +{# Show available icons, in pattern library view only #} +{% if is_pattern_library %} + + +{% endif %} diff --git a/tests/jinja/patterns_jinja/components/test_atom/test_atom.html b/tests/jinja/patterns_jinja/components/test_atom/test_atom.html new file mode 100644 index 00000000..9635774f --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_atom/test_atom.html @@ -0,0 +1 @@ +{{ atom_var }} diff --git a/tests/jinja/patterns_jinja/components/test_atom/test_atom.md b/tests/jinja/patterns_jinja/components/test_atom/test_atom.md new file mode 100644 index 00000000..3f4fce46 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_atom/test_atom.md @@ -0,0 +1,12 @@ +# Documentation for test atom - heading 1 + +**bold text** + +*italic text* + +## Here's a heading 2 + +### Here's a heading 3 + +- here's a list item +- and another diff --git a/tests/jinja/patterns_jinja/components/test_atom/test_atom.yaml b/tests/jinja/patterns_jinja/components/test_atom/test_atom.yaml new file mode 100644 index 00000000..f7baf100 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_atom/test_atom.yaml @@ -0,0 +1,2 @@ +context: + atom_var: 'atom_var value from test_atom.yaml' diff --git a/tests/jinja/patterns_jinja/components/test_extends/base.html b/tests/jinja/patterns_jinja/components/test_extends/base.html new file mode 100644 index 00000000..43c5ba6b --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_extends/base.html @@ -0,0 +1 @@ +{% block content %}base content{% endblock %} diff --git a/tests/jinja/patterns_jinja/components/test_extends/extended.html b/tests/jinja/patterns_jinja/components/test_extends/extended.html new file mode 100644 index 00000000..cfae2ef3 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_extends/extended.html @@ -0,0 +1,3 @@ +{% extends parent_template_name %} + +{% block content %}{{ super() }} - extended content{% endblock %} diff --git a/tests/jinja/patterns_jinja/components/test_extends/extended.yaml b/tests/jinja/patterns_jinja/components/test_extends/extended.yaml new file mode 100644 index 00000000..10ed3936 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_extends/extended.yaml @@ -0,0 +1,2 @@ +context: + parent_template_name: patterns_jinja/components/test_extends/base.html diff --git a/tests/jinja/patterns_jinja/components/test_includes/test_includes.html b/tests/jinja/patterns_jinja/components/test_includes/test_includes.html new file mode 100644 index 00000000..363b9015 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_includes/test_includes.html @@ -0,0 +1,5 @@ +{% include 'non-patterns/include.html' %} +{% include variable_include %} + +{# TODO: Unsupported override with Jinja #} +{# {{ error_tag() }} #} diff --git a/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml b/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml new file mode 100644 index 00000000..e4fa1aab --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml @@ -0,0 +1,8 @@ +context: + variable_include: non-patterns/variable_include.html + +# TODO: Unsupported override with Jinja +tags: + error_tag: + include: + template_name: "non-patterns/include.html" diff --git a/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.html b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.html new file mode 100644 index 00000000..9ca10558 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.html @@ -0,0 +1,3 @@ +{% include 'patterns_jinja/components/test_atom/test_atom.html' %} +{% set atom_var='atom_var value from test_molecule.html include tag' %} +{% include 'patterns_jinja/components/test_atom/test_atom.html' %} diff --git a/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.yaml b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.yaml new file mode 100644 index 00000000..af5614be --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.yaml @@ -0,0 +1,5 @@ +name: Pretty name for test molecule + +context: + atom_var: 'atom_var value from test_molecule.yaml' + utf8_test_var: '你好,世界' diff --git a/tests/jinja/patterns_jinja/components/test_molecule/test_molecule_no_context.html b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule_no_context.html new file mode 100644 index 00000000..4981b0f6 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule_no_context.html @@ -0,0 +1 @@ +No context diff --git a/tests/jinja/patterns_jinja/pages/search/search.html b/tests/jinja/patterns_jinja/pages/search/search.html new file mode 100644 index 00000000..44423c58 --- /dev/null +++ b/tests/jinja/patterns_jinja/pages/search/search.html @@ -0,0 +1,34 @@ +{% extends "patterns_jinja/base_jinja.html" %} + +{% block content %} +

{% if search_query %}Search results for “{{ search_query }}”{% else %}Search{% endif %}

+ + {% if search_results %} + {% with count=paginator.count %} + {{ count }} result{{ count|pluralize }} found. + {% endwith %} + + {% for result in search_results %} +

{{ result.title }}

+ {% endfor %} + + {% if paginator.num_pages > 1 %} + + {% endif %} + + {% elif search_query %} + No results found. + {% endif %} +{% endblock %} diff --git a/tests/jinja/patterns_jinja/pages/search/search.yaml b/tests/jinja/patterns_jinja/pages/search/search.yaml new file mode 100644 index 00000000..69e53929 --- /dev/null +++ b/tests/jinja/patterns_jinja/pages/search/search.yaml @@ -0,0 +1,5 @@ +context: + search_query: test query + search_results: + - title: First result + - title: Second result diff --git a/tests/jinja/patterns_jinja/pages/test_page/test_page.html b/tests/jinja/patterns_jinja/pages/test_page/test_page.html new file mode 100644 index 00000000..4ffb7218 --- /dev/null +++ b/tests/jinja/patterns_jinja/pages/test_page/test_page.html @@ -0,0 +1,12 @@ +{% extends 'patterns_jinja/base_page_jinja.html' %} + +{% block content %} +

{{ page.title }}

+{{ page.body }} + +{% include "patterns_jinja/components/accordion/accordion.html" %} + +{% if is_pattern_library %} +

is_pattern_library = {{ is_pattern_library }}

+{% endif %} +{% endblock %} diff --git a/tests/jinja/patterns_jinja/pages/test_page/test_page.yaml b/tests/jinja/patterns_jinja/pages/test_page/test_page.yaml new file mode 100644 index 00000000..4812e07e --- /dev/null +++ b/tests/jinja/patterns_jinja/pages/test_page/test_page.yaml @@ -0,0 +1,12 @@ +context: + page: + title: Jinja test page + body: > +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat + non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +

diff --git a/tests/jinja2.py b/tests/jinja2.py new file mode 100644 index 00000000..fa55978c --- /dev/null +++ b/tests/jinja2.py @@ -0,0 +1,55 @@ +from django.apps import apps +from django.contrib.staticfiles.storage import staticfiles_storage +from django.template.context_processors import csrf +from django.template.defaultfilters import ( + cut, + date, + linebreaks, + pluralize, + slugify, + truncatewords, + urlencode, +) +from django.urls import reverse + +from jinja2 import Environment + +from pattern_library.monkey_utils import override_jinja_tags + +if apps.is_installed("pattern_library"): + override_jinja_tags() + + +def error_tag(): + "Just raise an exception, never do anything" + raise Exception("error_tag raised an exception") + + +def pageurl(page): + """Approximation of wagtail built-in tag for realistic example.""" + return "/page/url" + + +def environment(**options): + env = Environment(**options) + env.globals.update( + { + "static": staticfiles_storage.url, + "url": reverse, + "csrf": csrf, + "error_tag": error_tag, + "pageurl": pageurl, + } + ) + env.filters.update( + { + "cut": cut, + "date": date, + "linebreaks": linebreaks, + "pluralize": pluralize, + "slugify": slugify, + "truncatewords": truncatewords, + "urlencode": urlencode, + } + ) + return env diff --git a/tests/pattern_contexts.py b/tests/pattern_contexts.py index 9857698e..571841e9 100644 --- a/tests/pattern_contexts.py +++ b/tests/pattern_contexts.py @@ -31,7 +31,34 @@ def replicate_pagination(context, request): # add dummy items to force pagination for i in range(50): - object_list.append(None) + object_list.append({"title": i}) + + # paginate and add ListView-like values + paginator = Paginator(object_list, original_length) + context.update( + paginator=paginator, + search_results=paginator.page(10), + is_paginated=True, + object_list=object_list, + ) + + +@register_context_modifier(template="patterns_jinja/pages/search/search.html") +def replicate_pagination_jinja(context, request): + """ + Replace lists of items using the 'page_obj.object_list' key + with a real Paginator page, and add a few other pagination-related + things to the context (like Django's `ListView` does). + """ + object_list = context.pop("search_results", None) + if object_list is None: + return + + original_length = len(object_list) + + # add dummy items to force pagination + for i in range(50): + object_list.append({"title": i}) # paginate and add ListView-like values paginator = Paginator(object_list, original_length) diff --git a/tests/settings/base.py b/tests/settings/base.py index a226c5fd..7a615f8f 100644 --- a/tests/settings/base.py +++ b/tests/settings/base.py @@ -42,12 +42,17 @@ ("atoms", ["patterns/atoms"]), ("molecules", ["patterns/molecules"]), ("pages", ["patterns/pages"]), + ("jinja", ["patterns_jinja/components"]), + ("jinja pages", ["patterns_jinja/pages"]), ], } TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + "tests/templates", + ], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -62,6 +67,21 @@ "builtins": ["pattern_library.loader_tags"], }, }, + { + "BACKEND": "django.template.backends.jinja2.Jinja2", + "DIRS": [ + "tests/jinja", + ], + "APP_DIRS": True, + "OPTIONS": { + "environment": "tests.jinja2.environment", + "extensions": [ + "jinja2.ext.do", + "jinja2.ext.i18n", + "jinja2.ext.loopcontrols", + ], + }, + }, ] MIDDLEWARE = [ diff --git a/tests/templates/patterns/pages/people/person_page.yaml b/tests/templates/patterns/pages/people/person_page.yaml index c82ed9bf..e8c3ea12 100644 --- a/tests/templates/patterns/pages/people/person_page.yaml +++ b/tests/templates/patterns/pages/people/person_page.yaml @@ -5,12 +5,12 @@ context: last_name: Doe photo: fake job_title: Stub person - website: 'https://example.com' - email: 'test@example.com' + website: "https://example.com" + email: "test@example.com" social_media_profile: all: - - profile_url: 'https://example.com/' - - profile_url: 'https://example.com/2' + - profile_url: "https://example.com/" + - profile_url: "https://example.com/2" phone_numbers: all: - phone_number: 12345678 diff --git a/tests/templates/patterns/pages/test_page/test_page.html b/tests/templates/patterns/pages/test_page/test_page.html index 5264f100..e5a241fb 100644 --- a/tests/templates/patterns/pages/test_page/test_page.html +++ b/tests/templates/patterns/pages/test_page/test_page.html @@ -1,3 +1,12 @@ {% extends 'patterns/base_page.html' %} -{% block content %}{{ page.body }}{% endblock %} +{% block content %} +

{{ page.title }}

+{{ page.body }} + +{% include "patterns/molecules/accordion/accordion.html" %} + +{% if is_pattern_library %} +

is_pattern_library = {{ is_pattern_library }}

+{% endif %} +{% endblock %} diff --git a/tests/tests/test_context_modifiers.py b/tests/tests/test_context_modifiers.py index 12ddd9f5..1fcb7450 100644 --- a/tests/tests/test_context_modifiers.py +++ b/tests/tests/test_context_modifiers.py @@ -33,7 +33,6 @@ def modifier_3(context, request): class ContextModifierTestCase(SimpleTestCase): - maxDiff = None def setUp(self): diff --git a/tests/tests/test_utils.py b/tests/tests/test_utils.py index 9730be5f..fe3191c6 100644 --- a/tests/tests/test_utils.py +++ b/tests/tests/test_utils.py @@ -5,15 +5,20 @@ from pattern_library.utils import ( get_pattern_config_str, - get_template_ancestors, + get_renderer, get_template_dirs, ) class TestGetTemplateAncestors(SimpleTestCase): + def setUp(self): + self.renderer = get_renderer() + def test_page(self): self.assertEqual( - get_template_ancestors("patterns/pages/test_page/test_page.html"), + self.renderer.get_template_ancestors( + "patterns/pages/test_page/test_page.html" + ), [ "patterns/pages/test_page/test_page.html", "patterns/base_page.html", @@ -23,7 +28,9 @@ def test_page(self): def test_fragment(self): self.assertEqual( - get_template_ancestors("patterns/atoms/test_atom/test_atom.html"), + self.renderer.get_template_ancestors( + "patterns/atoms/test_atom/test_atom.html" + ), [ "patterns/atoms/test_atom/test_atom.html", ], @@ -31,7 +38,7 @@ def test_fragment(self): def test_parent_template_from_variable(self): self.assertEqual( - get_template_ancestors( + self.renderer.get_template_ancestors( "patterns/atoms/test_extends/extended.html", context={"parent_template_name": "patterns/base.html"}, ),