mirror of
https://github.com/django-components/django-components.git
synced 2025-10-07 12:40:19 +00:00
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:
parent
0101f6dae6
commit
b8ff610a48
3 changed files with 92 additions and 39 deletions
|
@ -1,6 +1,7 @@
|
|||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
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.exceptions import TemplateSyntaxError
|
||||
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.safestring import SafeString, mark_safe
|
||||
from django.views import View
|
||||
|
@ -285,7 +287,14 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
# See https://github.com/EmilStenstrom/django-components/issues/414
|
||||
context = context if isinstance(context, Context) else Context(context)
|
||||
prepare_context(context, self.component_id)
|
||||
|
||||
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
|
||||
if slots_data:
|
||||
|
@ -445,3 +454,47 @@ class ComponentNode(Node):
|
|||
|
||||
trace_msg("RENDR", "COMP", self.name_fexp, self.component_id, "...Done!")
|
||||
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)
|
||||
|
|
7
tests/templates/block_in_component_parent.html
Normal file
7
tests/templates/block_in_component_parent.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends "block_in_component.html" %}
|
||||
{% load component_tags %}
|
||||
{% block body %}
|
||||
<div>
|
||||
58 giraffes and 2 pantaloons
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -206,19 +206,7 @@ class BlockCompatTests(BaseTestCase):
|
|||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
# TODO: https://github.com/EmilStenstrom/django-components/issues/508
|
||||
# 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"])
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_block_inside_component(self):
|
||||
component.registry.register("slotted_component", SlottedComponent)
|
||||
|
||||
|
@ -248,6 +236,35 @@ class BlockCompatTests(BaseTestCase):
|
|||
"""
|
||||
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"])
|
||||
def test_block_does_not_affect_inside_component(self):
|
||||
"""
|
||||
|
@ -319,19 +336,7 @@ class BlockCompatTests(BaseTestCase):
|
|||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
# TODO: https://github.com/EmilStenstrom/django-components/issues/508
|
||||
# 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"])
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot_inside_block__slot_default_block_override(self):
|
||||
component.registry.clear()
|
||||
component.registry.register("slotted_component", SlottedComponent)
|
||||
|
@ -403,19 +408,7 @@ class BlockCompatTests(BaseTestCase):
|
|||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
# TODO: https://github.com/EmilStenstrom/django-components/issues/508
|
||||
# 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"])
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot_inside_block__slot_overriden_block_overriden(self):
|
||||
component.registry.register("slotted_component", SlottedComponent)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue