mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
feat: on_xx_loaded extension hooks (#1242)
Some checks failed
Run tests / test_sampleproject (3.13) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.13) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.8) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.9) (push) Has been cancelled
Run tests / build (windows-latest, 3.10) (push) Has been cancelled
Run tests / build (windows-latest, 3.11) (push) Has been cancelled
Run tests / build (windows-latest, 3.12) (push) Has been cancelled
Run tests / build (windows-latest, 3.13) (push) Has been cancelled
Run tests / build (windows-latest, 3.8) (push) Has been cancelled
Run tests / build (windows-latest, 3.9) (push) Has been cancelled
Run tests / test_docs (3.13) (push) Has been cancelled
Docs - build & deploy / docs (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.10) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.11) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.12) (push) Has been cancelled
Some checks failed
Run tests / test_sampleproject (3.13) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.13) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.8) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.9) (push) Has been cancelled
Run tests / build (windows-latest, 3.10) (push) Has been cancelled
Run tests / build (windows-latest, 3.11) (push) Has been cancelled
Run tests / build (windows-latest, 3.12) (push) Has been cancelled
Run tests / build (windows-latest, 3.13) (push) Has been cancelled
Run tests / build (windows-latest, 3.8) (push) Has been cancelled
Run tests / build (windows-latest, 3.9) (push) Has been cancelled
Run tests / test_docs (3.13) (push) Has been cancelled
Docs - build & deploy / docs (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.10) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.11) (push) Has been cancelled
Run tests / build (ubuntu-latest, 3.12) (push) Has been cancelled
* feat: on_xx_loaded extension hooks * refactor: fix tests
This commit is contained in:
parent
efe5eb0ba5
commit
09bcf8dbcc
11 changed files with 720 additions and 113 deletions
|
@ -46,7 +46,7 @@ class TestMainMedia:
|
|||
rendered = TestComponent.render()
|
||||
|
||||
assertInHTML(
|
||||
'<div class="html-css-only" data-djc-id-ca1bc3e>Content</div>',
|
||||
'<div class="html-css-only" data-djc-id-ca1bc40>Content</div>',
|
||||
rendered,
|
||||
)
|
||||
assertInHTML(
|
||||
|
@ -70,6 +70,9 @@ class TestMainMedia:
|
|||
assert TestComponent.css == ".html-css-only { color: blue; }"
|
||||
assert TestComponent.js == "console.log('HTML and JS only');"
|
||||
|
||||
assert isinstance(TestComponent._template, Template)
|
||||
assert TestComponent._template.origin.component_cls is TestComponent
|
||||
|
||||
@djc_test(
|
||||
django_settings={
|
||||
"STATICFILES_DIRS": [
|
||||
|
@ -127,6 +130,9 @@ class TestMainMedia:
|
|||
assert TestComponent.css == ".html-css-only {\n color: blue;\n}\n"
|
||||
assert TestComponent.js == 'console.log("JS file");\n'
|
||||
|
||||
assert isinstance(TestComponent._template, Template)
|
||||
assert TestComponent._template.origin.component_cls is TestComponent
|
||||
|
||||
@djc_test(
|
||||
django_settings={
|
||||
"STATICFILES_DIRS": [
|
||||
|
@ -151,6 +157,9 @@ class TestMainMedia:
|
|||
assert ".html-css-only {\n color: blue;\n}" in TestComponent.css # type: ignore[operator]
|
||||
assert 'console.log("HTML and JS only");' in TestComponent.js # type: ignore[operator]
|
||||
|
||||
assert isinstance(TestComponent._template, Template)
|
||||
assert TestComponent._template.origin.component_cls is TestComponent
|
||||
|
||||
rendered = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
|
@ -199,6 +208,7 @@ class TestMainMedia:
|
|||
# the corresponding ComponentMedia instance is also on the parent class.
|
||||
assert AppLvlCompComponent._component_media.css is UNSET # type: ignore[attr-defined]
|
||||
assert AppLvlCompComponent._component_media.css_file == "app_lvl_comp.css" # type: ignore[attr-defined]
|
||||
assert AppLvlCompComponent._component_media._template is UNSET # type: ignore[attr-defined]
|
||||
|
||||
# Access the property to load the CSS
|
||||
_ = TestComponent.css
|
||||
|
@ -218,6 +228,9 @@ class TestMainMedia:
|
|||
assert AppLvlCompComponent._component_media.js == 'console.log("JS file");\n' # type: ignore[attr-defined]
|
||||
assert AppLvlCompComponent._component_media.js_file == "app_lvl_comp/app_lvl_comp.js" # type: ignore[attr-defined]
|
||||
|
||||
assert isinstance(AppLvlCompComponent._component_media._template, Template) # type: ignore[attr-defined]
|
||||
assert AppLvlCompComponent._component_media._template.origin.component_cls is AppLvlCompComponent # type: ignore[attr-defined]
|
||||
|
||||
def test_html_variable_filtered(self):
|
||||
class FilteredComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -1037,73 +1050,121 @@ class TestSubclassingAttributes:
|
|||
class TestComp(Component):
|
||||
js = None
|
||||
js_file = None
|
||||
template = None
|
||||
template_file = None
|
||||
|
||||
assert TestComp.js is None
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template is None
|
||||
assert TestComp.template_file is None
|
||||
|
||||
def test_mixing_none_and_non_none_raises(self):
|
||||
with pytest.raises(
|
||||
ImproperlyConfigured,
|
||||
match=re.escape("Received non-empty value from both 'js' and 'js_file' in Component TestComp"),
|
||||
match=re.escape("Received non-empty value from both 'template' and 'template_file' in Component TestComp"),
|
||||
):
|
||||
|
||||
class TestComp(Component):
|
||||
js = "console.log('hi')"
|
||||
js_file = None
|
||||
template = "<h1>hi</h1>"
|
||||
template_file = None
|
||||
|
||||
def test_both_non_none_raises(self):
|
||||
with pytest.raises(
|
||||
ImproperlyConfigured,
|
||||
match=re.escape("Received non-empty value from both 'js' and 'js_file' in Component TestComp"),
|
||||
match=re.escape("Received non-empty value from both 'template' and 'template_file' in Component TestComp"),
|
||||
):
|
||||
|
||||
class TestComp(Component):
|
||||
js = "console.log('hi')"
|
||||
js_file = "file.js"
|
||||
template = "<h1>hi</h1>"
|
||||
template_file = "file.html"
|
||||
|
||||
def test_parent_non_null_child_non_null(self):
|
||||
class ParentComp(Component):
|
||||
js = "console.log('parent')"
|
||||
template = "<h1>parent</h1>"
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = "console.log('child')"
|
||||
template = "<h1>child</h1>"
|
||||
|
||||
assert TestComp.js == "console.log('child')"
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template == "<h1>child</h1>"
|
||||
assert TestComp.template_file is None
|
||||
|
||||
assert isinstance(ParentComp._template, Template)
|
||||
assert ParentComp._template.source == "<h1>parent</h1>"
|
||||
assert ParentComp._template.origin.component_cls == ParentComp
|
||||
|
||||
assert isinstance(TestComp._template, Template)
|
||||
assert TestComp._template.source == "<h1>child</h1>"
|
||||
assert TestComp._template.origin.component_cls == TestComp
|
||||
|
||||
def test_parent_null_child_non_null(self):
|
||||
class ParentComp(Component):
|
||||
js = None
|
||||
template = None
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = "console.log('child')"
|
||||
template = "<h1>child</h1>"
|
||||
|
||||
assert TestComp.js == "console.log('child')"
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template == "<h1>child</h1>"
|
||||
assert TestComp.template_file is None
|
||||
|
||||
assert ParentComp._template is None
|
||||
|
||||
assert isinstance(TestComp._template, Template)
|
||||
assert TestComp._template.source == "<h1>child</h1>"
|
||||
assert TestComp._template.origin.component_cls == TestComp
|
||||
|
||||
def test_parent_non_null_child_null(self):
|
||||
class ParentComp(Component):
|
||||
js: Optional[str] = "console.log('parent')"
|
||||
template: Optional[str] = "<h1>parent</h1>"
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = None
|
||||
template = None
|
||||
|
||||
assert TestComp.js is None
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template is None
|
||||
assert TestComp.template_file is None
|
||||
|
||||
assert TestComp._template is None
|
||||
|
||||
assert isinstance(ParentComp._template, Template)
|
||||
assert ParentComp._template.source == "<h1>parent</h1>"
|
||||
assert ParentComp._template.origin.component_cls == ParentComp
|
||||
|
||||
def test_parent_null_child_null(self):
|
||||
class ParentComp(Component):
|
||||
js = None
|
||||
template = None
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = None
|
||||
template = None
|
||||
|
||||
assert TestComp.js is None
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template is None
|
||||
assert TestComp.template_file is None
|
||||
|
||||
assert TestComp._template is None
|
||||
assert ParentComp._template is None
|
||||
|
||||
def test_grandparent_non_null_parent_pass_child_pass(self):
|
||||
class GrandParentComp(Component):
|
||||
js = "console.log('grandparent')"
|
||||
template = "<h1>grandparent</h1>"
|
||||
|
||||
class ParentComp(GrandParentComp):
|
||||
pass
|
||||
|
@ -1113,45 +1174,97 @@ class TestSubclassingAttributes:
|
|||
|
||||
assert TestComp.js == "console.log('grandparent')"
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template == "<h1>grandparent</h1>"
|
||||
assert TestComp.template_file is None
|
||||
|
||||
assert isinstance(GrandParentComp._template, Template)
|
||||
assert GrandParentComp._template.source == "<h1>grandparent</h1>"
|
||||
assert GrandParentComp._template.origin.component_cls == GrandParentComp
|
||||
|
||||
assert isinstance(ParentComp._template, Template)
|
||||
assert ParentComp._template.source == "<h1>grandparent</h1>"
|
||||
assert ParentComp._template.origin.component_cls == ParentComp
|
||||
|
||||
assert isinstance(TestComp._template, Template)
|
||||
assert TestComp._template.source == "<h1>grandparent</h1>"
|
||||
assert TestComp._template.origin.component_cls == TestComp
|
||||
|
||||
def test_grandparent_non_null_parent_null_child_pass(self):
|
||||
class GrandParentComp(Component):
|
||||
js: Optional[str] = "console.log('grandparent')"
|
||||
template: Optional[str] = "<h1>grandparent</h1>"
|
||||
|
||||
class ParentComp(GrandParentComp):
|
||||
js = None
|
||||
template = None
|
||||
|
||||
class TestComp(ParentComp):
|
||||
pass
|
||||
|
||||
assert TestComp.js is None
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template is None
|
||||
assert TestComp.template_file is None
|
||||
|
||||
assert isinstance(GrandParentComp._template, Template)
|
||||
assert GrandParentComp._template.source == "<h1>grandparent</h1>"
|
||||
assert GrandParentComp._template.origin.component_cls == GrandParentComp
|
||||
|
||||
assert ParentComp._template is None
|
||||
assert TestComp._template is None
|
||||
|
||||
def test_grandparent_non_null_parent_pass_child_non_null(self):
|
||||
class GrandParentComp(Component):
|
||||
js = "console.log('grandparent')"
|
||||
template = "<h1>grandparent</h1>"
|
||||
|
||||
class ParentComp(GrandParentComp):
|
||||
pass
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = "console.log('child')"
|
||||
template = "<h1>child</h1>"
|
||||
|
||||
assert TestComp.js == "console.log('child')"
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template == "<h1>child</h1>"
|
||||
assert TestComp.template_file is None
|
||||
|
||||
assert isinstance(GrandParentComp._template, Template)
|
||||
assert GrandParentComp._template.source == "<h1>grandparent</h1>"
|
||||
assert GrandParentComp._template.origin.component_cls == GrandParentComp
|
||||
|
||||
assert isinstance(ParentComp._template, Template)
|
||||
assert ParentComp._template.source == "<h1>grandparent</h1>"
|
||||
assert ParentComp._template.origin.component_cls == ParentComp
|
||||
|
||||
assert isinstance(TestComp._template, Template)
|
||||
assert TestComp._template.source == "<h1>child</h1>"
|
||||
assert TestComp._template.origin.component_cls == TestComp
|
||||
|
||||
def test_grandparent_null_parent_pass_child_non_null(self):
|
||||
class GrandParentComp(Component):
|
||||
js = None
|
||||
template = None
|
||||
|
||||
class ParentComp(GrandParentComp):
|
||||
pass
|
||||
|
||||
class TestComp(ParentComp):
|
||||
js = "console.log('child')"
|
||||
template = "<h1>child</h1>"
|
||||
|
||||
assert TestComp.js == "console.log('child')"
|
||||
assert TestComp.js_file is None
|
||||
assert TestComp.template == "<h1>child</h1>"
|
||||
assert TestComp.template_file is None
|
||||
|
||||
assert GrandParentComp._template is None
|
||||
assert ParentComp._template is None
|
||||
|
||||
assert isinstance(TestComp._template, Template)
|
||||
assert TestComp._template.source == "<h1>child</h1>"
|
||||
assert TestComp._template.origin.component_cls == TestComp
|
||||
|
||||
|
||||
@djc_test
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Any, Callable, Dict, List, cast
|
|||
|
||||
import pytest
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template import Context
|
||||
from django.template import Context, Origin, Template
|
||||
from django.test import Client
|
||||
|
||||
from django_components import Component, Slot, SlotNode, register, registry
|
||||
|
@ -23,6 +23,10 @@ from django_components.extension import (
|
|||
OnComponentDataContext,
|
||||
OnComponentRenderedContext,
|
||||
OnSlotRenderedContext,
|
||||
OnTemplateLoadedContext,
|
||||
OnTemplateCompiledContext,
|
||||
OnJsLoadedContext,
|
||||
OnCssLoadedContext,
|
||||
)
|
||||
from django_components.extensions.cache import CacheExtension
|
||||
from django_components.extensions.debug_highlight import DebugHighlightExtension
|
||||
|
@ -85,6 +89,10 @@ class DummyExtension(ComponentExtension):
|
|||
"on_component_data": [],
|
||||
"on_component_rendered": [],
|
||||
"on_slot_rendered": [],
|
||||
"on_template_loaded": [],
|
||||
"on_template_compiled": [],
|
||||
"on_js_loaded": [],
|
||||
"on_css_loaded": [],
|
||||
}
|
||||
|
||||
urls = [
|
||||
|
@ -126,6 +134,18 @@ class DummyExtension(ComponentExtension):
|
|||
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> None:
|
||||
self.calls["on_slot_rendered"].append(ctx)
|
||||
|
||||
def on_template_loaded(self, ctx):
|
||||
self.calls["on_template_loaded"].append(ctx)
|
||||
|
||||
def on_template_compiled(self, ctx):
|
||||
self.calls["on_template_compiled"].append(ctx)
|
||||
|
||||
def on_js_loaded(self, ctx):
|
||||
self.calls["on_js_loaded"].append(ctx)
|
||||
|
||||
def on_css_loaded(self, ctx):
|
||||
self.calls["on_css_loaded"].append(ctx)
|
||||
|
||||
|
||||
class DummyNestedExtension(ComponentExtension):
|
||||
name = "test_nested_extension"
|
||||
|
@ -182,6 +202,19 @@ def with_registry(on_created: Callable):
|
|||
on_created(registry)
|
||||
|
||||
|
||||
class OverrideAssetExtension(ComponentExtension):
|
||||
name = "override_asset_extension"
|
||||
|
||||
def on_template_loaded(self, ctx):
|
||||
return "OVERRIDDEN TEMPLATE"
|
||||
|
||||
def on_js_loaded(self, ctx):
|
||||
return "OVERRIDDEN JS"
|
||||
|
||||
def on_css_loaded(self, ctx):
|
||||
return "OVERRIDDEN CSS"
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestExtension:
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
|
@ -469,6 +502,120 @@ class TestExtensionHooks:
|
|||
rendered = TestComponent.render(args=(), kwargs={"name": "Test"})
|
||||
assert rendered == "<div>OVERRIDDEN: Hello Test!</div>"
|
||||
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_asset_hooks__inlined(self):
|
||||
@register("test_comp_hooks")
|
||||
class TestComponent(Component):
|
||||
template = "Hello {{ name }}!"
|
||||
js = "console.log('hi');"
|
||||
css = "body { color: red; }"
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"name": kwargs.get("name", "World")}
|
||||
|
||||
# Render the component to trigger all hooks
|
||||
TestComponent.render(args=(), kwargs={"name": "Test"})
|
||||
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||
|
||||
# on_template_loaded
|
||||
assert len(extension.calls["on_template_loaded"]) == 1
|
||||
ctx1: OnTemplateLoadedContext = extension.calls["on_template_loaded"][0]
|
||||
assert ctx1.component_cls == TestComponent
|
||||
assert ctx1.content == "Hello {{ name }}!"
|
||||
assert isinstance(ctx1.origin, Origin)
|
||||
assert ctx1.origin.name.endswith("test_extension.py::TestComponent")
|
||||
assert ctx1.name is None
|
||||
|
||||
# on_template_compiled
|
||||
assert len(extension.calls["on_template_compiled"]) == 1
|
||||
ctx2: OnTemplateCompiledContext = extension.calls["on_template_compiled"][0]
|
||||
assert ctx2.component_cls == TestComponent
|
||||
assert isinstance(ctx2.template, Template)
|
||||
|
||||
# on_js_loaded
|
||||
assert len(extension.calls["on_js_loaded"]) == 1
|
||||
ctx3: OnJsLoadedContext = extension.calls["on_js_loaded"][0]
|
||||
assert ctx3.component_cls == TestComponent
|
||||
assert ctx3.content == "console.log('hi');"
|
||||
|
||||
# on_css_loaded
|
||||
assert len(extension.calls["on_css_loaded"]) == 1
|
||||
ctx4: OnCssLoadedContext = extension.calls["on_css_loaded"][0]
|
||||
assert ctx4.component_cls == TestComponent
|
||||
assert ctx4.content == "body { color: red; }"
|
||||
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_asset_hooks__file(self):
|
||||
@register("test_comp_hooks")
|
||||
class TestComponent(Component):
|
||||
template_file = "relative_file/relative_file.html"
|
||||
js_file = "relative_file/relative_file.js"
|
||||
css_file = "relative_file/relative_file.css"
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"name": kwargs.get("name", "World")}
|
||||
|
||||
# Render the component to trigger all hooks
|
||||
TestComponent.render(args=(), kwargs={"name": "Test"})
|
||||
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||
|
||||
# on_template_loaded
|
||||
# NOTE: The template file gets picked up by 'django.template.loaders.filesystem.Loader',
|
||||
# as well as our own loader, so we get two calls here.
|
||||
assert len(extension.calls["on_template_loaded"]) == 2
|
||||
ctx1: OnTemplateLoadedContext = extension.calls["on_template_loaded"][0]
|
||||
assert ctx1.component_cls == TestComponent
|
||||
assert ctx1.content == (
|
||||
'<form method="post">\n'
|
||||
' {% csrf_token %}\n'
|
||||
' <input type="text" name="variable" value="{{ variable }}">\n'
|
||||
' <input type="submit">\n'
|
||||
'</form>\n'
|
||||
)
|
||||
assert isinstance(ctx1.origin, Origin)
|
||||
assert ctx1.origin.name.endswith("relative_file.html")
|
||||
assert ctx1.name == "relative_file/relative_file.html"
|
||||
|
||||
# on_template_compiled
|
||||
assert len(extension.calls["on_template_compiled"]) == 2
|
||||
ctx2: OnTemplateCompiledContext = extension.calls["on_template_compiled"][0]
|
||||
assert ctx2.component_cls == TestComponent
|
||||
assert isinstance(ctx2.template, Template)
|
||||
|
||||
# on_js_loaded
|
||||
assert len(extension.calls["on_js_loaded"]) == 1
|
||||
ctx3: OnJsLoadedContext = extension.calls["on_js_loaded"][0]
|
||||
assert ctx3.component_cls == TestComponent
|
||||
assert ctx3.content == 'console.log("JS file");\n'
|
||||
|
||||
# on_css_loaded
|
||||
assert len(extension.calls["on_css_loaded"]) == 1
|
||||
ctx4: OnCssLoadedContext = extension.calls["on_css_loaded"][0]
|
||||
assert ctx4.component_cls == TestComponent
|
||||
assert ctx4.content == (
|
||||
'.html-css-only {\n'
|
||||
' color: blue;\n'
|
||||
'}\n'
|
||||
)
|
||||
|
||||
@djc_test(components_settings={"extensions": [OverrideAssetExtension]})
|
||||
def test_asset_hooks_override(self):
|
||||
@register("test_comp_override")
|
||||
class TestComponent(Component):
|
||||
template = "Hello {{ name }}!"
|
||||
js = "console.log('hi');"
|
||||
css = "body { color: red; }"
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"name": kwargs.get("name", "World")}
|
||||
|
||||
# No need to render, accessing the attributes should trigger the hooks
|
||||
assert TestComponent.template == "OVERRIDDEN TEMPLATE"
|
||||
assert TestComponent.js == "OVERRIDDEN JS"
|
||||
assert TestComponent.css == "OVERRIDDEN CSS"
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestExtensionViews:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue