fix: compat with block tag in django mode (#511)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-05-30 08:34:58 +02:00 committed by GitHub
parent 0101f6dae6
commit b8ff610a48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 92 additions and 39 deletions

View file

@ -1,6 +1,7 @@
import inspect import inspect
import os import os
import sys import sys
import types
from pathlib import Path from pathlib import Path
from typing import Any, ClassVar, Dict, List, Mapping, MutableMapping, Optional, Tuple, Type, Union from typing import Any, ClassVar, Dict, List, Mapping, MutableMapping, Optional, Tuple, Type, Union
@ -11,6 +12,7 @@ from django.template.base import FilterExpression, Node, NodeList, Template, Tex
from django.template.context import Context from django.template.context import Context
from django.template.exceptions import TemplateSyntaxError from django.template.exceptions import TemplateSyntaxError
from django.template.loader import get_template from django.template.loader import get_template
from django.template.loader_tags import BLOCK_CONTEXT_KEY
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import SafeString, mark_safe from django.utils.safestring import SafeString, mark_safe
from django.views import View from django.views import View
@ -285,7 +287,14 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
# See https://github.com/EmilStenstrom/django-components/issues/414 # See https://github.com/EmilStenstrom/django-components/issues/414
context = context if isinstance(context, Context) else Context(context) context = context if isinstance(context, Context) else Context(context)
prepare_context(context, self.component_id) prepare_context(context, self.component_id)
template = self.get_template(context) template = self.get_template(context)
_monkeypatch_template(template)
# Set `Template._is_component_nested` based on whether we're currently INSIDE
# the `{% extends %}` tag.
# Part of fix for https://github.com/EmilStenstrom/django-components/issues/508
template._is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
# Support passing slots explicitly to `render` method # Support passing slots explicitly to `render` method
if slots_data: if slots_data:
@ -445,3 +454,47 @@ class ComponentNode(Node):
trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!") trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!")
return output return output
def _monkeypatch_template(template: Template) -> None:
# Modify `Template.render` to set `isolated_context` kwarg of `push_state`
# based on our custom `Template._is_component_nested`.
#
# Part of fix for https://github.com/EmilStenstrom/django-components/issues/508
#
# NOTE 1: While we could've subclassed Template, then we would need to either
# 1) ask the user to change the backend, so all templates are of our subclass, or
# 2) copy the data from user's Template class instance to our subclass instance,
# which could lead to doubly parsing the source, and could be problematic if users
# used more exotic subclasses of Template.
#
# Instead, modifying only the `render` method of an already-existing instance
# should work well with any user-provided custom subclasses of Template, and it
# doesn't require the source to be parsed multiple times. User can pass extra args/kwargs,
# and can modify the rendering behavior by overriding the `_render` method.
#
# NOTE 2: Instead of setting `Template._is_component_nested`, alternatively we could
# have passed the value to `_monkeypatch_template` directly. However, we intentionally
# did NOT do that, so the monkey-patched method is more robust, and can be e.g. copied
# to other.
def _template_render(self: Template, context: Context, *args: Any, **kwargs: Any) -> str:
# ---------------- OUR CHANGES START ----------------
# We parametrized `isolated_context`, which was `True` in the original method.
if not hasattr(self, "_is_component_nested"):
isolated_context = True
else:
# MUST be `True` for templates that are NOT import with `{% extends %}` tag,
# and `False` otherwise.
isolated_context = not self._is_component_nested
# ---------------- OUR CHANGES END ----------------
with context.render_context.push_state(self, isolated_context=isolated_context):
if context.template is None:
with context.bind_template(self):
context.template_name = self.name
return self._render(context, *args, **kwargs)
else:
return self._render(context, *args, **kwargs)
# See https://stackoverflow.com/a/42154067/9788634
template.render = types.MethodType(_template_render, template)

View file

@ -0,0 +1,7 @@
{% extends "block_in_component.html" %}
{% load component_tags %}
{% block body %}
<div>
58 giraffes and 2 pantaloons
</div>
{% endblock %}

View file

@ -206,19 +206,7 @@ class BlockCompatTests(BaseTestCase):
""" """
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
# TODO: https://github.com/EmilStenstrom/django-components/issues/508 @parametrize_context_behavior(["django", "isolated"])
# Doesn't work in "django" mode. The problem is that, for some reason,
# the `context.render_context` inside Django's `BlockNode.render` has one
# extra layer pushed to the RenderContext's stack. But RenderContext checks only
# the latest layer, whereas the encountered {% block %} tags are defined in
# the second-last layer.
#
# The ideal solution would be figure out why there's the extra layer in "django" mode
# and not in "isolated", and fix "django" mode so inside `BlockNode.render`,
# the `context.render_context` has the same amount of layers as in the "isolated" mode.
#
# See https://github.com/django/django/blob/4.2/django/template/loader_tags.py
@parametrize_context_behavior(["isolated"])
def test_block_inside_component(self): def test_block_inside_component(self):
component.registry.register("slotted_component", SlottedComponent) component.registry.register("slotted_component", SlottedComponent)
@ -248,6 +236,35 @@ class BlockCompatTests(BaseTestCase):
""" """
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"])
def test_block_inside_component_parent(self):
component.registry.register("slotted_component", SlottedComponent)
@component.register("block_in_component_parent")
class BlockInCompParent(component.Component):
template_name = "block_in_component_parent.html"
template: types.django_html = """
{% load component_tags %}
{% component "block_in_component_parent" %}{% endcomponent %}
"""
rendered = Template(template).render(Context())
expected = """
<!DOCTYPE html>
<html lang="en">
<body>
<custom-template>
<header></header>
<main>
<div> 58 giraffes and 2 pantaloons </div>
</main>
<footer>Default footer</footer>
</custom-template>
</body>
</html>
"""
self.assertHTMLEqual(rendered, expected)
@parametrize_context_behavior(["django", "isolated"]) @parametrize_context_behavior(["django", "isolated"])
def test_block_does_not_affect_inside_component(self): def test_block_does_not_affect_inside_component(self):
""" """
@ -319,19 +336,7 @@ class BlockCompatTests(BaseTestCase):
""" """
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
# TODO: https://github.com/EmilStenstrom/django-components/issues/508 @parametrize_context_behavior(["django", "isolated"])
# Doesn't work in "django" mode. The problem is that, for some reason,
# the `context.render_context` inside Django's `BlockNode.render` has one
# extra layer pushed to the RenderContext's stack. But RenderContext checks only
# the latest layer, whereas the encountered {% block %} tags are defined in
# the second-last layer.
#
# The ideal solution would be figure out why there's the extra layer in "django" mode
# and not in "isolated", and fix "django" mode so inside `BlockNode.render`,
# the `context.render_context` has the same amount of layers as in the "isolated" mode.
#
# See https://github.com/django/django/blob/4.2/django/template/loader_tags.py
@parametrize_context_behavior(["isolated"])
def test_slot_inside_block__slot_default_block_override(self): def test_slot_inside_block__slot_default_block_override(self):
component.registry.clear() component.registry.clear()
component.registry.register("slotted_component", SlottedComponent) component.registry.register("slotted_component", SlottedComponent)
@ -403,19 +408,7 @@ class BlockCompatTests(BaseTestCase):
""" """
self.assertHTMLEqual(rendered, expected) self.assertHTMLEqual(rendered, expected)
# TODO: https://github.com/EmilStenstrom/django-components/issues/508 @parametrize_context_behavior(["django", "isolated"])
# Doesn't work in "django" mode. The problem is that, for some reason,
# the `context.render_context` inside Django's `BlockNode.render` has one
# extra layer pushed to the RenderContext's stack. But RenderContext checks only
# the latest layer, whereas the encountered {% block %} tags are defined in
# the second-last layer.
#
# The ideal solution would be figure out why there's the extra layer in "django" mode
# and not in "isolated", and fix "django" mode so inside `BlockNode.render`,
# the `context.render_context` has the same amount of layers as in the "isolated" mode.
#
# See https://github.com/django/django/blob/4.2/django/template/loader_tags.py
@parametrize_context_behavior(["isolated"])
def test_slot_inside_block__slot_overriden_block_overriden(self): def test_slot_inside_block__slot_overriden_block_overriden(self):
component.registry.register("slotted_component", SlottedComponent) component.registry.register("slotted_component", SlottedComponent)