mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
feat: on_render (#1231)
* feat: on_render * docs: fix typos * refactor: fix linter errors * refactor: make `error` in on_render_after optional to fix benchmarks * refactor: benchmark attempt 2 * refactor: fix linter errors * refactor: fix formatting
This commit is contained in:
parent
46e524e37d
commit
eceebb9696
24 changed files with 1793 additions and 417 deletions
|
@ -4477,7 +4477,7 @@ class Tabs(Component):
|
|||
"tabs_data": {"name": name},
|
||||
}
|
||||
|
||||
def on_render_after(self, context, template, rendered) -> str:
|
||||
def on_render_after(self, context, template, rendered, error=None) -> str:
|
||||
# By the time we get here, all child TabItem components should have been
|
||||
# rendered, and they should've populated the tabs list.
|
||||
tabs: List[TabEntry] = context["tabs"]
|
||||
|
@ -4530,7 +4530,7 @@ class TabItem(Component):
|
|||
"disabled": disabled,
|
||||
}
|
||||
|
||||
def on_render_after(self, context, template, content) -> None:
|
||||
def on_render_after(self, context, template, content, error=None) -> None:
|
||||
parent_tabs: List[dict] = context["parent_tabs"]
|
||||
parent_tabs.append({
|
||||
"header": context["header"],
|
||||
|
|
|
@ -5,7 +5,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
|||
|
||||
import os
|
||||
import re
|
||||
from typing import Any, NamedTuple
|
||||
from typing import Any, List, Literal, NamedTuple, Optional
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
@ -25,6 +25,7 @@ from django_components import (
|
|||
all_components,
|
||||
get_component_by_class_id,
|
||||
register,
|
||||
registry,
|
||||
types,
|
||||
)
|
||||
from django_components.template import _get_component_template
|
||||
|
@ -1429,260 +1430,555 @@ class TestComponentRender:
|
|||
|
||||
@djc_test
|
||||
class TestComponentHook:
|
||||
def test_on_render_before(self):
|
||||
@register("nested")
|
||||
class NestedComponent(Component):
|
||||
def _gen_slotted_component(self, calls: List[str]):
|
||||
class Slotted(Component):
|
||||
template = "Hello from slotted"
|
||||
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
calls.append("slotted__on_render_before")
|
||||
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
calls.append("slotted__on_render_pre")
|
||||
html, error = yield template.render(context) # type: ignore[union-attr]
|
||||
|
||||
calls.append("slotted__on_render_post")
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
) -> None:
|
||||
calls.append("slotted__on_render_after")
|
||||
|
||||
return Slotted
|
||||
|
||||
def _gen_inner_component(self, calls: List[str]):
|
||||
class Inner(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Hello from nested
|
||||
<div>
|
||||
{% slot "content" default / %}
|
||||
</div>
|
||||
Inner start
|
||||
{% slot "content" default / %}
|
||||
Inner end
|
||||
"""
|
||||
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
calls.append("inner__on_render_before")
|
||||
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
calls.append("inner__on_render_pre")
|
||||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
html, error = yield template.render(context)
|
||||
|
||||
calls.append("inner__on_render_post")
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
) -> None:
|
||||
calls.append("inner__on_render_after")
|
||||
|
||||
return Inner
|
||||
|
||||
def _gen_middle_component(self, calls: List[str]):
|
||||
class Middle(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Middle start
|
||||
{% component "inner" %}
|
||||
{% component "slotted" / %}
|
||||
{% endcomponent %}
|
||||
Middle text
|
||||
{% component "inner" / %}
|
||||
Middle end
|
||||
"""
|
||||
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
calls.append("middle__on_render_before")
|
||||
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
calls.append("middle__on_render_pre")
|
||||
html, error = yield template.render(context) # type: ignore[union-attr]
|
||||
|
||||
calls.append("middle__on_render_post")
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
) -> None:
|
||||
calls.append("middle__on_render_after")
|
||||
|
||||
return Middle
|
||||
|
||||
def _gen_outer_component(self, calls: List[str]):
|
||||
class Outer(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Outer start
|
||||
{% component "middle" / %}
|
||||
Outer text
|
||||
{% component "middle" / %}
|
||||
Outer end
|
||||
"""
|
||||
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
calls.append("outer__on_render_before")
|
||||
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
calls.append("outer__on_render_pre")
|
||||
html, error = yield template.render(context) # type: ignore[union-attr]
|
||||
|
||||
calls.append("outer__on_render_post")
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
) -> None:
|
||||
calls.append("outer__on_render_after")
|
||||
|
||||
return Outer
|
||||
|
||||
def _gen_broken_component(self):
|
||||
class BrokenComponent(Component):
|
||||
def on_render(self, context: Context, template: Template):
|
||||
raise ValueError("BROKEN")
|
||||
|
||||
return BrokenComponent
|
||||
|
||||
def test_order(self):
|
||||
calls: List[str] = []
|
||||
|
||||
registry.register("slotted", self._gen_slotted_component(calls))
|
||||
registry.register("inner", self._gen_inner_component(calls))
|
||||
registry.register("middle", self._gen_middle_component(calls))
|
||||
Outer = self._gen_outer_component(calls)
|
||||
|
||||
result = Outer.render()
|
||||
|
||||
assertHTMLEqual(
|
||||
result,
|
||||
"""
|
||||
Outer start
|
||||
Middle start
|
||||
Inner start
|
||||
Hello from slotted
|
||||
Inner end
|
||||
Middle text
|
||||
Inner start
|
||||
Inner end
|
||||
Middle end
|
||||
Outer text
|
||||
Middle start
|
||||
Inner start
|
||||
Hello from slotted
|
||||
Inner end
|
||||
Middle text
|
||||
Inner start
|
||||
Inner end
|
||||
Middle end
|
||||
Outer end
|
||||
""",
|
||||
)
|
||||
|
||||
assert calls == [
|
||||
"outer__on_render_before",
|
||||
"outer__on_render_pre",
|
||||
"middle__on_render_before",
|
||||
"middle__on_render_pre",
|
||||
"inner__on_render_before",
|
||||
"inner__on_render_pre",
|
||||
"slotted__on_render_before",
|
||||
"slotted__on_render_pre",
|
||||
"slotted__on_render_post",
|
||||
"slotted__on_render_after",
|
||||
"inner__on_render_post",
|
||||
"inner__on_render_after",
|
||||
"inner__on_render_before",
|
||||
"inner__on_render_pre",
|
||||
"inner__on_render_post",
|
||||
"inner__on_render_after",
|
||||
"middle__on_render_post",
|
||||
"middle__on_render_after",
|
||||
"middle__on_render_before",
|
||||
"middle__on_render_pre",
|
||||
"inner__on_render_before",
|
||||
"inner__on_render_pre",
|
||||
"slotted__on_render_before",
|
||||
"slotted__on_render_pre",
|
||||
"slotted__on_render_post",
|
||||
"slotted__on_render_after",
|
||||
"inner__on_render_post",
|
||||
"inner__on_render_after",
|
||||
"inner__on_render_before",
|
||||
"inner__on_render_pre",
|
||||
"inner__on_render_post",
|
||||
"inner__on_render_after",
|
||||
"middle__on_render_post",
|
||||
"middle__on_render_after",
|
||||
"outer__on_render_post",
|
||||
"outer__on_render_after",
|
||||
]
|
||||
|
||||
def test_context(self):
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
args: {{ args|safe }}
|
||||
kwargs: {{ kwargs|safe }}
|
||||
---
|
||||
from_on_before: {{ from_on_before }}
|
||||
---
|
||||
{% component "nested" %}
|
||||
Hello from simple
|
||||
{% endcomponent %}
|
||||
from_on_before__edited1: {{ from_on_before__edited1 }}
|
||||
from_on_before__edited2: {{ from_on_before__edited2 }}
|
||||
from_on_render_pre: {{ from_on_render_pre }}
|
||||
from_on_render_post: {{ from_on_render_post }}
|
||||
from_on_render_pre__edited2: {{ from_on_render_pre__edited2 }}
|
||||
from_on_render_post__edited2: {{ from_on_render_post__edited2 }}
|
||||
from_on_after: {{ from_on_after }}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_before"] = ":)"
|
||||
context["from_on_before"] = "1"
|
||||
|
||||
def on_render(self, context: Context, template: Template):
|
||||
context["from_on_render_pre"] = "2"
|
||||
# Check we can modify entries set by other methods
|
||||
context["from_on_before__edited1"] = context["from_on_before"] + " (on_render)"
|
||||
|
||||
html, error = yield template.render(context)
|
||||
|
||||
context["from_on_render_post"] = "3"
|
||||
|
||||
# NOTE: Since this is called AFTER the render, the values set here should NOT
|
||||
# make it to the rendered output.
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Template,
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
) -> None:
|
||||
context["from_on_after"] = "4"
|
||||
# Check we can modify entries set by other methods
|
||||
# NOTE: These also check that the previous values are available
|
||||
context["from_on_before__edited2"] = context["from_on_before"] + " (on_render_after)"
|
||||
context["from_on_render_pre__edited2"] = context["from_on_render_pre"] + " (on_render_after)"
|
||||
context["from_on_render_post__edited2"] = context["from_on_render_post"] + " (on_render_after)"
|
||||
|
||||
rendered = SimpleComponent.render()
|
||||
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
from_on_before: 1
|
||||
from_on_before__edited1: 1 (on_render)
|
||||
from_on_before__edited2:
|
||||
from_on_render_pre: 2
|
||||
from_on_render_post:
|
||||
from_on_render_pre__edited2:
|
||||
from_on_render_post__edited2:
|
||||
from_on_after:
|
||||
""",
|
||||
)
|
||||
|
||||
def test_template(self):
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
text
|
||||
"""
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
# Insert text into the Template
|
||||
#
|
||||
# NOTE: Users should NOT do this, because this will insert the text every time
|
||||
# the component is rendered.
|
||||
template.nodelist.append(TextNode("\n---\nFROM_ON_BEFORE"))
|
||||
|
||||
rendered = SimpleComponent.render()
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
args: []
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_before: :)
|
||||
---
|
||||
Hello from nested
|
||||
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
|
||||
Hello from simple
|
||||
</div>
|
||||
---
|
||||
FROM_ON_BEFORE
|
||||
""",
|
||||
)
|
||||
def on_render(self, context: Context, template: Template):
|
||||
template.nodelist.append(TextNode("\n---\nFROM_ON_RENDER_PRE"))
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
def test_on_render_after(self):
|
||||
captured_content = None
|
||||
html, error = yield template.render(context)
|
||||
|
||||
@register("nested")
|
||||
class NestedComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Hello from nested
|
||||
<div>
|
||||
{% slot "content" default / %}
|
||||
</div>
|
||||
"""
|
||||
template.nodelist.append(TextNode("\n---\nFROM_ON_RENDER_POST"))
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
args: {{ args|safe }}
|
||||
kwargs: {{ kwargs|safe }}
|
||||
---
|
||||
from_on_after: {{ from_on_after }}
|
||||
---
|
||||
{% component "nested" %}
|
||||
Hello from simple
|
||||
{% endcomponent %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
def on_render_after(self, context: Context, template: Template, content: str) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_after"] = ":)"
|
||||
|
||||
# Insert text into the Template
|
||||
# NOTE: Since this is called AFTER the render, the values set here should NOT
|
||||
# make it to the rendered output.
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Template,
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
) -> None:
|
||||
template.nodelist.append(TextNode("\n---\nFROM_ON_AFTER"))
|
||||
|
||||
nonlocal captured_content
|
||||
captured_content = content
|
||||
|
||||
rendered = SimpleComponent.render()
|
||||
|
||||
assertHTMLEqual(
|
||||
captured_content,
|
||||
"""
|
||||
args: []
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_after:
|
||||
---
|
||||
Hello from nested
|
||||
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
|
||||
Hello from simple
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
args: []
|
||||
kwargs: {}
|
||||
text
|
||||
---
|
||||
from_on_after:
|
||||
FROM_ON_BEFORE
|
||||
---
|
||||
Hello from nested
|
||||
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
|
||||
Hello from simple
|
||||
</div>
|
||||
FROM_ON_RENDER_PRE
|
||||
""",
|
||||
)
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_on_render_after_override_output(self, components_settings):
|
||||
captured_content = None
|
||||
|
||||
@register("nested")
|
||||
class NestedComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Hello from nested
|
||||
<div>
|
||||
{% slot "content" default / %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def test_on_render_no_yield(self):
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
args: {{ args|safe }}
|
||||
kwargs: {{ kwargs|safe }}
|
||||
---
|
||||
from_on_before: {{ from_on_before }}
|
||||
---
|
||||
{% component "nested" %}
|
||||
Hello from simple
|
||||
{% endcomponent %}
|
||||
text
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
def on_render_after(self, context: Context, template: Template, content: str) -> str:
|
||||
nonlocal captured_content
|
||||
captured_content = content
|
||||
|
||||
return "Chocolate cookie recipe: " + content
|
||||
def on_render(self, context: Context, template: Template):
|
||||
return "OVERRIDDEN"
|
||||
|
||||
rendered = SimpleComponent.render()
|
||||
assert rendered == "OVERRIDDEN"
|
||||
|
||||
assertHTMLEqual(
|
||||
captured_content,
|
||||
"""
|
||||
args: []
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_before:
|
||||
---
|
||||
Hello from nested
|
||||
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
|
||||
Hello from simple
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
Chocolate cookie recipe:
|
||||
args: []
|
||||
kwargs: {}
|
||||
---
|
||||
from_on_before:
|
||||
---
|
||||
Hello from nested
|
||||
<div data-djc-id-ca1bc3e data-djc-id-ca1bc40>
|
||||
Hello from simple
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
def test_on_render_before_after_same_context(self):
|
||||
context_in_before = None
|
||||
context_in_after = None
|
||||
|
||||
@register("nested")
|
||||
class NestedComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Hello from nested
|
||||
<div>
|
||||
{% slot "content" default / %}
|
||||
</div>
|
||||
"""
|
||||
def test_on_render_reraise_error(self):
|
||||
registry.register("broken", self._gen_broken_component())
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
args: {{ args|safe }}
|
||||
kwargs: {{ kwargs|safe }}
|
||||
---
|
||||
from_on_after: {{ from_on_after }}
|
||||
---
|
||||
{% component "nested" %}
|
||||
Hello from simple
|
||||
{% endcomponent %}
|
||||
{% component "broken" / %}
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
def on_render(self, context: Context, template: Template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
context["from_on_before"] = ":)"
|
||||
nonlocal context_in_before
|
||||
context_in_before = context
|
||||
raise error from None # Re-raise original error
|
||||
|
||||
# Check that modifying the context or template does nothing
|
||||
def on_render_after(self, context: Context, template: Template, html: str) -> None:
|
||||
context["from_on_after"] = ":)"
|
||||
nonlocal context_in_after
|
||||
context_in_after = context
|
||||
with pytest.raises(ValueError, match=re.escape("BROKEN")):
|
||||
SimpleComponent.render()
|
||||
|
||||
SimpleComponent.render()
|
||||
@djc_test(
|
||||
parametrize=(
|
||||
["template", "action", "method"],
|
||||
[
|
||||
["simple", "return_none", "on_render"],
|
||||
["broken", "return_none", "on_render"],
|
||||
[None, "return_none", "on_render"],
|
||||
|
||||
assert context_in_before == context_in_after
|
||||
assert "from_on_before" in context_in_before # type: ignore[operator]
|
||||
assert "from_on_after" in context_in_after # type: ignore[operator]
|
||||
["simple", "return_none", "on_render_after"],
|
||||
["broken", "return_none", "on_render_after"],
|
||||
[None, "return_none", "on_render_after"],
|
||||
|
||||
["simple", "no_return", "on_render"],
|
||||
["broken", "no_return", "on_render"],
|
||||
[None, "no_return", "on_render"],
|
||||
|
||||
["simple", "no_return", "on_render_after"],
|
||||
["broken", "no_return", "on_render_after"],
|
||||
[None, "no_return", "on_render_after"],
|
||||
|
||||
["simple", "raise_error", "on_render"],
|
||||
["broken", "raise_error", "on_render"],
|
||||
[None, "raise_error", "on_render"],
|
||||
|
||||
["simple", "raise_error", "on_render_after"],
|
||||
["broken", "raise_error", "on_render_after"],
|
||||
[None, "raise_error", "on_render_after"],
|
||||
|
||||
["simple", "return_html", "on_render"],
|
||||
["broken", "return_html", "on_render"],
|
||||
[None, "return_html", "on_render"],
|
||||
|
||||
["simple", "return_html", "on_render_after"],
|
||||
["broken", "return_html", "on_render_after"],
|
||||
[None, "return_html", "on_render_after"],
|
||||
],
|
||||
None
|
||||
)
|
||||
)
|
||||
def test_result_interception(
|
||||
self,
|
||||
template: Literal["simple", "broken", None],
|
||||
action: Literal["return_none", "no_return", "raise_error", "return_html"],
|
||||
method: Literal["on_render", "on_render_after"],
|
||||
):
|
||||
calls: List[str] = []
|
||||
|
||||
Broken = self._gen_broken_component()
|
||||
Slotted = self._gen_slotted_component(calls)
|
||||
Inner = self._gen_inner_component(calls)
|
||||
Middle = self._gen_middle_component(calls)
|
||||
Outer = self._gen_outer_component(calls)
|
||||
|
||||
# Make modifications to the components based on the parameters
|
||||
|
||||
# Set template
|
||||
if template is None:
|
||||
class Inner(Inner): # type: ignore
|
||||
template = None
|
||||
|
||||
elif template == "broken":
|
||||
class Inner(Inner): # type: ignore
|
||||
template = "{% component 'broken' / %}"
|
||||
|
||||
elif template == "simple":
|
||||
pass
|
||||
|
||||
# Set `on_render` behavior
|
||||
if method == "on_render":
|
||||
if action == "return_none":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
html, error = yield template.render(context)
|
||||
return None
|
||||
|
||||
elif action == "no_return":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
html, error = yield template.render(context)
|
||||
|
||||
elif action == "raise_error":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
html, error = yield template.render(context)
|
||||
raise ValueError("ERROR_FROM_ON_RENDER")
|
||||
|
||||
elif action == "return_html":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
html, error = yield template.render(context)
|
||||
return "HTML_FROM_ON_RENDER"
|
||||
else:
|
||||
raise pytest.fail(f"Unknown action: {action}")
|
||||
|
||||
# Set `on_render_after` behavior
|
||||
elif method == "on_render_after":
|
||||
if action == "return_none":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render_after(self, context: Context, template: Template, html: Optional[str], error: Optional[Exception]): # noqa: E501
|
||||
return None
|
||||
|
||||
elif action == "no_return":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render_after(self, context: Context, template: Template, html: Optional[str], error: Optional[Exception]): # noqa: E501
|
||||
pass
|
||||
|
||||
elif action == "raise_error":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render_after(self, context: Context, template: Template, html: Optional[str], error: Optional[Exception]): # noqa: E501
|
||||
raise ValueError("ERROR_FROM_ON_RENDER")
|
||||
|
||||
elif action == "return_html":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render_after(self, context: Context, template: Template, html: Optional[str], error: Optional[Exception]): # noqa: E501
|
||||
return "HTML_FROM_ON_RENDER"
|
||||
else:
|
||||
raise pytest.fail(f"Unknown action: {action}")
|
||||
else:
|
||||
raise pytest.fail(f"Unknown method: {method}")
|
||||
|
||||
registry.register("broken", Broken)
|
||||
registry.register("slotted", Slotted)
|
||||
registry.register("inner", Inner)
|
||||
registry.register("middle", Middle)
|
||||
registry.register("outer", Outer)
|
||||
|
||||
def _gen_expected_output(inner1: str, inner2: str):
|
||||
return f"""
|
||||
Outer start
|
||||
Middle start
|
||||
{inner1}
|
||||
Middle text
|
||||
{inner2}
|
||||
Middle end
|
||||
Outer text
|
||||
Middle start
|
||||
{inner1}
|
||||
Middle text
|
||||
{inner2}
|
||||
Middle end
|
||||
Outer end
|
||||
"""
|
||||
|
||||
# Assert based on the behavior
|
||||
if template is None:
|
||||
# Overriden HTML
|
||||
if action == "return_html":
|
||||
expected = _gen_expected_output(inner1="HTML_FROM_ON_RENDER", inner2="HTML_FROM_ON_RENDER")
|
||||
result = Outer.render()
|
||||
assertHTMLEqual(result, expected)
|
||||
# Overriden error
|
||||
elif action == "raise_error":
|
||||
with pytest.raises(ValueError, match="ERROR_FROM_ON_RENDER"):
|
||||
Outer.render()
|
||||
# Original output
|
||||
elif action in ["return_none", "no_return"]:
|
||||
expected = _gen_expected_output(inner1="", inner2="")
|
||||
result = Outer.render()
|
||||
assertHTMLEqual(result, expected)
|
||||
else:
|
||||
raise pytest.fail(f"Unknown action: {action}")
|
||||
|
||||
elif template == "simple":
|
||||
# Overriden HTML
|
||||
if action == "return_html":
|
||||
expected = _gen_expected_output(inner1="HTML_FROM_ON_RENDER", inner2="HTML_FROM_ON_RENDER")
|
||||
result = Outer.render()
|
||||
assertHTMLEqual(result, expected)
|
||||
# Overriden error
|
||||
elif action == "raise_error":
|
||||
with pytest.raises(ValueError, match="ERROR_FROM_ON_RENDER"):
|
||||
Outer.render()
|
||||
# Original output
|
||||
elif action in ["return_none", "no_return"]:
|
||||
expected = _gen_expected_output(
|
||||
inner1="Inner start Hello from slotted Inner end",
|
||||
inner2="Inner start Inner end",
|
||||
)
|
||||
result = Outer.render()
|
||||
assertHTMLEqual(result, expected)
|
||||
else:
|
||||
raise pytest.fail(f"Unknown action: {action}")
|
||||
|
||||
elif template == "broken":
|
||||
# Overriden HTML
|
||||
if action == "return_html":
|
||||
expected = _gen_expected_output(inner1="HTML_FROM_ON_RENDER", inner2="HTML_FROM_ON_RENDER")
|
||||
result = Outer.render()
|
||||
assertHTMLEqual(result, expected)
|
||||
# Overriden error
|
||||
elif action == "raise_error":
|
||||
with pytest.raises(ValueError, match="ERROR_FROM_ON_RENDER"):
|
||||
Outer.render()
|
||||
# Original output
|
||||
elif action in ["return_none", "no_return"]:
|
||||
with pytest.raises(ValueError, match="broken"):
|
||||
Outer.render()
|
||||
else:
|
||||
raise pytest.fail(f"Unknown action: {action}")
|
||||
|
||||
else:
|
||||
raise pytest.fail(f"Unknown template: {template}")
|
||||
|
||||
|
||||
@djc_test
|
||||
|
|
|
@ -1218,11 +1218,17 @@ class TestContextVarsIsFilled:
|
|||
|
||||
@register("is_filled_vars")
|
||||
class IsFilledVarsComponent(self.IsFilledVarsComponent): # type: ignore[name-defined]
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
nonlocal captured_before
|
||||
captured_before = self.is_filled.copy()
|
||||
|
||||
def on_render_after(self, context: Context, template: Template, content: str) -> None:
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
content: Optional[str],
|
||||
error: Optional[Exception],
|
||||
) -> None:
|
||||
nonlocal captured_after
|
||||
captured_after = self.is_filled.copy()
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ from django_components.extension import (
|
|||
OnComponentUnregisteredContext,
|
||||
OnComponentInputContext,
|
||||
OnComponentDataContext,
|
||||
OnComponentRenderedContext,
|
||||
OnSlotRenderedContext,
|
||||
)
|
||||
from django_components.extensions.cache import CacheExtension
|
||||
|
@ -82,6 +83,7 @@ class DummyExtension(ComponentExtension):
|
|||
"on_component_unregistered": [],
|
||||
"on_component_input": [],
|
||||
"on_component_data": [],
|
||||
"on_component_rendered": [],
|
||||
"on_slot_rendered": [],
|
||||
}
|
||||
|
||||
|
@ -118,6 +120,9 @@ class DummyExtension(ComponentExtension):
|
|||
def on_component_data(self, ctx: OnComponentDataContext) -> None:
|
||||
self.calls["on_component_data"].append(ctx)
|
||||
|
||||
def on_component_rendered(self, ctx: OnComponentRenderedContext) -> None:
|
||||
self.calls["on_component_rendered"].append(ctx)
|
||||
|
||||
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> None:
|
||||
self.calls["on_slot_rendered"].append(ctx)
|
||||
|
||||
|
@ -147,6 +152,20 @@ class SlotOverrideExtension(ComponentExtension):
|
|||
return "OVERRIDEN BY EXTENSION"
|
||||
|
||||
|
||||
class ErrorOnComponentRenderedExtension(ComponentExtension):
|
||||
name = "error_on_component_rendered"
|
||||
|
||||
def on_component_rendered(self, ctx: OnComponentRenderedContext):
|
||||
raise RuntimeError("Custom error from extension")
|
||||
|
||||
|
||||
class ReturnHtmlOnComponentRenderedExtension(ComponentExtension):
|
||||
name = "return_html_on_component_rendered"
|
||||
|
||||
def on_component_rendered(self, ctx: OnComponentRenderedContext):
|
||||
return f"<div>OVERRIDDEN: {ctx.result}</div>"
|
||||
|
||||
|
||||
def with_component_cls(on_created: Callable):
|
||||
class TempComponent(Component):
|
||||
template = "Hello {{ name }}!"
|
||||
|
@ -340,6 +359,45 @@ class TestExtensionHooks:
|
|||
assert data_call.js_data == {"script": "console.log('Hello!')"}
|
||||
assert data_call.css_data == {"style": "body { color: blue; }"}
|
||||
|
||||
# Verify on_component_rendered was called with correct args
|
||||
assert len(extension.calls["on_component_rendered"]) == 1
|
||||
rendered_call: OnComponentRenderedContext = extension.calls["on_component_rendered"][0]
|
||||
assert rendered_call.component_cls == TestComponent
|
||||
assert isinstance(rendered_call.component, TestComponent)
|
||||
assert isinstance(rendered_call.component_id, str)
|
||||
assert rendered_call.result == "<!-- _RENDERED TestComponent_f4a4f0,ca1bc3e,, -->Hello Test!"
|
||||
assert rendered_call.error is None
|
||||
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_component_render_hooks__error(self):
|
||||
@register("test_comp")
|
||||
class TestComponent(Component):
|
||||
template = "Hello {{ name }}!"
|
||||
|
||||
def on_render_after(self, context, template, result, error):
|
||||
raise Exception("Oopsie woopsie")
|
||||
|
||||
with pytest.raises(Exception, match="Oopsie woopsie"):
|
||||
# Render the component with some args and kwargs
|
||||
TestComponent.render(
|
||||
context=Context({"foo": "bar"}),
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "Test"},
|
||||
slots={"content": "Some content"},
|
||||
)
|
||||
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||
|
||||
# Verify on_component_rendered was called with correct args
|
||||
assert len(extension.calls["on_component_rendered"]) == 1
|
||||
rendered_call: OnComponentRenderedContext = extension.calls["on_component_rendered"][0]
|
||||
assert rendered_call.component_cls == TestComponent
|
||||
assert isinstance(rendered_call.component, TestComponent)
|
||||
assert isinstance(rendered_call.component_id, str)
|
||||
assert rendered_call.result is None
|
||||
assert isinstance(rendered_call.error, Exception)
|
||||
assert str(rendered_call.error) == "An error occured while rendering components TestComponent:\nOopsie woopsie"
|
||||
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_on_slot_rendered(self):
|
||||
@register("test_comp")
|
||||
|
@ -387,6 +445,30 @@ class TestExtensionHooks:
|
|||
|
||||
assert rendered == "Hello OVERRIDEN BY EXTENSION!"
|
||||
|
||||
@djc_test(components_settings={"extensions": [ErrorOnComponentRenderedExtension]})
|
||||
def test_on_component_rendered__error_from_extension(self):
|
||||
@register("test_comp_error_ext")
|
||||
class TestComponent(Component):
|
||||
template = "Hello {{ name }}!"
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"name": kwargs.get("name", "World")}
|
||||
|
||||
with pytest.raises(RuntimeError, match="Custom error from extension"):
|
||||
TestComponent.render(args=(), kwargs={"name": "Test"})
|
||||
|
||||
@djc_test(components_settings={"extensions": [ReturnHtmlOnComponentRenderedExtension]})
|
||||
def test_on_component_rendered__return_html_from_extension(self):
|
||||
@register("test_comp_html_ext")
|
||||
class TestComponent(Component):
|
||||
template = "Hello {{ name }}!"
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"name": kwargs.get("name", "World")}
|
||||
|
||||
rendered = TestComponent.render(args=(), kwargs={"name": "Test"})
|
||||
assert rendered == "<div>OVERRIDDEN: Hello Test!</div>"
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestExtensionViews:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue