From f0fc1f66bca3e9381ffebc62697946aa606adf27 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 4 Dec 2023 17:46:53 -0800 Subject: [PATCH 001/109] Move example component to keep handling simpler --- laces/test/components/__init__.py | 6 +++++- laces/test/components/heading.py | 5 ----- 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 laces/test/components/heading.py diff --git a/laces/test/components/__init__.py b/laces/test/components/__init__.py index 45f4f4c..23f2557 100644 --- a/laces/test/components/__init__.py +++ b/laces/test/components/__init__.py @@ -1 +1,5 @@ -from .heading import * # noqa: F401, F403 +from laces.components import Component + + +class Heading(Component): + template_name = "components/heading.html" diff --git a/laces/test/components/heading.py b/laces/test/components/heading.py deleted file mode 100644 index 23f2557..0000000 --- a/laces/test/components/heading.py +++ /dev/null @@ -1,5 +0,0 @@ -from laces.components import Component - - -class Heading(Component): - template_name = "components/heading.html" From 4bf808b3e2542cf53a276b3aeeebd730f0aec4f5 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 4 Dec 2023 17:50:45 -0800 Subject: [PATCH 002/109] Rename tempalte and variables to express what they test --- laces/test/components/__init__.py | 4 ++-- laces/test/components/templates/components/heading.html | 1 - .../components/templates/components/static-template.html | 1 + laces/test/home/templates/home/home.html | 2 +- laces/test/home/views.py | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 laces/test/components/templates/components/heading.html create mode 100644 laces/test/components/templates/components/static-template.html diff --git a/laces/test/components/__init__.py b/laces/test/components/__init__.py index 23f2557..b1a0ae7 100644 --- a/laces/test/components/__init__.py +++ b/laces/test/components/__init__.py @@ -1,5 +1,5 @@ from laces.components import Component -class Heading(Component): - template_name = "components/heading.html" +class StaticTemplate(Component): + template_name = "components/static-template.html" diff --git a/laces/test/components/templates/components/heading.html b/laces/test/components/templates/components/heading.html deleted file mode 100644 index e583759..0000000 --- a/laces/test/components/templates/components/heading.html +++ /dev/null @@ -1 +0,0 @@ -

Test

diff --git a/laces/test/components/templates/components/static-template.html b/laces/test/components/templates/components/static-template.html new file mode 100644 index 0000000..328ee9f --- /dev/null +++ b/laces/test/components/templates/components/static-template.html @@ -0,0 +1 @@ +

Static Template

diff --git a/laces/test/home/templates/home/home.html b/laces/test/home/templates/home/home.html index 1a00229..6af2c9e 100644 --- a/laces/test/home/templates/home/home.html +++ b/laces/test/home/templates/home/home.html @@ -1,2 +1,2 @@ {% load laces %} -{% component heading %} +{% component static_template %} diff --git a/laces/test/home/views.py b/laces/test/home/views.py index ea1f6fe..19cf225 100644 --- a/laces/test/home/views.py +++ b/laces/test/home/views.py @@ -1,12 +1,12 @@ from django.shortcuts import render -from laces.test.components import Heading +from laces.test.components import StaticTemplate def home(request): - heading = Heading() + static_template = StaticTemplate() return render( request, template_name="home/home.html", - context={"heading": heading}, + context={"static_template": static_template}, ) From f5f536572665e2b96af20c4002823f76c7397f86 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 4 Dec 2023 18:02:13 -0800 Subject: [PATCH 003/109] Test rendering of static template --- laces/test/tests/test_components.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 laces/test/tests/test_components.py diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py new file mode 100644 index 0000000..aaba872 --- /dev/null +++ b/laces/test/tests/test_components.py @@ -0,0 +1,20 @@ +from django.test import SimpleTestCase + +from laces.test.components import StaticTemplate + + +class TestStaticTemplate(SimpleTestCase): + def setUp(self): + self.component = StaticTemplate() + + def test_template_name(self): + self.assertEqual( + self.component.template_name, + "components/static-template.html", + ) + + def test_render_html(self): + self.assertEqual( + self.component.render_html(), + "

Static Template

\n", + ) From fc3aa9e3e6f40dc44179f534a22268f40e38a5a4 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 4 Dec 2023 20:23:44 -0800 Subject: [PATCH 004/109] Test component that passes fixed value to template context --- laces/test/components/__init__.py | 11 +++++- .../templates/components/hello-name.html | 1 + .../templates/components/hello-world.html | 1 + .../templates/components/static-template.html | 1 - laces/test/home/templates/home/home.html | 3 +- laces/test/home/views.py | 13 +++++-- laces/test/tests/test_components.py | 38 +++++++++++++++++-- 7 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 laces/test/components/templates/components/hello-name.html create mode 100644 laces/test/components/templates/components/hello-world.html delete mode 100644 laces/test/components/templates/components/static-template.html diff --git a/laces/test/components/__init__.py b/laces/test/components/__init__.py index b1a0ae7..1646e64 100644 --- a/laces/test/components/__init__.py +++ b/laces/test/components/__init__.py @@ -1,5 +1,12 @@ from laces.components import Component -class StaticTemplate(Component): - template_name = "components/static-template.html" +class RendersTemplateWithFixedContentComponent(Component): + template_name = "components/hello-world.html" + + +class PassesFixedNameToContextComponent(Component): + template_name = "components/hello-name.html" + + def get_context_data(self, parent_context=None): + return {"name": "Alice"} diff --git a/laces/test/components/templates/components/hello-name.html b/laces/test/components/templates/components/hello-name.html new file mode 100644 index 0000000..71499e2 --- /dev/null +++ b/laces/test/components/templates/components/hello-name.html @@ -0,0 +1 @@ +

Hello {{ name }}

diff --git a/laces/test/components/templates/components/hello-world.html b/laces/test/components/templates/components/hello-world.html new file mode 100644 index 0000000..f3e333e --- /dev/null +++ b/laces/test/components/templates/components/hello-world.html @@ -0,0 +1 @@ +

Hello World

diff --git a/laces/test/components/templates/components/static-template.html b/laces/test/components/templates/components/static-template.html deleted file mode 100644 index 328ee9f..0000000 --- a/laces/test/components/templates/components/static-template.html +++ /dev/null @@ -1 +0,0 @@ -

Static Template

diff --git a/laces/test/home/templates/home/home.html b/laces/test/home/templates/home/home.html index 6af2c9e..4c79e20 100644 --- a/laces/test/home/templates/home/home.html +++ b/laces/test/home/templates/home/home.html @@ -1,2 +1,3 @@ {% load laces %} -{% component static_template %} +{% component fixed_content %} +{% component passes_name %} diff --git a/laces/test/home/views.py b/laces/test/home/views.py index 19cf225..7d57370 100644 --- a/laces/test/home/views.py +++ b/laces/test/home/views.py @@ -1,12 +1,19 @@ from django.shortcuts import render -from laces.test.components import StaticTemplate +from laces.test.components import ( + PassesFixedNameToContextComponent, + RendersTemplateWithFixedContentComponent, +) def home(request): - static_template = StaticTemplate() + fixed_content = RendersTemplateWithFixedContentComponent() + passes_name = PassesFixedNameToContextComponent() return render( request, template_name="home/home.html", - context={"static_template": static_template}, + context={ + "fixed_content": fixed_content, + "passes_name": passes_name, + }, ) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index aaba872..67a2823 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -1,20 +1,50 @@ from django.test import SimpleTestCase -from laces.test.components import StaticTemplate +from laces.test.components import ( + PassesFixedNameToContextComponent, + RendersTemplateWithFixedContentComponent, +) class TestStaticTemplate(SimpleTestCase): + """Test that the template is rendered.""" + + def setUp(self): + self.component = RendersTemplateWithFixedContentComponent() + + def test_template_name(self): + self.assertEqual( + self.component.template_name, + "components/hello-world.html", + ) + + def test_render_html(self): + self.assertEqual( + self.component.render_html(), + "

Hello World

\n", + ) + + +class TestPassesFixedNameToContextComponent(SimpleTestCase): + """Test that the context is used to render the template.""" + def setUp(self): - self.component = StaticTemplate() + self.component = PassesFixedNameToContextComponent() def test_template_name(self): self.assertEqual( self.component.template_name, - "components/static-template.html", + "components/hello-name.html", + ) + + def test_get_context_data(self): + self.assertEqual( + self.component.get_context_data(), + {"name": "Alice"}, ) def test_render_html(self): self.assertEqual( self.component.render_html(), - "

Static Template

\n", + "

Hello Alice

\n", ) From fbd4903190337980936f906fdc77dd81d4495fa6 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 12:51:48 -0800 Subject: [PATCH 005/109] Flip order of methods in Component to higher level at top I usually find it easier to reason about modules and classes when the higher level code it at the top. I also think it's nice when the order somewhat follows the control flow. --- laces/components.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/laces/components.py b/laces/components.py index 46fbadf..2d8502f 100644 --- a/laces/components.py +++ b/laces/components.py @@ -13,11 +13,6 @@ class Component(metaclass=MediaDefiningClass): https://github.com/wagtail/wagtail/blob/094834909d5c4b48517fd2703eb1f6d386572ffa/wagtail/admin/ui/components.py#L8-L22 # noqa: E501 """ - def get_context_data( - self, parent_context: MutableMapping[str, Any] - ) -> MutableMapping[str, Any]: - return {} - def render_html(self, parent_context: MutableMapping[str, Any] = None) -> str: """ Return string representation of the object. @@ -39,6 +34,11 @@ def render_html(self, parent_context: MutableMapping[str, Any] = None) -> str: template = get_template(self.template_name) return template.render(context_data) + def get_context_data( + self, parent_context: MutableMapping[str, Any] + ) -> MutableMapping[str, Any]: + return {} + class MediaContainer(list): """ From 4c1bd4f65282e7de344589b76f6fc208c6228803 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 13:08:10 -0800 Subject: [PATCH 006/109] Add test for the basic Component class --- laces/tests/test_components.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 laces/tests/test_components.py diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py new file mode 100644 index 0000000..6a4c609 --- /dev/null +++ b/laces/tests/test_components.py @@ -0,0 +1,30 @@ +from django.template import Context +from django.test import SimpleTestCase + +from laces.components import Component + + +class TestComponent(SimpleTestCase): + """Directly test the Component class.""" + + def setUp(self): + self.component = Component() + + def test_render_html(self): + """Test the `render_html` method.""" + # The default Component does not specify a `tempalte_name` attribute which is + # required for `render_html`. + with self.assertRaises(AttributeError): + self.component.render_html() + + def test_get_context_data_parent_context_empty_context(self): + """ + Test the default get_context_data. + + The parent context should not matter, but we use it as it is used in + `render_html`. + """ + result = self.component.get_context_data(parent_context=Context()) + + self.assertIsInstance(result, dict) + self.assertEqual(result, {}) From e598382af802e5fbfa91daede0d32c5389939304 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 14:15:46 -0800 Subject: [PATCH 007/109] Add test for media property --- laces/components.py | 5 +++++ laces/tests/test_components.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/laces/components.py b/laces/components.py index 2d8502f..408fe5d 100644 --- a/laces/components.py +++ b/laces/components.py @@ -11,6 +11,11 @@ class Component(metaclass=MediaDefiningClass): Extracted from Wagtail. See: https://github.com/wagtail/wagtail/blob/094834909d5c4b48517fd2703eb1f6d386572ffa/wagtail/admin/ui/components.py#L8-L22 # noqa: E501 + + A component uses the `MetaDefiningClass` metaclass to add a `media` property, which + allows the definitions of CSS and JavaScript assets that are associated with the + component. This is practically the same as `Media` class used by Django forms. + See also: https://docs.djangoproject.com/en/4.2/topics/forms/media/ """ def render_html(self, parent_context: MutableMapping[str, Any] = None) -> str: diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 6a4c609..a591eca 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -1,3 +1,4 @@ +from django.forms.widgets import Media from django.template import Context from django.test import SimpleTestCase @@ -28,3 +29,16 @@ def test_get_context_data_parent_context_empty_context(self): self.assertIsInstance(result, dict) self.assertEqual(result, {}) + + def test_media(self): + """ + Test the `media` property. + + The `media` property is added through the `metaclass=MediaDefiningClass` + definition. + + """ + self.assertIsInstance(self.component.media, Media) + empty_media = Media() + self.assertEqual(self.component.media._css, empty_media._css) + self.assertEqual(self.component.media._js, empty_media._js) From aa379ebd492ad7c680ca186b8d9e3c1d83694571 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 14:36:27 -0800 Subject: [PATCH 008/109] Add test for subclass that defines a template name --- laces/templates/.gitkeep | 0 laces/templates/example.html | 0 laces/tests/test_components.py | 17 +++++++++++++++++ testmanage.py | 0 4 files changed, 17 insertions(+) create mode 100644 laces/templates/.gitkeep create mode 100644 laces/templates/example.html mode change 100644 => 100755 testmanage.py diff --git a/laces/templates/.gitkeep b/laces/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/laces/templates/example.html b/laces/templates/example.html new file mode 100644 index 0000000..e69de29 diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index a591eca..1a9c066 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -42,3 +42,20 @@ def test_media(self): empty_media = Media() self.assertEqual(self.component.media._css, empty_media._css) self.assertEqual(self.component.media._js, empty_media._js) + + +class TestComponentSubclasses(SimpleTestCase): + """ + Test the Component class through subclasses. + + Most functionality of the Component class is only unlocked through subclassing and + definition of certain attributes (like `template_name`). + """ + + def test_render_html_with_template_name_set(self): + class ExampleComponent(Component): + template_name = "example.html" + + result = ExampleComponent().render_html() + + self.assertIsInstance(result, str) diff --git a/testmanage.py b/testmanage.py old mode 100644 new mode 100755 From f434c3bb4639bcfec6a646d5626195b94692abe9 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 14:57:24 -0800 Subject: [PATCH 009/109] Create example template in test code --- laces/templates/example.html | 0 laces/tests/test_components.py | 23 ++++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) delete mode 100644 laces/templates/example.html diff --git a/laces/templates/example.html b/laces/templates/example.html deleted file mode 100644 index e69de29..0000000 diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 1a9c066..0876e26 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -1,3 +1,8 @@ +import os + +from pathlib import Path + +from django.conf import settings from django.forms.widgets import Media from django.template import Context from django.test import SimpleTestCase @@ -52,10 +57,26 @@ class TestComponentSubclasses(SimpleTestCase): definition of certain attributes (like `template_name`). """ + def setUp(self): + self.example_template_name = "example.html" + self.example_template = ( + Path(settings.PROJECT_DIR) / "templates" / self.example_template_name + ) + + def set_example_template_content(self, content: str): + with open(self.example_template, "w") as f: + f.write(content) + def test_render_html_with_template_name_set(self): class ExampleComponent(Component): - template_name = "example.html" + template_name = self.example_template_name + + self.set_example_template_content("Test") result = ExampleComponent().render_html() self.assertIsInstance(result, str) + self.assertEqual(result, "Test") + + def tearDown(self): + os.remove(path=self.example_template) From 218473826cc603c145ad9f1b83eb830cad41ab84 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 15:05:10 -0800 Subject: [PATCH 010/109] Test for SafeString --- laces/tests/test_components.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 0876e26..21256c1 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -6,6 +6,7 @@ from django.forms.widgets import Media from django.template import Context from django.test import SimpleTestCase +from django.utils.html import SafeString from laces.components import Component @@ -54,7 +55,8 @@ class TestComponentSubclasses(SimpleTestCase): Test the Component class through subclasses. Most functionality of the Component class is only unlocked through subclassing and - definition of certain attributes (like `template_name`). + definition of certain attributes (like `template_name`) or overriding of the + existing methods. """ def setUp(self): @@ -76,6 +78,7 @@ class ExampleComponent(Component): result = ExampleComponent().render_html() self.assertIsInstance(result, str) + self.assertIsInstance(result, SafeString) self.assertEqual(result, "Test") def tearDown(self): From 939ac362e994332fca1843377f62baa37c205a5f Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 15:19:29 -0800 Subject: [PATCH 011/109] Include randomness in example template file names --- laces/tests/test_components.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 21256c1..b59f5e6 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -1,4 +1,5 @@ import os +import random from pathlib import Path @@ -59,8 +60,19 @@ class TestComponentSubclasses(SimpleTestCase): existing methods. """ + @classmethod + def make_example_template_name(cls): + return f"example-{random.randint(1000, 10000)}.html" + + @classmethod + def get_example_template_name(cls): + example_template_name = cls.make_example_template_name() + while os.path.exists(example_template_name): + example_template_name = cls.make_example_template_name() + return example_template_name + def setUp(self): - self.example_template_name = "example.html" + self.example_template_name = self.get_example_template_name() self.example_template = ( Path(settings.PROJECT_DIR) / "templates" / self.example_template_name ) From c3fe1c28d5634bebbad281858ada4d3174c2098f Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 15:22:20 -0800 Subject: [PATCH 012/109] Test rendering with context value --- laces/tests/test_components.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index b59f5e6..ea07e52 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -82,9 +82,12 @@ def set_example_template_content(self, content: str): f.write(content) def test_render_html_with_template_name_set(self): + # ----------------------------------------------------------------------------- class ExampleComponent(Component): template_name = self.example_template_name + # ----------------------------------------------------------------------------- + self.set_example_template_content("Test") result = ExampleComponent().render_html() @@ -93,5 +96,21 @@ class ExampleComponent(Component): self.assertIsInstance(result, SafeString) self.assertEqual(result, "Test") + def test_render_html_with_template_name_set_and_data_from_get_context_data(self): + # ----------------------------------------------------------------------------- + class ExampleComponent(Component): + template_name = self.example_template_name + + def get_context_data(self, parent_context): + return {"name": "World"} + + # ----------------------------------------------------------------------------- + + self.set_example_template_content("Hello {{ name }}") + + result = ExampleComponent().render_html() + + self.assertEqual(result, "Hello World") + def tearDown(self): os.remove(path=self.example_template) From ee5076eee55fb8fa1d8dc660bd3479600cf4f3c5 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 15:50:29 -0800 Subject: [PATCH 013/109] Test render_html when get_context_data returns None The implementation has a special case for some reason. --- laces/tests/test_components.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index ea07e52..8d9d7df 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -2,6 +2,7 @@ import random from pathlib import Path +from test.support import os_helper from django.conf import settings from django.forms.widgets import Media @@ -76,12 +77,17 @@ def setUp(self): self.example_template = ( Path(settings.PROJECT_DIR) / "templates" / self.example_template_name ) + os_helper.create_empty_file(self.example_template) def set_example_template_content(self, content: str): with open(self.example_template, "w") as f: f.write(content) def test_render_html_with_template_name_set(self): + """ + Test the `render_html` with a set `template_name` attribute. + """ + # ----------------------------------------------------------------------------- class ExampleComponent(Component): template_name = self.example_template_name @@ -97,6 +103,10 @@ class ExampleComponent(Component): self.assertEqual(result, "Test") def test_render_html_with_template_name_set_and_data_from_get_context_data(self): + """ + Test the `render_html` with `get_context_data` providing data for the context. + """ + # ----------------------------------------------------------------------------- class ExampleComponent(Component): template_name = self.example_template_name @@ -112,5 +122,27 @@ def get_context_data(self, parent_context): self.assertEqual(result, "Hello World") + def test_render_html_when_get_context_data_returns_None(self): + """ + Test the `render_html` method when `get_context_data` returns `None`. + + This behavior was present when the class was extracted. It is not totally clear + why this specific check is needed. By default, the `get_context_data` method + provides and empty dict. If an override wanted to `get_context_data` return + `None`, it should be expected that no context data is available during + rendering. The underlying `template.render` method does not seem to case about + `None` as the context. + """ + + # ----------------------------------------------------------------------------- + class ExampleComponent(Component): + def get_context_data(self, parent_context): + return None + + # ----------------------------------------------------------------------------- + + with self.assertRaises(TypeError): + ExampleComponent().render_html() + def tearDown(self): os.remove(path=self.example_template) From 64a84b2d586da7fc24ec167800b4add3b33d301e Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 15:52:10 -0800 Subject: [PATCH 014/109] Remove empty models file --- laces/models.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 laces/models.py diff --git a/laces/models.py b/laces/models.py deleted file mode 100644 index e69de29..0000000 From 77751af0a77cfe20e22e0be8c6865ee20d70ab88 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 19:51:39 -0800 Subject: [PATCH 015/109] Add method docstring --- laces/components.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/laces/components.py b/laces/components.py index 408fe5d..c2c13c3 100644 --- a/laces/components.py +++ b/laces/components.py @@ -56,6 +56,13 @@ class MediaContainer(list): @property def media(self): + """ + Return a `Media` object containing the media definitions of all members. + + This makes use of the `Media.__add__` method, which combines the media + definitions of two `Media` objects. + + """ media = Media() for item in self: media += item.media From 396d06d701fd29352b5c792ca8e9e09d9d4b6dd4 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 19:54:43 -0800 Subject: [PATCH 016/109] Use Markdown in docstring over reStructuredText --- laces/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/laces/components.py b/laces/components.py index c2c13c3..1b313a4 100644 --- a/laces/components.py +++ b/laces/components.py @@ -23,7 +23,7 @@ def render_html(self, parent_context: MutableMapping[str, Any] = None) -> str: Return string representation of the object. Given a context dictionary from the calling template (which may be a - `django.template.Context` object or a plain ``dict`` of context variables), + `django.template.Context` object or a plain `dict` of context variables), returns the string representation to be rendered. This will be subject to Django's HTML escaping rules, so a return value @@ -47,7 +47,7 @@ def get_context_data( class MediaContainer(list): """ - A list that provides a ``media`` property that combines the media definitions + A list that provides a `media` property that combines the media definitions of its members. Extracted from Wagtail. See: From 26878bb52cf78246094db59476db3aa6fd4fd3ac Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 20:02:16 -0800 Subject: [PATCH 017/109] Add first media container test --- laces/tests/test_components.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 8d9d7df..ef9e844 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -10,7 +10,7 @@ from django.test import SimpleTestCase from django.utils.html import SafeString -from laces.components import Component +from laces.components import Component, MediaContainer class TestComponent(SimpleTestCase): @@ -146,3 +146,24 @@ def get_context_data(self, parent_context): def tearDown(self): os.remove(path=self.example_template) + + +class TestMediaContainer(SimpleTestCase): + """Test the MediaContainer class.""" + + def setUp(self): + self.media_container = MediaContainer() + + def test_single_member(self): + class ExampleClass: + media = Media(css={"all": ("example.css",)}) + + example = ExampleClass() + + self.media_container.append(example) + + self.assertIsInstance(self.media_container.media, Media) + self.assertEqual(self.media_container.media._css, example.media._css) + self.assertEqual(self.media_container.media._css, {"all": ["example.css"]}) + self.assertEqual(self.media_container.media._js, example.media._js) + self.assertEqual(self.media_container.media._js, []) From 2f5dd702b444a3e5b3e82632f92a78052701cbd7 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 20:07:48 -0800 Subject: [PATCH 018/109] Add second test for media container --- laces/tests/test_components.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index ef9e844..cbe1ed8 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -155,15 +155,35 @@ def setUp(self): self.media_container = MediaContainer() def test_single_member(self): + # ----------------------------------------------------------------------------- class ExampleClass: - media = Media(css={"all": ("example.css",)}) + media = Media(css={"all": ["example.css"]}) + # ----------------------------------------------------------------------------- example = ExampleClass() + self.media_container.append(example) + result = self.media_container.media + + self.assertIsInstance(result, Media) + self.assertEqual(result._css, example.media._css) + self.assertEqual(result._css, {"all": ["example.css"]}) + self.assertEqual(result._js, example.media._js) + self.assertEqual(result._js, []) + + def test_two_same_members_of_same_class(self): + # ----------------------------------------------------------------------------- + class ExampleClass: + media = Media(css={"all": ["example.css"]}, js=["example.js"]) + + # ----------------------------------------------------------------------------- + example = ExampleClass() self.media_container.append(example) - self.assertIsInstance(self.media_container.media, Media) - self.assertEqual(self.media_container.media._css, example.media._css) - self.assertEqual(self.media_container.media._css, {"all": ["example.css"]}) - self.assertEqual(self.media_container.media._js, example.media._js) - self.assertEqual(self.media_container.media._js, []) + result = self.media_container.media + + self.assertIsInstance(result, Media) + self.assertEqual(result._css, example.media._css) + self.assertEqual(result._css, {"all": ["example.css"]}) + self.assertEqual(result._js, example.media._js) + self.assertEqual(result._js, ["example.js"]) From 5caaf101d37cefcdc2d1452e7a690f5848469c9b Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 20:10:34 -0800 Subject: [PATCH 019/109] Test empty container --- laces/tests/test_components.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index cbe1ed8..ebfb482 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -154,6 +154,13 @@ class TestMediaContainer(SimpleTestCase): def setUp(self): self.media_container = MediaContainer() + def test_empty(self): + result = self.media_container.media + + self.assertIsInstance(result, Media) + self.assertEqual(result._css, {}) + self.assertEqual(result._js, []) + def test_single_member(self): # ----------------------------------------------------------------------------- class ExampleClass: From f0824c70440103adef1c465a257b1cbb11aa6b3c Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 20:15:16 -0800 Subject: [PATCH 020/109] Test members of different classes --- laces/tests/test_components.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index ebfb482..b68c878 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -178,7 +178,7 @@ class ExampleClass: self.assertEqual(result._js, example.media._js) self.assertEqual(result._js, []) - def test_two_same_members_of_same_class(self): + def test_two_members_of_same_class(self): # ----------------------------------------------------------------------------- class ExampleClass: media = Media(css={"all": ["example.css"]}, js=["example.js"]) @@ -194,3 +194,35 @@ class ExampleClass: self.assertEqual(result._css, {"all": ["example.css"]}) self.assertEqual(result._js, example.media._js) self.assertEqual(result._js, ["example.js"]) + + def test_two_members_of_different_classes(self): + # ----------------------------------------------------------------------------- + class ExampleClass: + media = Media(css={"all": ["shared.css"]}, js=["example.js"]) + + class OtherExampleClass: + media = Media( + css={ + "all": ["other.css", "shared.css"], + "print": ["print.css"], + }, + js=["other.js"], + ) + + # ----------------------------------------------------------------------------- + example = ExampleClass() + self.media_container.append(example) + other = OtherExampleClass() + self.media_container.append(other) + + result = self.media_container.media + + self.assertIsInstance(result, Media) + self.assertEqual( + result._css, + { + "all": ["other.css", "shared.css"], + "print": ["print.css"], + }, + ) + self.assertEqual(result._js, ["example.js", "other.js"]) From 5cc1c5ce20222b7bfa0055a6dc938694fe065996 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 20:19:12 -0800 Subject: [PATCH 021/109] Expand `MediaContainer` docstring --- laces/components.py | 9 +++++++++ laces/tests/test_components.py | 13 ++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/laces/components.py b/laces/components.py index 1b313a4..b55ffe3 100644 --- a/laces/components.py +++ b/laces/components.py @@ -52,6 +52,15 @@ class MediaContainer(list): Extracted from Wagtail. See: https://github.com/wagtail/wagtail/blob/ca8a87077b82e20397e5a5b80154d923995e6ca9/wagtail/admin/ui/components.py#L25-L36 # noqa: E501 + + The `MediaContainer` functionality depends on the `django.forms.widgets.Media` + class. The `Media` class provides the logic to combine the media definitions of + multiple objects through its `__add__` method. The `MediaContainer` relies on this + functionality to provide a `media` property that combines the media definitions of + its members. + + See also: + https://docs.djangoproject.com/en/4.2/topics/forms/media """ @property diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index b68c878..09cd89c 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -149,7 +149,18 @@ def tearDown(self): class TestMediaContainer(SimpleTestCase): - """Test the MediaContainer class.""" + """ + Test the MediaContainer class. + + The `MediaContainer` functionality depends on the `django.forms.widgets.Media` + class. The `Media` class provides the logic to combine the media definitions of + multiple objects through its `__add__` method. The `MediaContainer` relies on this + functionality to provide a `media` property that combines the media definitions of + its members. + + See also: + https://docs.djangoproject.com/en/4.2/topics/forms/media + """ def setUp(self): self.media_container = MediaContainer() From d1c14fa17deba727e3405c7bf6ac36cb22794e26 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 20:28:40 -0800 Subject: [PATCH 022/109] Test nested Media class def in Component subclass --- laces/tests/test_components.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 09cd89c..0e4f69d 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -144,6 +144,29 @@ def get_context_data(self, parent_context): with self.assertRaises(TypeError): ExampleComponent().render_html() + def test_media_defined_through_nested_class(self): + """ + Test the `media` property when defined through a nested class. + + The `media` property is added through the `metaclass=MediaDefiningClass` + definition. This test ensures that the `media` property is available when + configured through a nested class. + """ + + # ----------------------------------------------------------------------------- + class ExampleComponent(Component): + class Media: + css = {"all": ["example.css"]} + js = ["example.js"] + + # ----------------------------------------------------------------------------- + + result = ExampleComponent().media + + self.assertIsInstance(result, Media) + self.assertEqual(result._css, {"all": ["example.css"]}) + self.assertEqual(result._js, ["example.js"]) + def tearDown(self): os.remove(path=self.example_template) From 3149f3bc8f418840de10eaaa1d37ed34965206f7 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 20:32:25 -0800 Subject: [PATCH 023/109] Remove reliance on os_helper module --- laces/tests/test_components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 0e4f69d..57d0221 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -2,7 +2,6 @@ import random from pathlib import Path -from test.support import os_helper from django.conf import settings from django.forms.widgets import Media @@ -77,7 +76,8 @@ def setUp(self): self.example_template = ( Path(settings.PROJECT_DIR) / "templates" / self.example_template_name ) - os_helper.create_empty_file(self.example_template) + # Write content to the template file to ensure it exists. + self.set_example_template_content("") def set_example_template_content(self, content: str): with open(self.example_template, "w") as f: From 649f8a87ea6305f7b37a74c02f05e0226be0afeb Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 10 Dec 2023 20:35:27 -0800 Subject: [PATCH 024/109] Add test docstring --- laces/test/tests/test_components.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 67a2823..4bcb06a 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -1,3 +1,6 @@ +""" +Tests for a variety of different ways how components may be used. + """ from django.test import SimpleTestCase from laces.test.components import ( From 37b0de43ecb5e0bb261d6a8011ef4ebb6261a444 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 18:11:06 -0800 Subject: [PATCH 025/109] Add test with only mock component in context --- laces/tests/test_templatetags/test_laces.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index cc29047..9d23379 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -13,6 +13,27 @@ class TestComponentTag(SimpleTestCase): https://github.com/wagtail/wagtail/blob/main/wagtail/admin/tests/test_templatetags.py#L225-L305 # noqa: E501 """ + def setUp(self): + self.parent_template = Template("") + + def set_parent_template(self, template_string): + self.parent_template = Template(template_string) + + def render_parent_template_with_context(self, context: dict): + return self.parent_template.render(Context(context)) + + def test_only_component_in_context(self): + from unittest import mock + + component = mock.Mock(name="component") + + self.set_parent_template("{% load laces %}{% component component %}") + + self.render_parent_template_with_context({"component": component}) + + self.assertTrue(component.render_html.called) + self.assertTrue(component.render_html.called_with(Context())) + def test_passing_context_to_component(self): class MyComponent(Component): def render_html(self, parent_context): From 2aa7b1eeb5fa2a8e28298bc2f3f92a4929501873 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 18:21:23 -0800 Subject: [PATCH 026/109] Test other variable in context --- laces/tests/test_templatetags/test_laces.py | 29 +++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 9d23379..52e60f0 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -1,3 +1,5 @@ +from unittest.mock import Mock + from django.template import Context, Template from django.test import SimpleTestCase from django.utils.html import format_html @@ -15,6 +17,10 @@ class TestComponentTag(SimpleTestCase): def setUp(self): self.parent_template = Template("") + # Testing with a mock component to make it easy to check method calls and passed + # arguments. Since it is the responsibility of the component to render itself, + # we don't need to check the rendering here. + self.component = Mock(name="component") def set_parent_template(self, template_string): self.parent_template = Template(template_string) @@ -22,17 +28,30 @@ def set_parent_template(self, template_string): def render_parent_template_with_context(self, context: dict): return self.parent_template.render(Context(context)) + def assertRenderHTMLCalledWith(self, context: dict): + self.assertTrue(self.component.render_html.called_with(Context(context))) + def test_only_component_in_context(self): - from unittest import mock + self.set_parent_template("{% load laces %}{% component component %}") - component = mock.Mock(name="component") + self.render_parent_template_with_context({"component": self.component}) + self.assertTrue(self.component.render_html.called) + # The component itself is not included in the context that is passed to the + # `render_html` method. + self.assertRenderHTMLCalledWith({}) + + def test_other_variable_in_context(self): self.set_parent_template("{% load laces %}{% component component %}") - self.render_parent_template_with_context({"component": component}) + self.render_parent_template_with_context( + { + "component": self.component, + "test": "something", + } + ) - self.assertTrue(component.render_html.called) - self.assertTrue(component.render_html.called_with(Context())) + self.assertRenderHTMLCalledWith({"test": "something"}) def test_passing_context_to_component(self): class MyComponent(Component): From af05b78f5c96388c081bbb065ee7353dd3888b31 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 18:23:56 -0800 Subject: [PATCH 027/109] Test with sets extra context --- laces/tests/test_templatetags/test_laces.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 52e60f0..41a8327 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -53,6 +53,15 @@ def test_other_variable_in_context(self): self.assertRenderHTMLCalledWith({"test": "something"}) + def test_with_sets_extra_context(self): + self.set_parent_template( + "{% load laces %}{% component component with test='something' %}" + ) + + self.render_parent_template_with_context({"component": self.component}) + + self.assertRenderHTMLCalledWith({"test": "something"}) + def test_passing_context_to_component(self): class MyComponent(Component): def render_html(self, parent_context): From c3243902ebb7d2ebb0f5c8894f5b246f08bfffab Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 18:25:12 -0800 Subject: [PATCH 028/109] Rename context variable to avoid shadowing --- laces/tests/test_templatetags/test_laces.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 41a8327..c8e7088 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -32,9 +32,9 @@ def assertRenderHTMLCalledWith(self, context: dict): self.assertTrue(self.component.render_html.called_with(Context(context))) def test_only_component_in_context(self): - self.set_parent_template("{% load laces %}{% component component %}") + self.set_parent_template("{% load laces %}{% component my_component %}") - self.render_parent_template_with_context({"component": self.component}) + self.render_parent_template_with_context({"my_component": self.component}) self.assertTrue(self.component.render_html.called) # The component itself is not included in the context that is passed to the @@ -42,11 +42,11 @@ def test_only_component_in_context(self): self.assertRenderHTMLCalledWith({}) def test_other_variable_in_context(self): - self.set_parent_template("{% load laces %}{% component component %}") + self.set_parent_template("{% load laces %}{% component my_component %}") self.render_parent_template_with_context( { - "component": self.component, + "my_component": self.component, "test": "something", } ) @@ -55,10 +55,10 @@ def test_other_variable_in_context(self): def test_with_sets_extra_context(self): self.set_parent_template( - "{% load laces %}{% component component with test='something' %}" + "{% load laces %}{% component my_component with test='something' %}" ) - self.render_parent_template_with_context({"component": self.component}) + self.render_parent_template_with_context({"my_component": self.component}) self.assertRenderHTMLCalledWith({"test": "something"}) From bff31c208858126bf3f6682a7fcd9940b13c8fd9 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 18:30:12 -0800 Subject: [PATCH 029/109] Test with block sets extra context --- laces/tests/test_templatetags/test_laces.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index c8e7088..b3e3f72 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -53,7 +53,18 @@ def test_other_variable_in_context(self): self.assertRenderHTMLCalledWith({"test": "something"}) - def test_with_sets_extra_context(self): + def test_with_block_sets_extra_context(self): + self.set_parent_template( + """ + {% load laces %}{% with test='something' %}{% component my_component %}{% endwith %} + """ + ) + + self.render_parent_template_with_context({"my_component": self.component}) + + self.assertRenderHTMLCalledWith({"test": "something"}) + + def test_with_keyword_sets_extra_context(self): self.set_parent_template( "{% load laces %}{% component my_component with test='something' %}" ) From 288f58c1fcc715b4b34c429a164bf584b97a39d8 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 18:37:58 -0800 Subject: [PATCH 030/109] Refactor helper --- laces/tests/test_templatetags/test_laces.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index b3e3f72..52da22d 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -23,6 +23,7 @@ def setUp(self): self.component = Mock(name="component") def set_parent_template(self, template_string): + template_string = "{% load laces %}" + template_string self.parent_template = Template(template_string) def render_parent_template_with_context(self, context: dict): @@ -32,7 +33,7 @@ def assertRenderHTMLCalledWith(self, context: dict): self.assertTrue(self.component.render_html.called_with(Context(context))) def test_only_component_in_context(self): - self.set_parent_template("{% load laces %}{% component my_component %}") + self.set_parent_template("{% component my_component %}") self.render_parent_template_with_context({"my_component": self.component}) @@ -42,7 +43,7 @@ def test_only_component_in_context(self): self.assertRenderHTMLCalledWith({}) def test_other_variable_in_context(self): - self.set_parent_template("{% load laces %}{% component my_component %}") + self.set_parent_template("{% component my_component %}") self.render_parent_template_with_context( { @@ -56,7 +57,7 @@ def test_other_variable_in_context(self): def test_with_block_sets_extra_context(self): self.set_parent_template( """ - {% load laces %}{% with test='something' %}{% component my_component %}{% endwith %} + {% with test='something' %}{% component my_component %}{% endwith %} """ ) @@ -65,9 +66,7 @@ def test_with_block_sets_extra_context(self): self.assertRenderHTMLCalledWith({"test": "something"}) def test_with_keyword_sets_extra_context(self): - self.set_parent_template( - "{% load laces %}{% component my_component with test='something' %}" - ) + self.set_parent_template("{% component my_component with test='something' %}") self.render_parent_template_with_context({"my_component": self.component}) From 6ac1ca338e3ef12925212e894a463b3b7944db05 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 18:38:24 -0800 Subject: [PATCH 031/109] Test with-only keyword limits context --- laces/tests/test_templatetags/test_laces.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 52da22d..8ef803c 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -72,6 +72,23 @@ def test_with_keyword_sets_extra_context(self): self.assertRenderHTMLCalledWith({"test": "something"}) + def test_with_only_keyword_limits_extra_context(self): + self.set_parent_template( + "{% component my_component with test='nothing else' only %}" + ) + + self.render_parent_template_with_context( + { + "my_component": self.component, + "other": "something else", + } + ) + + # The `other` variable from the parent's rendering context is not included in + # the context that is passed to the `render_html` method. This is because of the + # `only` keyword. + self.assertRenderHTMLCalledWith({"test": "nothing else"}) + def test_passing_context_to_component(self): class MyComponent(Component): def render_html(self, parent_context): From c8d3d31b3816851da1266bd83c9d5f6882eb9a5e Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 19:58:15 -0800 Subject: [PATCH 032/109] Test with block overrides outer context --- laces/tests/test_templatetags/test_laces.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 8ef803c..bc3e95e 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -89,6 +89,25 @@ def test_with_only_keyword_limits_extra_context(self): # `only` keyword. self.assertRenderHTMLCalledWith({"test": "nothing else"}) + def test_with_block_overrides_context(self): + self.set_parent_template( + """ + {% with test='something else' %}{% component my_component %}{% endwith %} + """ + ) + + self.render_parent_template_with_context( + { + "my_component": self.component, + "test": "something", + } + ) + + self.assertRenderHTMLCalledWith({"test": "something else"}) + + # TODO: Test with keyword overrides + # TODO: Test interaction of with block and with keyword + def test_passing_context_to_component(self): class MyComponent(Component): def render_html(self, parent_context): From 260ec304ca006dede868fbe29539681951132547 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 20:01:40 -0800 Subject: [PATCH 033/109] Test with keyword overrides context --- laces/tests/test_templatetags/test_laces.py | 23 ++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index bc3e95e..5c4c1c1 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -56,9 +56,7 @@ def test_other_variable_in_context(self): def test_with_block_sets_extra_context(self): self.set_parent_template( - """ - {% with test='something' %}{% component my_component %}{% endwith %} - """ + "{% with test='something' %}{% component my_component %}{% endwith %}" ) self.render_parent_template_with_context({"my_component": self.component}) @@ -91,9 +89,21 @@ def test_with_only_keyword_limits_extra_context(self): def test_with_block_overrides_context(self): self.set_parent_template( - """ - {% with test='something else' %}{% component my_component %}{% endwith %} - """ + "{% with test='something else' %}{% component my_component %}{% endwith %}" + ) + + self.render_parent_template_with_context( + { + "my_component": self.component, + "test": "something", + } + ) + + self.assertRenderHTMLCalledWith({"test": "something else"}) + + def test_with_keyword_overrides_context(self): + self.set_parent_template( + "{% component my_component with test='something else' %}" ) self.render_parent_template_with_context( @@ -105,7 +115,6 @@ def test_with_block_overrides_context(self): self.assertRenderHTMLCalledWith({"test": "something else"}) - # TODO: Test with keyword overrides # TODO: Test interaction of with block and with keyword def test_passing_context_to_component(self): From d108d3131eda20e4ee8e51d7fd6ca6a0cdbf00f8 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 20:03:19 -0800 Subject: [PATCH 034/109] Test with keyword overrides with block --- laces/tests/test_templatetags/test_laces.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 5c4c1c1..0b93b6e 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -115,7 +115,18 @@ def test_with_keyword_overrides_context(self): self.assertRenderHTMLCalledWith({"test": "something else"}) - # TODO: Test interaction of with block and with keyword + def test_with_keyword_overrides_with_block(self): + self.set_parent_template( + """ + {% with test='something' %} + {% component my_component with test='something else' %} + {% endwith %} + """ + ) + + self.render_parent_template_with_context({"my_component": self.component}) + + self.assertRenderHTMLCalledWith({"test": "something else"}) def test_passing_context_to_component(self): class MyComponent(Component): From 3b45a65b0961e5311ea13914329c05264dca3ad8 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 20:04:59 -0800 Subject: [PATCH 035/109] Remove tests that have been replaced --- laces/tests/test_templatetags/test_laces.py | 25 --------------------- 1 file changed, 25 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 0b93b6e..caa9a9b 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -128,31 +128,6 @@ def test_with_keyword_overrides_with_block(self): self.assertRenderHTMLCalledWith({"test": "something else"}) - def test_passing_context_to_component(self): - class MyComponent(Component): - def render_html(self, parent_context): - return format_html( - "

{} was here

", parent_context.get("first_name", "nobody") - ) - - template = Template( - "{% load laces %}{% with first_name='Kilroy' %}{% component my_component %}{% endwith %}" - ) - html = template.render(Context({"my_component": MyComponent()})) - self.assertEqual(html, "

Kilroy was here

") - - template = Template( - "{% load laces %}{% component my_component with first_name='Kilroy' %}" - ) - html = template.render(Context({"my_component": MyComponent()})) - self.assertEqual(html, "

Kilroy was here

") - - template = Template( - "{% load laces %}{% with first_name='Kilroy' %}{% component my_component with surname='Silk' only %}{% endwith %}" - ) - html = template.render(Context({"my_component": MyComponent()})) - self.assertEqual(html, "

nobody was here

") - def test_fallback_render_method(self): class MyComponent(Component): def render_html(self, parent_context): From 14bf180e9e157dc7f87159530bd5d2392bd2af78 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 18 Dec 2023 20:55:15 -0800 Subject: [PATCH 036/109] Use spec on mock to be able to test alternatives paths The paths depend on an attribute being present. To be able to test that, we need objects that don't have the attribute. --- laces/tests/test_templatetags/test_laces.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index caa9a9b..699751a 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -20,7 +20,7 @@ def setUp(self): # Testing with a mock component to make it easy to check method calls and passed # arguments. Since it is the responsibility of the component to render itself, # we don't need to check the rendering here. - self.component = Mock(name="component") + self.component = Mock(name="component", spec=Component) def set_parent_template(self, template_string): template_string = "{% load laces %}" + template_string @@ -37,7 +37,6 @@ def test_only_component_in_context(self): self.render_parent_template_with_context({"my_component": self.component}) - self.assertTrue(self.component.render_html.called) # The component itself is not included in the context that is passed to the # `render_html` method. self.assertRenderHTMLCalledWith({}) From 4bedfe84e90f0cfa55da13a7e750d7d81063b0d6 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 18:21:41 -0800 Subject: [PATCH 037/109] Change mock to only mock the `render_html` method --- laces/tests/test_templatetags/test_laces.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 699751a..882a99b 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -17,10 +17,13 @@ class TestComponentTag(SimpleTestCase): def setUp(self): self.parent_template = Template("") - # Testing with a mock component to make it easy to check method calls and passed - # arguments. Since it is the responsibility of the component to render itself, - # we don't need to check the rendering here. - self.component = Mock(name="component", spec=Component) + + class ExampleComponent(Component): + pass + + self.component = ExampleComponent() + # Using a mock to be able to check if the `render_html` method is called. + self.component.render_html: Mock = Mock(return_value="") def set_parent_template(self, template_string): template_string = "{% load laces %}" + template_string @@ -37,6 +40,7 @@ def test_only_component_in_context(self): self.render_parent_template_with_context({"my_component": self.component}) + self.assertTrue(self.component.render_html.called) # The component itself is not included in the context that is passed to the # `render_html` method. self.assertRenderHTMLCalledWith({}) From b0d1b2c91b12a8c55efa1f187ee39a1eb477d089 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 18:31:37 -0800 Subject: [PATCH 038/109] Test output in parent template --- laces/tests/test_templatetags/test_laces.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 882a99b..6ef4fcc 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -23,7 +23,7 @@ class ExampleComponent(Component): self.component = ExampleComponent() # Using a mock to be able to check if the `render_html` method is called. - self.component.render_html: Mock = Mock(return_value="") + self.component.render_html: Mock = Mock(return_value="Rendered HTML") def set_parent_template(self, template_string): template_string = "{% load laces %}" + template_string @@ -35,6 +35,16 @@ def render_parent_template_with_context(self, context: dict): def assertRenderHTMLCalledWith(self, context: dict): self.assertTrue(self.component.render_html.called_with(Context(context))) + def test_output_is_rendered_html(self): + self.assertEqual(self.component.render_html(), "Rendered HTML") + self.set_parent_template("{% component my_component %}") + + result = self.render_parent_template_with_context( + {"my_component": self.component}, + ) + + self.assertEqual(result, "Rendered HTML") + def test_only_component_in_context(self): self.set_parent_template("{% component my_component %}") From cfc48e313143ef08b15f31790c32b5533218dce5 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 18:58:31 -0800 Subject: [PATCH 039/109] Test fallback render method --- laces/tests/test_templatetags/test_laces.py | 81 ++++++++++++++------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 6ef4fcc..8456706 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -35,16 +35,6 @@ def render_parent_template_with_context(self, context: dict): def assertRenderHTMLCalledWith(self, context: dict): self.assertTrue(self.component.render_html.called_with(Context(context))) - def test_output_is_rendered_html(self): - self.assertEqual(self.component.render_html(), "Rendered HTML") - self.set_parent_template("{% component my_component %}") - - result = self.render_parent_template_with_context( - {"my_component": self.component}, - ) - - self.assertEqual(result, "Rendered HTML") - def test_only_component_in_context(self): self.set_parent_template("{% component my_component %}") @@ -141,28 +131,65 @@ def test_with_keyword_overrides_with_block(self): self.assertRenderHTMLCalledWith({"test": "something else"}) - def test_fallback_render_method(self): - class MyComponent(Component): - def render_html(self, parent_context): - return format_html("

I am a component

") + def test_render_html_output(self): + self.set_parent_template("{% component my_component %}") + self.assertEqual(self.component.render_html(), "Rendered HTML") + + result = self.render_parent_template_with_context( + {"my_component": self.component}, + ) + + # This matches the return value of the `render_html` method. No other content + # is present in the parent template. + self.assertEqual(result, "Rendered HTML") - class MyNonComponent: + def test_fallback_render_method_arg_true_and_object_with_render_method(self): + # ----------------------------------------------------------------------------- + class ExampleNonComponentWithRenderMethod: def render(self): - return format_html("

I am not a component

") + return "Rendered non-component" - template = Template("{% load laces %}{% component my_component %}") - html = template.render(Context({"my_component": MyComponent()})) - self.assertEqual(html, "

I am a component

") - with self.assertRaises(ValueError): - template.render(Context({"my_component": MyNonComponent()})) + # ----------------------------------------------------------------------------- + non_component = ExampleNonComponentWithRenderMethod() + self.set_parent_template( + "{% component my_non_component fallback_render_method=True %}" + ) - template = Template( - "{% load laces %}{% component my_component fallback_render_method=True %}" + result = self.render_parent_template_with_context( + {"my_non_component": non_component}, ) - html = template.render(Context({"my_component": MyComponent()})) - self.assertEqual(html, "

I am a component

") - html = template.render(Context({"my_component": MyNonComponent()})) - self.assertEqual(html, "

I am not a component

") + + self.assertEqual(result, "Rendered non-component") + + def test_fallback_render_method_arg_true_but_object_without_render_method(self): + # ----------------------------------------------------------------------------- + class ExampleNonComponentWithoutRenderMethod: + pass + + # ----------------------------------------------------------------------------- + non_component = ExampleNonComponentWithoutRenderMethod() + self.set_parent_template( + "{% component my_non_component fallback_render_method=True %}" + ) + + with self.assertRaises(ValueError): + self.render_parent_template_with_context( + {"my_non_component": non_component}, + ) + + def test_no_fallback_render_method_arg_and_object_without_render_method(self): + # ----------------------------------------------------------------------------- + class ExampleNonComponentWithoutRenderMethod: + pass + + # ----------------------------------------------------------------------------- + non_component = ExampleNonComponentWithoutRenderMethod() + self.set_parent_template("{% component my_non_component %}") + + with self.assertRaises(ValueError): + self.render_parent_template_with_context( + {"my_non_component": non_component}, + ) def test_component_escapes_unsafe_strings(self): class MyComponent(Component): From 4746af99ba58cfb5f11b383b3522c811a5a0ad05 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 19:11:12 -0800 Subject: [PATCH 040/109] Rename test methods for clarity --- laces/tests/test_templatetags/test_laces.py | 41 +++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 8456706..1b42299 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -35,7 +35,18 @@ def render_parent_template_with_context(self, context: dict): def assertRenderHTMLCalledWith(self, context: dict): self.assertTrue(self.component.render_html.called_with(Context(context))) - def test_only_component_in_context(self): + def test_render_html_return_in_parent_template(self): + self.set_parent_template("Before {% component my_component %} After") + self.assertEqual(self.component.render_html(), "Rendered HTML") + + result = self.render_parent_template_with_context( + {"my_component": self.component}, + ) + + # This matches the return value of the `render_html` method. + self.assertEqual(result, "Before Rendered HTML After") + + def test_render_html_parent_context_when_only_component_in_context(self): self.set_parent_template("{% component my_component %}") self.render_parent_template_with_context({"my_component": self.component}) @@ -45,7 +56,7 @@ def test_only_component_in_context(self): # `render_html` method. self.assertRenderHTMLCalledWith({}) - def test_other_variable_in_context(self): + def test_render_html_parent_context_when_other_variable_in_context(self): self.set_parent_template("{% component my_component %}") self.render_parent_template_with_context( @@ -57,7 +68,7 @@ def test_other_variable_in_context(self): self.assertRenderHTMLCalledWith({"test": "something"}) - def test_with_block_sets_extra_context(self): + def test_render_html_parent_context_when_with_block_sets_extra_context(self): self.set_parent_template( "{% with test='something' %}{% component my_component %}{% endwith %}" ) @@ -66,14 +77,16 @@ def test_with_block_sets_extra_context(self): self.assertRenderHTMLCalledWith({"test": "something"}) - def test_with_keyword_sets_extra_context(self): + def test_render_html_parent_context_when_with_keyword_sets_extra_context(self): self.set_parent_template("{% component my_component with test='something' %}") self.render_parent_template_with_context({"my_component": self.component}) self.assertRenderHTMLCalledWith({"test": "something"}) - def test_with_only_keyword_limits_extra_context(self): + def test_render_html_parent_context_when_with_only_keyword_limits_extra_context( + self, + ): self.set_parent_template( "{% component my_component with test='nothing else' only %}" ) @@ -90,7 +103,7 @@ def test_with_only_keyword_limits_extra_context(self): # `only` keyword. self.assertRenderHTMLCalledWith({"test": "nothing else"}) - def test_with_block_overrides_context(self): + def test_render_html_parent_context_when_with_block_overrides_context(self): self.set_parent_template( "{% with test='something else' %}{% component my_component %}{% endwith %}" ) @@ -104,7 +117,7 @@ def test_with_block_overrides_context(self): self.assertRenderHTMLCalledWith({"test": "something else"}) - def test_with_keyword_overrides_context(self): + def test_render_html_parent_context_when_with_keyword_overrides_context(self): self.set_parent_template( "{% component my_component with test='something else' %}" ) @@ -118,7 +131,7 @@ def test_with_keyword_overrides_context(self): self.assertRenderHTMLCalledWith({"test": "something else"}) - def test_with_keyword_overrides_with_block(self): + def test_render_html_parent_context_when_with_keyword_overrides_with_block(self): self.set_parent_template( """ {% with test='something' %} @@ -131,18 +144,6 @@ def test_with_keyword_overrides_with_block(self): self.assertRenderHTMLCalledWith({"test": "something else"}) - def test_render_html_output(self): - self.set_parent_template("{% component my_component %}") - self.assertEqual(self.component.render_html(), "Rendered HTML") - - result = self.render_parent_template_with_context( - {"my_component": self.component}, - ) - - # This matches the return value of the `render_html` method. No other content - # is present in the parent template. - self.assertEqual(result, "Rendered HTML") - def test_fallback_render_method_arg_true_and_object_with_render_method(self): # ----------------------------------------------------------------------------- class ExampleNonComponentWithRenderMethod: From 606ef2decc2d11b02b49a9bee7e00665455eb4aa Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 19:17:25 -0800 Subject: [PATCH 041/109] Refactor string escaping test --- laces/tests/test_templatetags/test_laces.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 1b42299..e558b55 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -192,15 +192,18 @@ class ExampleNonComponentWithoutRenderMethod: {"my_non_component": non_component}, ) - def test_component_escapes_unsafe_strings(self): - class MyComponent(Component): - def render_html(self, parent_context): - return "Look, I'm running with scissors! 8< 8< 8<" + def test_render_html_return_in_parent_template_is_escaped(self): + self.component.render_html.return_value = ( + "Look, I'm running with scissors! 8< 8< 8<" + ) + self.set_parent_template("{% component my__component %}") + + result = self.render_parent_template_with_context( + {"my__component": self.component}, + ) - template = Template("{% load laces %}

{% component my_component %}

") - html = template.render(Context({"my_component": MyComponent()})) self.assertEqual( - html, "

Look, I'm running with scissors! 8< 8< 8<

" + result, "Look, I'm running with scissors! 8< 8< 8<" ) def test_error_on_rendering_non_component(self): From 3f62f9d4ebae6009e9d7d069c38eff311c48d4ce Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 19:20:18 -0800 Subject: [PATCH 042/109] Relocate escaping test --- laces/tests/test_templatetags/test_laces.py | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index e558b55..fb70d01 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -46,6 +46,20 @@ def test_render_html_return_in_parent_template(self): # This matches the return value of the `render_html` method. self.assertEqual(result, "Before Rendered HTML After") + def test_render_html_return_in_parent_template_is_escaped(self): + self.component.render_html.return_value = ( + "Look, I'm running with scissors! 8< 8< 8<" + ) + self.set_parent_template("{% component my_component %}") + + result = self.render_parent_template_with_context( + {"my_component": self.component}, + ) + + self.assertEqual( + result, "Look, I'm running with scissors! 8< 8< 8<" + ) + def test_render_html_parent_context_when_only_component_in_context(self): self.set_parent_template("{% component my_component %}") @@ -192,20 +206,6 @@ class ExampleNonComponentWithoutRenderMethod: {"my_non_component": non_component}, ) - def test_render_html_return_in_parent_template_is_escaped(self): - self.component.render_html.return_value = ( - "Look, I'm running with scissors! 8< 8< 8<" - ) - self.set_parent_template("{% component my__component %}") - - result = self.render_parent_template_with_context( - {"my__component": self.component}, - ) - - self.assertEqual( - result, "Look, I'm running with scissors! 8< 8< 8<" - ) - def test_error_on_rendering_non_component(self): template = Template("{% load laces %}

{% component my_component %}

") From 0821c0ee9fd4e0b263f5d45463b5176ad87591eb Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 19:24:36 -0800 Subject: [PATCH 043/109] Incorporate error representation test in other test --- laces/tests/test_templatetags/test_laces.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index fb70d01..f0b5632 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -195,23 +195,21 @@ class ExampleNonComponentWithoutRenderMethod: def test_no_fallback_render_method_arg_and_object_without_render_method(self): # ----------------------------------------------------------------------------- class ExampleNonComponentWithoutRenderMethod: - pass + def __repr__(self): + return "" # ----------------------------------------------------------------------------- non_component = ExampleNonComponentWithoutRenderMethod() self.set_parent_template("{% component my_non_component %}") - with self.assertRaises(ValueError): + with self.assertRaises(ValueError) as cm: self.render_parent_template_with_context( {"my_non_component": non_component}, ) - - def test_error_on_rendering_non_component(self): - template = Template("{% load laces %}

{% component my_component %}

") - - with self.assertRaises(ValueError) as cm: - template.render(Context({"my_component": "hello"})) - self.assertEqual(str(cm.exception), "Cannot render 'hello' as a component") + self.assertEqual( + str(cm.exception), + "Cannot render as a component", + ) def test_render_as_var(self): class MyComponent(Component): From 1876a0f005ddcbbb1043d5aefd0bc82fbec34260 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 19:37:02 -0800 Subject: [PATCH 044/109] Refactor test for as-keyword --- laces/tests/test_templatetags/test_laces.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index f0b5632..e52c599 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -2,7 +2,6 @@ from django.template import Context, Template from django.test import SimpleTestCase -from django.utils.html import format_html from laces.components import Component @@ -57,7 +56,8 @@ def test_render_html_return_in_parent_template_is_escaped(self): ) self.assertEqual( - result, "Look, I'm running with scissors! 8< 8< 8<" + result, + "Look, I'm running with scissors! 8< 8< 8<", ) def test_render_html_parent_context_when_only_component_in_context(self): @@ -211,13 +211,13 @@ def __repr__(self): "Cannot render as a component", ) - def test_render_as_var(self): - class MyComponent(Component): - def render_html(self, parent_context): - return format_html("

I am a component

") + def test_as_keyword_stores_render_html_return_as_variable(self): + self.set_parent_template( + "{% component my_component as my_var %}The result was: {{ my_var }}" + ) - template = Template( - "{% load laces %}{% component my_component as my_html %}The result was: {{ my_html }}" + result = self.render_parent_template_with_context( + {"my_component": self.component}, ) - html = template.render(Context({"my_component": MyComponent()})) - self.assertEqual(html, "The result was:

I am a component

") + + self.assertEqual(result, "The result was: Rendered HTML") From c16491f67fc3d3246efc6e9eade364821237ee7f Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 19:43:31 -0800 Subject: [PATCH 045/109] Test return value is formatted html --- laces/tests/test_templatetags/test_laces.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index e52c599..123e4b9 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -2,6 +2,7 @@ from django.template import Context, Template from django.test import SimpleTestCase +from django.utils.html import format_html from laces.components import Component @@ -60,6 +61,16 @@ def test_render_html_return_in_parent_template_is_escaped(self): "Look, I'm running with scissors! 8< 8< 8<", ) + def test_render_html_return_not_escaped_when_formatted_html(self): + self.component.render_html.return_value = format_html("

My component

") + self.set_parent_template("{% component my_component %}") + + result = self.render_parent_template_with_context( + {"my_component": self.component}, + ) + + self.assertEqual(result, "

My component

") + def test_render_html_parent_context_when_only_component_in_context(self): self.set_parent_template("{% component my_component %}") From 5488ed7da3543811f893736db33081fd0f3892cd Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 19:51:42 -0800 Subject: [PATCH 046/109] Test render_html return not escaped when real template --- laces/tests/test_templatetags/test_laces.py | 30 ++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 123e4b9..6778369 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -1,5 +1,10 @@ +import os +import random + +from pathlib import Path from unittest.mock import Mock +from django.conf import settings from django.template import Context, Template from django.test import SimpleTestCase from django.utils.html import format_html @@ -46,7 +51,7 @@ def test_render_html_return_in_parent_template(self): # This matches the return value of the `render_html` method. self.assertEqual(result, "Before Rendered HTML After") - def test_render_html_return_in_parent_template_is_escaped(self): + def test_render_html_return_is_escaped(self): self.component.render_html.return_value = ( "Look, I'm running with scissors! 8< 8< 8<" ) @@ -71,6 +76,29 @@ def test_render_html_return_not_escaped_when_formatted_html(self): self.assertEqual(result, "

My component

") + def test_render_html_return_not_escaped_when_actually_rendered_template(self): + example_template_name = f"example-{random.randint(1000, 10000)}.html" + example_template = ( + Path(settings.PROJECT_DIR) / "templates" / example_template_name + ) + with open(example_template, "w") as f: + f.write("

My component

") + + # ----------------------------------------------------------------------------- + class RealExampleComponent(Component): + template_name = example_template_name + + # ----------------------------------------------------------------------------- + component = RealExampleComponent() + self.set_parent_template("{% component my_component %}") + + result = self.render_parent_template_with_context( + {"my_component": component}, + ) + + self.assertEqual(result, "

My component

") + os.remove(example_template) + def test_render_html_parent_context_when_only_component_in_context(self): self.set_parent_template("{% component my_component %}") From 08a09286f8f9e18dbf403b3bf09eb97ff72d121f Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 20:01:06 -0800 Subject: [PATCH 047/109] Test ability to disable autoescaping --- laces/tests/test_templatetags/test_laces.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 6778369..cb27f5d 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -260,3 +260,20 @@ def test_as_keyword_stores_render_html_return_as_variable(self): ) self.assertEqual(result, "The result was: Rendered HTML") + + def test_autoescape_off_block_can_disable_escaping_of_render_html_return(self): + self.component.render_html.return_value = ( + "Look, I'm running with scissors! 8< 8< 8<" + ) + self.set_parent_template( + "{% autoescape off %}{% component my_component %}{% endautoescape %}" + ) + + result = self.render_parent_template_with_context( + {"my_component": self.component}, + ) + + self.assertEqual( + result, + "Look, I'm running with scissors! 8< 8< 8<", + ) From 85f6d86b76e117272c26f7bf759a7311bc919091 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 20:19:44 -0800 Subject: [PATCH 048/109] Update docstring of ComponentNode.render --- laces/templatetags/laces.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/laces/templatetags/laces.py b/laces/templatetags/laces.py index 761960f..55703ed 100644 --- a/laces/templatetags/laces.py +++ b/laces/templatetags/laces.py @@ -29,12 +29,28 @@ def __init__( self.target_var = target_var def render(self, context: template.Context) -> str: - # Render a component by calling its render_html method, passing request and context from the - # calling template. - # If fallback_render_method is true, objects without a render_html method will have render() - # called instead (with no arguments) - this is to provide deprecation path for things that have - # been newly upgraded to use the component pattern. + """ + Render the ComponentNode template node. + The rendering is done by rendering the passed component by calling its + `render_html` method and passing context from the calling template. + + If the passed object does not have a `render_html` method but a `render` method + and the `fallback_render_method` arguments of the template tag is true, then + the `render` method is used. The `render` method does not receive any arguments. + + Additional context variables can be passed to the component by using the `with` + keyword. The `with` keyword accepts a list of key-value pairs. The key is the + name of the context variable and the value is the value of the context variable. + + The `only` keyword can be used to isolate the context. This means that the + context variables from the parent context are not passed to the component the + only context variables passed to the component are the ones passed with the + `with` keyword. + + The `as` keyword can be used to store the rendered component in a variable + in the parent context. The variable name is passed after the `as` keyword. + """ component = self.component.resolve(context) if self.fallback_render_method: From 279c5edcd876badf2f8813bd97dfbd7b9d29f4ab Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Wed, 20 Dec 2023 20:35:19 -0800 Subject: [PATCH 049/109] Install coverage --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9cf8bb1..cb30427 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,8 @@ dependencies = [ [project.optional-dependencies] testing = [ "dj-database-url==2.1.0", - "pre-commit==3.4.0" + "pre-commit==3.4.0", + "coverage==7.3.4", ] ci = [ "tox==4.11.3", From 83c9dbd9c43bd16973babfcc47a50bc7f381eadb Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 21 Dec 2023 20:22:17 -0800 Subject: [PATCH 050/109] Add basic coverage instructions to README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 48c7ebf..8d4a7b4 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,20 @@ $ tox -e interactive You can now visit `http://localhost:8020/`. +#### Testing with coverage + +To run tests with coverage, use: + +```sh +$ coverage run ./testmanage.py test +``` + +Then see the results with + +```sh +$ coverage report +``` + ### Python version management Tox will attempt to find installed Python versions on your machine. From cadde9407f1ef8248657ee5709ad530db875fae5 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 21 Dec 2023 20:22:40 -0800 Subject: [PATCH 051/109] Add codecov steps to CI --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4404f25..07fca79 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,3 +49,7 @@ jobs: run: tox env: DB: sqlite + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 0ffe209ccac3eb3e738f6aaafaf7fcb13d80a2ce Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 21 Dec 2023 20:46:50 -0800 Subject: [PATCH 052/109] Explicitly name .coverage file --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 07fca79..d2ea306 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,3 +53,5 @@ jobs: uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: ./.coverage From d04e01d1c7666e4bca3889547ce978abbb816f0d Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 11:22:02 -0800 Subject: [PATCH 053/109] Test tag with no arguments --- laces/tests/test_templatetags/test_laces.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index cb27f5d..857af5b 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -5,7 +5,7 @@ from unittest.mock import Mock from django.conf import settings -from django.template import Context, Template +from django.template import Context, Template, TemplateSyntaxError from django.test import SimpleTestCase from django.utils.html import format_html @@ -277,3 +277,9 @@ def test_autoescape_off_block_can_disable_escaping_of_render_html_return(self): result, "Look, I'm running with scissors! 8< 8< 8<", ) + + def test_no_arguments(self): + with self.assertRaises(TemplateSyntaxError): + # The template is already parsed when the parent template is set. This is + # the moment where the parsing error is raised. + self.set_parent_template("{% component %}") From 8ed41d098ee757e7d52ecd6ecd77274eecee2dc0 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 11:26:00 -0800 Subject: [PATCH 054/109] Test tag parsing with unknown flag --- laces/tests/test_templatetags/test_laces.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 857af5b..8d44cd0 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -278,8 +278,14 @@ def test_autoescape_off_block_can_disable_escaping_of_render_html_return(self): "Look, I'm running with scissors! 8< 8< 8<", ) - def test_no_arguments(self): + def test_parsing_no_arguments(self): with self.assertRaises(TemplateSyntaxError): # The template is already parsed when the parent template is set. This is # the moment where the parsing error is raised. self.set_parent_template("{% component %}") + + def test_parsing_unknown_flag(self): + with self.assertRaises(TemplateSyntaxError): + # The template is already parsed when the parent template is set. This is + # the moment where the parsing error is raised. + self.set_parent_template("{% component my_component unknown_flag %}") From eea9ff232fe29510b3a53ecc53c8dd0722012a9e Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 11:31:09 -0800 Subject: [PATCH 055/109] Show missing in coverage report --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 4e1ef99..03e0b31 100644 --- a/.coveragerc +++ b/.coveragerc @@ -22,3 +22,4 @@ exclude_lines = if __name__ == .__main__.: ignore_errors = True +show_missing = True From 5a1197299e10af6479610d7abef1cd44a89ef541 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 11:35:39 -0800 Subject: [PATCH 056/109] Make assertions more accurate --- laces/tests/test_templatetags/test_laces.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 8d44cd0..b809425 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -279,13 +279,23 @@ def test_autoescape_off_block_can_disable_escaping_of_render_html_return(self): ) def test_parsing_no_arguments(self): - with self.assertRaises(TemplateSyntaxError): + with self.assertRaises(TemplateSyntaxError) as cm: # The template is already parsed when the parent template is set. This is # the moment where the parsing error is raised. self.set_parent_template("{% component %}") - def test_parsing_unknown_flag(self): - with self.assertRaises(TemplateSyntaxError): + self.assertEqual( + str(cm.exception), + "'component' tag requires at least one argument, the component object", + ) + + def test_parsing_unknown_kwarg(self): + with self.assertRaises(TemplateSyntaxError) as cm: # The template is already parsed when the parent template is set. This is # the moment where the parsing error is raised. - self.set_parent_template("{% component my_component unknown_flag %}") + self.set_parent_template("{% component my_component unknown_kwarg=True %}") + + self.assertEqual( + str(cm.exception), + "'component' tag only accepts 'fallback_render_method' as a keyword argument", + ) From 1f6d13a664f7a4d9ddae0cff706bc8d600b32789 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 11:36:37 -0800 Subject: [PATCH 057/109] Move comment above block to which it applies --- laces/tests/test_templatetags/test_laces.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index b809425..8316b70 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -279,9 +279,9 @@ def test_autoescape_off_block_can_disable_escaping_of_render_html_return(self): ) def test_parsing_no_arguments(self): + # The template is already parsed when the parent template is set. This is the + # moment where the parsing error is raised. with self.assertRaises(TemplateSyntaxError) as cm: - # The template is already parsed when the parent template is set. This is - # the moment where the parsing error is raised. self.set_parent_template("{% component %}") self.assertEqual( @@ -290,9 +290,9 @@ def test_parsing_no_arguments(self): ) def test_parsing_unknown_kwarg(self): + # The template is already parsed when the parent template is set. This is the + # moment where the parsing error is raised. with self.assertRaises(TemplateSyntaxError) as cm: - # The template is already parsed when the parent template is set. This is - # the moment where the parsing error is raised. self.set_parent_template("{% component my_component unknown_kwarg=True %}") self.assertEqual( From 122bcb250aaab1066f39ae441592a10469180fa3 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 12:18:39 -0800 Subject: [PATCH 058/109] Test `as` keyword without following variable name --- laces/tests/test_templatetags/test_laces.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 8316b70..4a74711 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -261,6 +261,17 @@ def test_as_keyword_stores_render_html_return_as_variable(self): self.assertEqual(result, "The result was: Rendered HTML") + def test_as_keyword_without_variable_name(self): + # The template is already parsed when the parent template is set. This is the + # moment where the parsing error is raised. + with self.assertRaises(TemplateSyntaxError) as cm: + self.set_parent_template("{% component my_component as %}") + + self.assertEqual( + str(cm.exception), + "'component' tag with 'as' must be followed by a variable name", + ) + def test_autoescape_off_block_can_disable_escaping_of_render_html_return(self): self.component.render_html.return_value = ( "Look, I'm running with scissors! 8< 8< 8<" From 5b69da678b505be8c6321ba085118f063eae1f94 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 12:21:33 -0800 Subject: [PATCH 059/109] Test tag with unknown bit --- laces/tests/test_templatetags/test_laces.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 4a74711..08bdc0b 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -310,3 +310,14 @@ def test_parsing_unknown_kwarg(self): str(cm.exception), "'component' tag only accepts 'fallback_render_method' as a keyword argument", ) + + def test_parsing_unknown_bit(self): + # The template is already parsed when the parent template is set. This is the + # moment where the parsing error is raised. + with self.assertRaises(TemplateSyntaxError) as cm: + self.set_parent_template("{% component my_component unknown_bit %}") + + self.assertEqual( + str(cm.exception), + "'component' tag received an unknown argument: 'unknown_bit'", + ) From 6113db54ece1b1565e0ed827ff8fac3ab1a09cf5 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 12:28:44 -0800 Subject: [PATCH 060/109] Change variable name for clarity Calling an argument that requires a name and value a "kwarg" (keyword argument), rather than "flag" is more in line with Python conventions. --- laces/templatetags/laces.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/laces/templatetags/laces.py b/laces/templatetags/laces.py index 55703ed..5c7c864 100644 --- a/laces/templatetags/laces.py +++ b/laces/templatetags/laces.py @@ -100,9 +100,9 @@ def component(parser, token): # the only valid keyword argument immediately following the component # is fallback_render_method - flags = token_kwargs(bits, parser) - fallback_render_method = flags.pop("fallback_render_method", None) - if flags: + kwargs = token_kwargs(bits, parser) + fallback_render_method = kwargs.pop("fallback_render_method", None) + if kwargs: raise template.TemplateSyntaxError( "'component' tag only accepts 'fallback_render_method' as a keyword argument" ) From f95731ed3cfc6849235103b1c0406acf9fc651a4 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 13:04:21 -0800 Subject: [PATCH 061/109] Enable combined coverage report for tox environments --- README.md | 8 ++++++++ tox.ini | 3 +++ 2 files changed, 11 insertions(+) diff --git a/README.md b/README.md index 8d4a7b4..66003fc 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,14 @@ Then see the results with $ coverage report ``` +Running the tests with tox will automatically use coverage and store the data for each environment in a separate file. +To combine the results into one overarching report use: + +```sh +$ coverage combine +$ coverage report +``` + ### Python version management Tox will attempt to find installed Python versions on your machine. diff --git a/tox.ini b/tox.ini index cfc4498..037b240 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,9 @@ deps = django4.2: Django>=4.2,<4.3 djangomain: git+https://github.com/django/django.git@main#egg=Django +setenv = + COVERAGE_FILE = .coverage.{envname} + [testenv:interactive] basepython = python3.10 From addb60b4e306a645c07f7234412a78656b924e80 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 13:09:48 -0800 Subject: [PATCH 062/109] Combine reports in GitHub Actions --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2ea306..02e576e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,6 +49,8 @@ jobs: run: tox env: DB: sqlite + - name: Combine coverage reports + run: coverage combine - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: From cb9d3019e5e905b0fc2e8a1dad9f5b76f7aef5fc Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 13:13:41 -0800 Subject: [PATCH 063/109] Revert "Combine reports in GitHub Actions" This reverts commit addb60b4e306a645c07f7234412a78656b924e80. --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02e576e..d2ea306 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,8 +49,6 @@ jobs: run: tox env: DB: sqlite - - name: Combine coverage reports - run: coverage combine - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: From 49a9ba4f63ec6db3d5591a469d628f06f98d1804 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 13:33:26 -0800 Subject: [PATCH 064/109] Convert coverage report to json for CodeCov.io --- .github/workflows/test.yml | 4 +++- .gitignore | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d2ea306..0e9ca45 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,9 +49,11 @@ jobs: run: tox env: DB: sqlite + - name: Convert coverage report to JSON + run: coverage json -o ./.coverage.json - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: - files: ./.coverage + files: ./.coverage.json diff --git a/.gitignore b/.gitignore index 4060bfb..ba4a900 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ /dist /laces.egg-info /.coverage +/.coverage.json /htmlcov /.tox /.venv From 44d2e62ef5747d07a23b48b13872ee32faa040c3 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 13:43:49 -0800 Subject: [PATCH 065/109] Move tox to testing dependencies You might want to run tox locally. Thus, it makes no sense to have it only be a dependency in the CI section of requirements. Also, in CI we run tests. So we should also install the testing dependencies. --- .github/workflows/test.yml | 2 +- pyproject.toml | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e9ca45..fd8979d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: - name: Install run: | python -m pip install --upgrade pip setuptools wheel - python -m pip install .[ci] + python -m pip install .[testing,ci] - name: Test run: tox env: diff --git a/pyproject.toml b/pyproject.toml index cb30427..1216e5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,13 +35,14 @@ testing = [ "dj-database-url==2.1.0", "pre-commit==3.4.0", "coverage==7.3.4", -] -ci = [ "tox==4.11.3", - "tox-gh-actions==3.1.3", # Allow use of pyenv for virtual environments. To enable you need to set `VIRTUALENV_DISCOVERY=pyenv` in the shell. + # This is useful to help tox find the correct python version when using pyenv. "virtualenv-pyenv==0.4.0" ] +ci = [ + "tox-gh-actions==3.1.3", +] [project.urls] Home = "https://github.com/tbrlpld/laces" From 5e28a3512ee783379c50700f16078c0b678d9451 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 14:38:28 -0800 Subject: [PATCH 066/109] Remove the parallel coverage option to enable json upload --- .coveragerc | 3 +++ .github/workflows/test.yml | 4 +--- README.md | 8 -------- pyproject.toml | 6 +++--- tox.ini | 6 ++---- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.coveragerc b/.coveragerc index 03e0b31..3259411 100644 --- a/.coveragerc +++ b/.coveragerc @@ -23,3 +23,6 @@ exclude_lines = ignore_errors = True show_missing = True + +[json] +output = .coverage.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd8979d..f343ce3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,13 +44,11 @@ jobs: - name: Install run: | python -m pip install --upgrade pip setuptools wheel - python -m pip install .[testing,ci] + python -m pip install .[ci] - name: Test run: tox env: DB: sqlite - - name: Convert coverage report to JSON - run: coverage json -o ./.coverage.json - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: diff --git a/README.md b/README.md index 66003fc..8d4a7b4 100644 --- a/README.md +++ b/README.md @@ -317,14 +317,6 @@ Then see the results with $ coverage report ``` -Running the tests with tox will automatically use coverage and store the data for each environment in a separate file. -To combine the results into one overarching report use: - -```sh -$ coverage combine -$ coverage report -``` - ### Python version management Tox will attempt to find installed Python versions on your machine. diff --git a/pyproject.toml b/pyproject.toml index 1216e5c..e94b429 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,14 +35,14 @@ testing = [ "dj-database-url==2.1.0", "pre-commit==3.4.0", "coverage==7.3.4", +] +ci = [ "tox==4.11.3", + "tox-gh-actions==3.1.3", # Allow use of pyenv for virtual environments. To enable you need to set `VIRTUALENV_DISCOVERY=pyenv` in the shell. # This is useful to help tox find the correct python version when using pyenv. "virtualenv-pyenv==0.4.0" ] -ci = [ - "tox-gh-actions==3.1.3", -] [project.urls] Home = "https://github.com/tbrlpld/laces" diff --git a/tox.ini b/tox.ini index 037b240..ee32b14 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,8 @@ DB = [testenv] install_command = pip install -e ".[testing]" -U {opts} {packages} commands = coverage run testmanage.py test --deprecation all {posargs: -v 2} +commands_post = + coverage json basepython = python3.8: python3.8 @@ -27,16 +29,12 @@ basepython = python3.11: python3.11 deps = - coverage - django3.2: Django>=3.2,<4.0 django4.0: Django>=4.0,<4.1 django4.1: Django>=4.1,<4.2 django4.2: Django>=4.2,<4.3 djangomain: git+https://github.com/django/django.git@main#egg=Django -setenv = - COVERAGE_FILE = .coverage.{envname} [testenv:interactive] basepython = python3.10 From 715fd8df43cd4215ba54b0e29e621426bc07ce91 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 14:47:22 -0800 Subject: [PATCH 067/109] Use append to combine coverage over different tox environments --- README.md | 5 +++++ tox.ini | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d4a7b4..258f7be 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,11 @@ Then see the results with $ coverage report ``` +When the tests are run with `tox`, the coverage report is combined for all environments. +This is done by using the `--apend` flag when running coverage in `tox`. +This means it will also include previous results. +To get a clean report, you can run `coverage erase` before running `tox`. + ### Python version management Tox will attempt to find installed Python versions on your machine. diff --git a/tox.ini b/tox.ini index ee32b14..684bee9 100644 --- a/tox.ini +++ b/tox.ini @@ -18,8 +18,10 @@ DB = [testenv] install_command = pip install -e ".[testing]" -U {opts} {packages} -commands = coverage run testmanage.py test --deprecation all {posargs: -v 2} +# Run coverage in append mode so that we get a combined report over all environments. This can not be combined with parallel mode. This only affects local working, because each env is run on a different runner in CI. +commands = coverage run -a testmanage.py test --deprecation all {posargs: -v 2} commands_post = + # Convert to json for CodeCov coverage json basepython = From 31795f52c5da17b323360ac890811c9ca524ea44 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 14:58:30 -0800 Subject: [PATCH 068/109] Move json conversion into tox commands --- .coveragerc | 3 --- tox.ini | 12 +++++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3259411..03e0b31 100644 --- a/.coveragerc +++ b/.coveragerc @@ -23,6 +23,3 @@ exclude_lines = ignore_errors = True show_missing = True - -[json] -output = .coverage.json diff --git a/tox.ini b/tox.ini index 684bee9..1405b37 100644 --- a/tox.ini +++ b/tox.ini @@ -18,11 +18,13 @@ DB = [testenv] install_command = pip install -e ".[testing]" -U {opts} {packages} -# Run coverage in append mode so that we get a combined report over all environments. This can not be combined with parallel mode. This only affects local working, because each env is run on a different runner in CI. -commands = coverage run -a testmanage.py test --deprecation all {posargs: -v 2} -commands_post = - # Convert to json for CodeCov - coverage json +# Run coverage in append mode so that we get a combined report over all environments. +# This can not be combined with parallel mode. +# This only affects local working, because each env is run on a different runner in CI. +# The report is converted to json to be uploaded to codecov. +commands = + coverage run -a testmanage.py test --deprecation all {posargs: -v 2} + coverage json --data-file .coverage -o .coverage.json basepython = python3.8: python3.8 From ac9f3c859f695d11138702683bc6d399ec5fea55 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 15:00:46 -0800 Subject: [PATCH 069/109] Add coverage back to explicit tox dependencies I have a hunch this is causing the fail on GH. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 1405b37..ae362c8 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,8 @@ basepython = python3.11: python3.11 deps = + coverage + django3.2: Django>=3.2,<4.0 django4.0: Django>=4.0,<4.1 django4.1: Django>=4.1,<4.2 From c25154fa6d5afa9a6791977b88f257f93d38c3c8 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 15:03:23 -0800 Subject: [PATCH 070/109] Add coverage comment --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index ae362c8..edccaf9 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,7 @@ basepython = python3.11: python3.11 deps = + # Coverage is required here (even though it's in pyproject.toml) to make it work on GitHub Actions coverage django3.2: Django>=3.2,<4.0 From 8b8cad62737364a6a8a3374bb1235f85e0ef892d Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 15:05:36 -0800 Subject: [PATCH 071/109] Add CodeCov badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 258f7be..5496b83 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![PyPI version](https://badge.fury.io/py/laces.svg)](https://badge.fury.io/py/laces) [![laces CI](https://github.com/tbrlpld/laces/actions/workflows/test.yml/badge.svg)](https://github.com/tbrlpld/laces/actions/workflows/test.yml) +[![codecov](https://codecov.io/gh/tbrlpld/laces/graph/badge.svg?token=FMHEHNVPSX)](https://codecov.io/gh/tbrlpld/laces) --- From 3d9ec897daf713cb8066a60f9f04881211f5ed89 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 28 Dec 2023 15:10:08 -0800 Subject: [PATCH 072/109] Move json conversion from commands to post commands --- tox.ini | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index edccaf9..69617c0 100644 --- a/tox.ini +++ b/tox.ini @@ -18,12 +18,14 @@ DB = [testenv] install_command = pip install -e ".[testing]" -U {opts} {packages} -# Run coverage in append mode so that we get a combined report over all environments. -# This can not be combined with parallel mode. -# This only affects local working, because each env is run on a different runner in CI. -# The report is converted to json to be uploaded to codecov. commands = + # Run coverage in append mode so that we get a combined report over all environments. + # This can not be combined with parallel mode. + # This only affects local working, because each env is run on a different runner in CI. coverage run -a testmanage.py test --deprecation all {posargs: -v 2} + +commands_post = + # The report is converted to json to be uploaded to codecov. coverage json --data-file .coverage -o .coverage.json basepython = From 9a8d3362803f1972d61395dd13c41d3c2db6da96 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sat, 30 Dec 2023 15:23:52 -0800 Subject: [PATCH 073/109] Rename test.home app to test.example app --- laces/test/{home => example}/__init__.py | 0 laces/test/{home => example}/templates/home/home.html | 0 laces/test/{home => example}/views.py | 0 laces/test/settings.py | 2 +- laces/test/urls.py | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) rename laces/test/{home => example}/__init__.py (100%) rename laces/test/{home => example}/templates/home/home.html (100%) rename laces/test/{home => example}/views.py (100%) diff --git a/laces/test/home/__init__.py b/laces/test/example/__init__.py similarity index 100% rename from laces/test/home/__init__.py rename to laces/test/example/__init__.py diff --git a/laces/test/home/templates/home/home.html b/laces/test/example/templates/home/home.html similarity index 100% rename from laces/test/home/templates/home/home.html rename to laces/test/example/templates/home/home.html diff --git a/laces/test/home/views.py b/laces/test/example/views.py similarity index 100% rename from laces/test/home/views.py rename to laces/test/example/views.py diff --git a/laces/test/settings.py b/laces/test/settings.py index b2148d7..e7e80dd 100644 --- a/laces/test/settings.py +++ b/laces/test/settings.py @@ -35,7 +35,7 @@ INSTALLED_APPS = [ "laces", "laces.test", - "laces.test.home", + "laces.test.example", "laces.test.components", "django.contrib.admin", "django.contrib.auth", diff --git a/laces/test/urls.py b/laces/test/urls.py index f586e93..ccfc981 100644 --- a/laces/test/urls.py +++ b/laces/test/urls.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.urls import path -from laces.test.home.views import home +from laces.test.example.views import home urlpatterns = [ From c2144640f51298d9effecb40013d68983843f937 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sat, 30 Dec 2023 15:30:48 -0800 Subject: [PATCH 074/109] Move example components from app to module This is just easier for the code organization to keep it in one app. --- laces/test/{components/__init__.py => example/components.py} | 0 .../templates/components/hello-name.html | 0 .../templates/components/hello-world.html | 0 laces/test/example/views.py | 2 +- laces/test/settings.py | 1 - laces/test/tests/test_components.py | 4 ++-- 6 files changed, 3 insertions(+), 4 deletions(-) rename laces/test/{components/__init__.py => example/components.py} (100%) rename laces/test/{components => example}/templates/components/hello-name.html (100%) rename laces/test/{components => example}/templates/components/hello-world.html (100%) diff --git a/laces/test/components/__init__.py b/laces/test/example/components.py similarity index 100% rename from laces/test/components/__init__.py rename to laces/test/example/components.py diff --git a/laces/test/components/templates/components/hello-name.html b/laces/test/example/templates/components/hello-name.html similarity index 100% rename from laces/test/components/templates/components/hello-name.html rename to laces/test/example/templates/components/hello-name.html diff --git a/laces/test/components/templates/components/hello-world.html b/laces/test/example/templates/components/hello-world.html similarity index 100% rename from laces/test/components/templates/components/hello-world.html rename to laces/test/example/templates/components/hello-world.html diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 7d57370..73db7d7 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -1,6 +1,6 @@ from django.shortcuts import render -from laces.test.components import ( +from laces.test.example.components import ( PassesFixedNameToContextComponent, RendersTemplateWithFixedContentComponent, ) diff --git a/laces/test/settings.py b/laces/test/settings.py index e7e80dd..343cbd3 100644 --- a/laces/test/settings.py +++ b/laces/test/settings.py @@ -36,7 +36,6 @@ "laces", "laces.test", "laces.test.example", - "laces.test.components", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 4bcb06a..e467086 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -1,9 +1,9 @@ """ Tests for a variety of different ways how components may be used. - """ +""" from django.test import SimpleTestCase -from laces.test.components import ( +from laces.test.example.components import ( PassesFixedNameToContextComponent, RendersTemplateWithFixedContentComponent, ) From afb66a647ab7109000eb45648a03aba95f6a75bc Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sat, 30 Dec 2023 15:38:31 -0800 Subject: [PATCH 075/109] Rename "home" to "kitchen sink" Because that is how we are going to use it. --- .../templates/{home/home.html => pages/kitchen-sink.html} | 0 laces/test/example/views.py | 4 ++-- laces/test/urls.py | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) rename laces/test/example/templates/{home/home.html => pages/kitchen-sink.html} (100%) diff --git a/laces/test/example/templates/home/home.html b/laces/test/example/templates/pages/kitchen-sink.html similarity index 100% rename from laces/test/example/templates/home/home.html rename to laces/test/example/templates/pages/kitchen-sink.html diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 73db7d7..e275f26 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -6,12 +6,12 @@ ) -def home(request): +def kitchen_sink(request): fixed_content = RendersTemplateWithFixedContentComponent() passes_name = PassesFixedNameToContextComponent() return render( request, - template_name="home/home.html", + template_name="pages/kitchen-sink.html", context={ "fixed_content": fixed_content, "passes_name": passes_name, diff --git a/laces/test/urls.py b/laces/test/urls.py index ccfc981..85bfc8f 100644 --- a/laces/test/urls.py +++ b/laces/test/urls.py @@ -1,10 +1,8 @@ -from django.contrib import admin from django.urls import path -from laces.test.example.views import home +from laces.test.example.views import kitchen_sink urlpatterns = [ - path("", home), - path("django-admin/", admin.site.urls), + path("", kitchen_sink), ] From 3bd4612abc64e2fc015537873652455c890d58c5 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sat, 30 Dec 2023 16:08:27 -0800 Subject: [PATCH 076/109] Make first example in README complete --- README.md | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5496b83..1224d3d 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,7 @@ That's it. ### Creating components -The preferred way to create a component is to define a subclass of `laces.components.Component` and specify a `template_name` attribute on it. -The rendered template will then be used as the component's HTML representation: +The simplest way to create a component is to define a subclass of `laces.components.Component` and specify a `template_name` attribute on it. ```python # my_app/components.py @@ -61,18 +60,48 @@ from laces.components import Component class WelcomePanel(Component): - template_name = "my_app/panels/welcome.html" + template_name = "my_app/components/welcome.html" +``` + +```html+django +{# my_app/templates/my_app/components/welcome.html #} + +

Welcome to my app!

+``` + +With the above in place, you then instantiate the component (e.g. in a view) and pass it to another template for rendering. + +```python +# my_app/views.py + +from django.shortcuts import render + +from my_app.components import WelcomePanel -my_welcome_panel = WelcomePanel() +def home(request): + welcome = WelcomePanel() # <-- Instantiates the component + return render( + request, + "my_app/home.html", + {"welcome": welcome}, # <-- Passes the component to the template + ) ``` +In the template, we `load` the `laces` tag library and use the `component` tag to render the component. + ```html+django -{# my_app/templates/my_app/panels/welcome.html #} +{# my_app/templates/my_app/home.html #} -

Welcome to my app!

+{% load laces %} +{% component welcome %} ``` +That's it! +The component template will be rendered with the context of the calling template. + +### Without a template + For simple cases that don't require a template, the `render_html` method can be overridden instead: ```python From e6d660ace2464ab70c6621f0aaed927fda27347c Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sat, 30 Dec 2023 16:09:48 -0800 Subject: [PATCH 077/109] Fix example formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1224d3d..3073d67 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ from my_app.components import WelcomePanel def welcome_page(request): - panel = (WelcomePanel(),) + panel = WelcomePanel() return render( request, From 87e44f6b14086f24bdeaae9882a6c0a40cef8495 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sat, 30 Dec 2023 16:14:56 -0800 Subject: [PATCH 078/109] Add test for kitchen sink view --- laces/test/tests/test_views.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 laces/test/tests/test_views.py diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py new file mode 100644 index 0000000..d49851d --- /dev/null +++ b/laces/test/tests/test_views.py @@ -0,0 +1,17 @@ +from http import HTTPStatus + +from django.test import RequestFactory, TestCase + +from laces.test.example.views import kitchen_sink + + +class TestKitchenSink(TestCase): + def test_get(self): + factory = RequestFactory() + request = factory.get("/") + + response = kitchen_sink(request) + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertContains(response, "

Hello World

") + self.assertContains(response, "

Hello Alice

") From 767898b7e839ad4de360910db211e2b6741053a7 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 15:55:33 -0800 Subject: [PATCH 079/109] Further clarify basic example explanations --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3073d67..ce94945 100644 --- a/README.md +++ b/README.md @@ -84,11 +84,11 @@ def home(request): return render( request, "my_app/home.html", - {"welcome": welcome}, # <-- Passes the component to the template + {"welcome": welcome}, # <-- Passes the component to the view template ) ``` -In the template, we `load` the `laces` tag library and use the `component` tag to render the component. +In the view template, we `load` the `laces` tag library and use the `component` tag to render the component. ```html+django {# my_app/templates/my_app/home.html #} @@ -98,11 +98,16 @@ In the template, we `load` the `laces` tag library and use the `component` tag t ``` That's it! -The component template will be rendered with the context of the calling template. +The component's template will be rendered right there in the view template. + +Of course, this is a very simple example and not much more useful than using a simple `include`. +We will go into some more useful use cases below. ### Without a template -For simple cases that don't require a template, the `render_html` method can be overridden instead: +Before we dig deeper into the component use cases, just a quick note that components don't have to have a template. +For simple cases that don't require a template, the `render_html` method can be overridden instead. +If the return value contains HTML, it should be marked as safe using `django.utils.html.format_html` or `django.utils.safestring.mark_safe`. ```python # my_app/components.py From 3ee0455b75a6a27f80d043740aa7e3de4f3c839a Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 16:00:51 -0800 Subject: [PATCH 080/109] Add example with overriden render_html --- laces/test/example/components.py | 7 +++++++ .../example/templates/pages/kitchen-sink.html | 3 ++- laces/test/example/views.py | 7 +++++-- laces/test/tests/test_components.py | 19 ++++++++++++++++++- laces/test/tests/test_views.py | 1 + 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/laces/test/example/components.py b/laces/test/example/components.py index 1646e64..84c0e52 100644 --- a/laces/test/example/components.py +++ b/laces/test/example/components.py @@ -1,3 +1,5 @@ +from django.utils.html import format_html + from laces.components import Component @@ -5,6 +7,11 @@ class RendersTemplateWithFixedContentComponent(Component): template_name = "components/hello-world.html" +class ReturnsFixedContentComponent(Component): + def render_html(self, parent_context=None): + return format_html("

Hello World Return

\n") + + class PassesFixedNameToContextComponent(Component): template_name = "components/hello-name.html" diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 4c79e20..c5e4bcd 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -1,3 +1,4 @@ {% load laces %} -{% component fixed_content %} +{% component fixed_content_template %} +{% component fixed_content_return %} {% component passes_name %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index e275f26..97e6c39 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -3,17 +3,20 @@ from laces.test.example.components import ( PassesFixedNameToContextComponent, RendersTemplateWithFixedContentComponent, + ReturnsFixedContentComponent, ) def kitchen_sink(request): - fixed_content = RendersTemplateWithFixedContentComponent() + fixed_content_template = RendersTemplateWithFixedContentComponent() + fixed_content_return = ReturnsFixedContentComponent() passes_name = PassesFixedNameToContextComponent() return render( request, template_name="pages/kitchen-sink.html", context={ - "fixed_content": fixed_content, + "fixed_content_template": fixed_content_template, + "fixed_content_return": fixed_content_return, "passes_name": passes_name, }, ) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index e467086..5d58822 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -6,10 +6,11 @@ from laces.test.example.components import ( PassesFixedNameToContextComponent, RendersTemplateWithFixedContentComponent, + ReturnsFixedContentComponent, ) -class TestStaticTemplate(SimpleTestCase): +class TestRendersTempalteWithFixedContentComponent(SimpleTestCase): """Test that the template is rendered.""" def setUp(self): @@ -28,6 +29,22 @@ def test_render_html(self): ) +class TestReturnsFixedContentComponent(SimpleTestCase): + """Test the component that returns fixed content.""" + + def setUp(self): + self.component = ReturnsFixedContentComponent() + + def test_template_name(self): + self.assertFalse(hasattr(self.component, "template_name")) + + def test_render_html(self): + self.assertEqual( + self.component.render_html(), + "

Hello World Return

\n", + ) + + class TestPassesFixedNameToContextComponent(SimpleTestCase): """Test that the context is used to render the template.""" diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index d49851d..70d8361 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -14,4 +14,5 @@ def test_get(self): self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, "

Hello World

") + self.assertContains(response, "

Hello World Return

") self.assertContains(response, "

Hello Alice

") From 38be3bd03cd91a7d85fd01fb1a9bf82fa484715d Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 16:02:24 -0800 Subject: [PATCH 081/109] Simplify render_html override This is also closer to the example test. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce94945..288c20f 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ from laces.components import Component class WelcomePanel(Component): - def render_html(self, parent_context): - return format_html("

{}

", "Welcome to my app!") + def render_html(self, parent_context=None): + return format_html("

Welcome to my app!

") ``` ### Passing context to the template From e1baa530903516ce1e587616b10e112d4bae24f1 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 16:11:20 -0800 Subject: [PATCH 082/109] Make language in README examples more consistent --- README.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 288c20f..67408b4 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ class WelcomePanel(Component): return format_html("

Welcome to my app!

") ``` -### Passing context to the template +### Passing context to the component template The `get_context_data` method can be overridden to pass context variables to the template. As with `render_html`, this receives the context dictionary from the calling template. @@ -133,7 +133,7 @@ from laces.components import Component class WelcomePanel(Component): - template_name = "my_app/panels/welcome.html" + template_name = "my_app/components/welcome.html" def get_context_data(self, parent_context): context = super().get_context_data(parent_context) @@ -142,7 +142,7 @@ class WelcomePanel(Component): ``` ```html+django -{# my_app/templates/my_app/panels/welcome.html #} +{# my_app/templates/my_app/components/welcome.html #}

Welcome to my app, {{ username }}!

``` @@ -158,7 +158,7 @@ from laces.components import Component class WelcomePanel(Component): - template_name = "my_app/panels/welcome.html" + template_name = "my_app/components/welcome.html" class Media: css = {"all": ("my_app/css/welcome-panel.css",)} @@ -169,7 +169,7 @@ class WelcomePanel(Component): The `laces` tag library provides a `{% component %}` tag for including components on a template. This takes care of passing context variables from the calling template to the component (which would not be the case for a basic `{{ ... }}` variable tag). -For example, given the view passes an instance of `WelcomePanel` to the context of `my_app/welcome.html`. +For example, given the view passes an instance of `WelcomePanel` to the context of `my_app/home.html`. ```python # my_app/views.py @@ -179,45 +179,45 @@ from django.shortcuts import render from my_app.components import WelcomePanel -def welcome_page(request): - panel = WelcomePanel() +def home(request): + welcome = WelcomePanel() return render( request, - "my_app/welcome.html", + "my_app/home.html", { - "panel": panel, + "welcome": welcome, }, ) ``` -The template `my_app/templates/my_app/welcome.html` could render the panel as follows: +The template `my_app/templates/my_app/home.html` could render the welcome panel component as follows: ```html+django -{# my_app/templates/my_app/welcome.html #} +{# my_app/templates/my_app/home.html #} {% load laces %} -{% component panel %} +{% component welcome %} ``` You can pass additional context variables to the component using the keyword `with`: ```html+django -{% component panel with username=request.user.username %} +{% component welcome with username=request.user.username %} ``` To render the component with only the variables provided (and no others from the calling template's context), use `only`: ```html+django -{% component panel with username=request.user.username only %} +{% component welcome with username=request.user.username only %} ``` To store the component's rendered output in a variable rather than outputting it immediately, use `as` followed by the variable name: ```html+django -{% component panel as panel_html %} +{% component welcome as welcome_html %} -{{ panel_html }} +{{ welcome_html }} ``` Note that it is your template's responsibility to output any media declarations defined on the components. @@ -232,20 +232,20 @@ from django.shortcuts import render from my_app.components import WelcomePanel -def welcome_page(request): - panels = [ +def home(request): + components = [ WelcomePanel(), ] media = Media() - for panel in panels: - media += panel.media + for component in components: + media += component.media render( request, - "my_app/welcome.html", + "my_app/home.html", { - "panels": panels, + "components": components, "media": media, }, ) @@ -253,7 +253,7 @@ def welcome_page(request): ```html+django -{# my_app/templates/my_app/welcome.html #} +{# my_app/templates/my_app/home.html #} {% load laces %} @@ -262,8 +262,8 @@ def welcome_page(request): {{ media.css }} - {% for panel in panels %} - {% component panel %} + {% for comp in components %} + {% component comp %} {% endfor %} ``` From 0e41bce67837cbd160da4250dd60ab78ccc51529 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 16:28:08 -0800 Subject: [PATCH 083/109] Add example passing instance attribute to context --- laces/test/example/components.py | 11 ++++++++ .../example/templates/pages/kitchen-sink.html | 3 ++- laces/test/example/views.py | 7 +++-- laces/test/tests/test_components.py | 26 +++++++++++++++++++ laces/test/tests/test_views.py | 1 + 5 files changed, 45 insertions(+), 3 deletions(-) diff --git a/laces/test/example/components.py b/laces/test/example/components.py index 84c0e52..d19145c 100644 --- a/laces/test/example/components.py +++ b/laces/test/example/components.py @@ -17,3 +17,14 @@ class PassesFixedNameToContextComponent(Component): def get_context_data(self, parent_context=None): return {"name": "Alice"} + + +class PassesInstanceAttributeToContextComponent(Component): + template_name = "components/hello-name.html" + + def __init__(self, name, **kwargs): + super().__init__(**kwargs) + self.name = name + + def get_context_data(self, parent_context=None): + return {"name": self.name} diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index c5e4bcd..53ff0f9 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -1,4 +1,5 @@ {% load laces %} {% component fixed_content_template %} {% component fixed_content_return %} -{% component passes_name %} +{% component passes_fixed_name %} +{% component passes_instance_attr_name %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 97e6c39..60a5474 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -2,6 +2,7 @@ from laces.test.example.components import ( PassesFixedNameToContextComponent, + PassesInstanceAttributeToContextComponent, RendersTemplateWithFixedContentComponent, ReturnsFixedContentComponent, ) @@ -10,13 +11,15 @@ def kitchen_sink(request): fixed_content_template = RendersTemplateWithFixedContentComponent() fixed_content_return = ReturnsFixedContentComponent() - passes_name = PassesFixedNameToContextComponent() + passes_fixed_name = PassesFixedNameToContextComponent() + passes_instance_attr_name = PassesInstanceAttributeToContextComponent(name="Bob") return render( request, template_name="pages/kitchen-sink.html", context={ "fixed_content_template": fixed_content_template, "fixed_content_return": fixed_content_return, - "passes_name": passes_name, + "passes_fixed_name": passes_fixed_name, + "passes_instance_attr_name": passes_instance_attr_name, }, ) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 5d58822..04e7e6a 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -5,6 +5,7 @@ from laces.test.example.components import ( PassesFixedNameToContextComponent, + PassesInstanceAttributeToContextComponent, RendersTemplateWithFixedContentComponent, ReturnsFixedContentComponent, ) @@ -68,3 +69,28 @@ def test_render_html(self): self.component.render_html(), "

Hello Alice

\n", ) + + +class TestPassesInstanceAttributeToContextComponent(SimpleTestCase): + """Test that the context is used to render the template.""" + + def setUp(self): + self.component = PassesInstanceAttributeToContextComponent(name="Bob") + + def test_template_name(self): + self.assertEqual( + self.component.template_name, + "components/hello-name.html", + ) + + def test_get_context_data(self): + self.assertEqual( + self.component.get_context_data(), + {"name": "Bob"}, + ) + + def test_render_html(self): + self.assertEqual( + self.component.render_html(), + "

Hello Bob

\n", + ) diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 70d8361..25a6c22 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -16,3 +16,4 @@ def test_get(self): self.assertContains(response, "

Hello World

") self.assertContains(response, "

Hello World Return

") self.assertContains(response, "

Hello Alice

") + self.assertContains(response, "

Hello Bob

") From dd0cb04f349b0594c65b93a47b0e66f457e41f26 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 16:37:27 -0800 Subject: [PATCH 084/109] Test dataclass as dict as context --- laces/test/example/components.py | 12 +++++++++ .../example/templates/pages/kitchen-sink.html | 1 + laces/test/example/views.py | 4 +++ laces/test/tests/test_components.py | 26 +++++++++++++++++-- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/laces/test/example/components.py b/laces/test/example/components.py index d19145c..b51d62b 100644 --- a/laces/test/example/components.py +++ b/laces/test/example/components.py @@ -1,3 +1,5 @@ +from dataclasses import asdict, dataclass + from django.utils.html import format_html from laces.components import Component @@ -28,3 +30,13 @@ def __init__(self, name, **kwargs): def get_context_data(self, parent_context=None): return {"name": self.name} + + +@dataclass +class DataclassAsDictToContextComponent(Component): + template_name = "components/hello-name.html" + + name: str + + def get_context_data(self, parent_context=None): + return asdict(self) diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 53ff0f9..6be2e69 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -3,3 +3,4 @@ {% component fixed_content_return %} {% component passes_fixed_name %} {% component passes_instance_attr_name %} +{% component dataclass_attr_name %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 60a5474..09afe9b 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -1,6 +1,7 @@ from django.shortcuts import render from laces.test.example.components import ( + DataclassAsDictToContextComponent, PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, RendersTemplateWithFixedContentComponent, @@ -13,6 +14,8 @@ def kitchen_sink(request): fixed_content_return = ReturnsFixedContentComponent() passes_fixed_name = PassesFixedNameToContextComponent() passes_instance_attr_name = PassesInstanceAttributeToContextComponent(name="Bob") + dataclass_attr_name = DataclassAsDictToContextComponent(name="Charlie") + return render( request, template_name="pages/kitchen-sink.html", @@ -21,5 +24,6 @@ def kitchen_sink(request): "fixed_content_return": fixed_content_return, "passes_fixed_name": passes_fixed_name, "passes_instance_attr_name": passes_instance_attr_name, + "dataclass_attr_name": dataclass_attr_name, }, ) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 04e7e6a..5565b82 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -4,6 +4,7 @@ from django.test import SimpleTestCase from laces.test.example.components import ( + DataclassAsDictToContextComponent, PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, RendersTemplateWithFixedContentComponent, @@ -72,8 +73,6 @@ def test_render_html(self): class TestPassesInstanceAttributeToContextComponent(SimpleTestCase): - """Test that the context is used to render the template.""" - def setUp(self): self.component = PassesInstanceAttributeToContextComponent(name="Bob") @@ -94,3 +93,26 @@ def test_render_html(self): self.component.render_html(), "

Hello Bob

\n", ) + + +class TestDataclassComponent(SimpleTestCase): + def setUp(self): + self.component = DataclassAsDictToContextComponent(name="Charlie") + + def test_template_name(self): + self.assertEqual( + self.component.template_name, + "components/hello-name.html", + ) + + def test_get_context_data(self): + self.assertEqual( + self.component.get_context_data(), + {"name": "Charlie"}, + ) + + def test_render_html(self): + self.assertEqual( + self.component.render_html(), + "

Hello Charlie

\n", + ) From efc293fc56344ac5576cf5ac29b9e8928db3c8fd Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 16:40:46 -0800 Subject: [PATCH 085/109] Make template proper html doc --- .../example/templates/pages/kitchen-sink.html | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 6be2e69..012dbf1 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -1,6 +1,14 @@ {% load laces %} -{% component fixed_content_template %} -{% component fixed_content_return %} -{% component passes_fixed_name %} -{% component passes_instance_attr_name %} -{% component dataclass_attr_name %} + + + + Kitchen Sink + + + {% component fixed_content_template %} + {% component fixed_content_return %} + {% component passes_fixed_name %} + {% component passes_instance_attr_name %} + {% component dataclass_attr_name %} + + From 1cebc37bb85e32aee7708698d232c73b16eb2651 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 17:02:29 -0800 Subject: [PATCH 086/109] Example passing name from parent context --- laces/test/example/components.py | 9 +++++- .../example/templates/pages/kitchen-sink.html | 1 + laces/test/example/views.py | 8 +++-- laces/test/tests/test_components.py | 30 +++++++++++++++++-- laces/test/tests/test_views.py | 2 ++ 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/laces/test/example/components.py b/laces/test/example/components.py index b51d62b..bc8aa9e 100644 --- a/laces/test/example/components.py +++ b/laces/test/example/components.py @@ -33,10 +33,17 @@ def get_context_data(self, parent_context=None): @dataclass -class DataclassAsDictToContextComponent(Component): +class DataclassAsDictContextComponent(Component): template_name = "components/hello-name.html" name: str def get_context_data(self, parent_context=None): return asdict(self) + + +class PassesNameFromParentContextComponent(Component): + template_name = "components/hello-name.html" + + def get_context_data(self, parent_context): + return {"name": parent_context["name"]} diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 012dbf1..f52cedd 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -10,5 +10,6 @@ {% component passes_fixed_name %} {% component passes_instance_attr_name %} {% component dataclass_attr_name %} + {% component passed_name_from_parent_context %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 09afe9b..e5d1a37 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -1,9 +1,10 @@ from django.shortcuts import render from laces.test.example.components import ( - DataclassAsDictToContextComponent, + DataclassAsDictContextComponent, PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, + PassesNameFromParentContextComponent, RendersTemplateWithFixedContentComponent, ReturnsFixedContentComponent, ) @@ -14,7 +15,8 @@ def kitchen_sink(request): fixed_content_return = ReturnsFixedContentComponent() passes_fixed_name = PassesFixedNameToContextComponent() passes_instance_attr_name = PassesInstanceAttributeToContextComponent(name="Bob") - dataclass_attr_name = DataclassAsDictToContextComponent(name="Charlie") + dataclass_attr_name = DataclassAsDictContextComponent(name="Charlie") + passed_name_from_parent_context = PassesNameFromParentContextComponent() return render( request, @@ -25,5 +27,7 @@ def kitchen_sink(request): "passes_fixed_name": passes_fixed_name, "passes_instance_attr_name": passes_instance_attr_name, "dataclass_attr_name": dataclass_attr_name, + "passed_name_from_parent_context": passed_name_from_parent_context, + "name": "Dan", }, ) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 5565b82..1434c87 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -4,9 +4,10 @@ from django.test import SimpleTestCase from laces.test.example.components import ( - DataclassAsDictToContextComponent, + DataclassAsDictContextComponent, PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, + PassesNameFromParentContextComponent, RendersTemplateWithFixedContentComponent, ReturnsFixedContentComponent, ) @@ -95,9 +96,9 @@ def test_render_html(self): ) -class TestDataclassComponent(SimpleTestCase): +class TestDataclassAsDictContextComponent(SimpleTestCase): def setUp(self): - self.component = DataclassAsDictToContextComponent(name="Charlie") + self.component = DataclassAsDictContextComponent(name="Charlie") def test_template_name(self): self.assertEqual( @@ -116,3 +117,26 @@ def test_render_html(self): self.component.render_html(), "

Hello Charlie

\n", ) + + +class TestPassesNameFromParentContextComponent(SimpleTestCase): + def setUp(self): + self.component = PassesNameFromParentContextComponent() + + def test_template_name(self): + self.assertEqual( + self.component.template_name, + "components/hello-name.html", + ) + + def test_get_context_data(self): + self.assertEqual( + self.component.get_context_data(parent_context={"name": "Dan"}), + {"name": "Dan"}, + ) + + def test_render_html(self): + self.assertEqual( + self.component.render_html(parent_context={"name": "Dan"}), + "

Hello Dan

\n", + ) diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 25a6c22..4f846b9 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -17,3 +17,5 @@ def test_get(self): self.assertContains(response, "

Hello World Return

") self.assertContains(response, "

Hello Alice

") self.assertContains(response, "

Hello Bob

") + self.assertContains(response, "

Hello Charlie

") + self.assertContains(response, "

Hello Dan

") From a83ea6132d9c566c3dbb8531399ecd5c45d64fc4 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 17:05:40 -0800 Subject: [PATCH 087/109] Add example setting name with with-block in parent --- laces/test/example/templates/pages/kitchen-sink.html | 5 ++++- laces/test/example/views.py | 4 ++-- laces/test/tests/test_views.py | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index f52cedd..14c93d5 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -10,6 +10,9 @@ {% component passes_fixed_name %} {% component passes_instance_attr_name %} {% component dataclass_attr_name %} - {% component passed_name_from_parent_context %} + {% component passes_name_from_parent_context %} + {% with name="Erin" %} + {% component passes_name_from_parent_context %} + {% endwith %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index e5d1a37..00c8136 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -16,7 +16,7 @@ def kitchen_sink(request): passes_fixed_name = PassesFixedNameToContextComponent() passes_instance_attr_name = PassesInstanceAttributeToContextComponent(name="Bob") dataclass_attr_name = DataclassAsDictContextComponent(name="Charlie") - passed_name_from_parent_context = PassesNameFromParentContextComponent() + passes_name_from_parent_context = PassesNameFromParentContextComponent() return render( request, @@ -27,7 +27,7 @@ def kitchen_sink(request): "passes_fixed_name": passes_fixed_name, "passes_instance_attr_name": passes_instance_attr_name, "dataclass_attr_name": dataclass_attr_name, - "passed_name_from_parent_context": passed_name_from_parent_context, + "passes_name_from_parent_context": passes_name_from_parent_context, "name": "Dan", }, ) diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 4f846b9..0e07d50 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -19,3 +19,4 @@ def test_get(self): self.assertContains(response, "

Hello Bob

") self.assertContains(response, "

Hello Charlie

") self.assertContains(response, "

Hello Dan

") + self.assertContains(response, "

Hello Erin

") From 381610c01c0a857ee60c039c8b2c486504012108 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 17:07:23 -0800 Subject: [PATCH 088/109] Add example setting name with with-keyword --- laces/test/example/templates/pages/kitchen-sink.html | 1 + laces/test/tests/test_views.py | 1 + 2 files changed, 2 insertions(+) diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 14c93d5..425567a 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -14,5 +14,6 @@ {% with name="Erin" %} {% component passes_name_from_parent_context %} {% endwith %} + {% component passes_name_from_parent_context with name="Faythe" %} diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 0e07d50..1633aae 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -20,3 +20,4 @@ def test_get(self): self.assertContains(response, "

Hello Charlie

") self.assertContains(response, "

Hello Dan

") self.assertContains(response, "

Hello Erin

") + self.assertContains(response, "

Hello Faythe

") From 1fea6c7634e9b7ee41118432c2fa4505886b4005 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 17:14:37 -0800 Subject: [PATCH 089/109] Fix typo --- laces/test/tests/test_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 1434c87..5d67ae1 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -13,7 +13,7 @@ ) -class TestRendersTempalteWithFixedContentComponent(SimpleTestCase): +class TestRendersTemplateWithFixedContentComponent(SimpleTestCase): """Test that the template is rendered.""" def setUp(self): From 035ae6147faa306200dc09b55ac4367635b2a9d2 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 17:16:42 -0800 Subject: [PATCH 090/109] Remove somewhat misleading example --- laces/test/example/templates/pages/kitchen-sink.html | 1 - laces/test/tests/test_views.py | 1 - 2 files changed, 2 deletions(-) diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 425567a..14c93d5 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -14,6 +14,5 @@ {% with name="Erin" %} {% component passes_name_from_parent_context %} {% endwith %} - {% component passes_name_from_parent_context with name="Faythe" %} diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 1633aae..0e07d50 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -20,4 +20,3 @@ def test_get(self): self.assertContains(response, "

Hello Charlie

") self.assertContains(response, "

Hello Dan

") self.assertContains(response, "

Hello Erin

") - self.assertContains(response, "

Hello Faythe

") From 9077da65cf2b5d1ecc2ea6f0b7dbe66530dad78e Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 18:43:55 -0800 Subject: [PATCH 091/109] Add example component with child components --- laces/test/example/components.py | 33 ++++++++++++++++ .../example/templates/components/section.html | 5 +++ .../example/templates/pages/kitchen-sink.html | 1 + laces/test/example/views.py | 8 ++++ laces/test/tests/test_components.py | 39 +++++++++++++++++++ laces/test/tests/test_views.py | 24 ++++++++---- 6 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 laces/test/example/templates/components/section.html diff --git a/laces/test/example/components.py b/laces/test/example/components.py index bc8aa9e..c2b0007 100644 --- a/laces/test/example/components.py +++ b/laces/test/example/components.py @@ -47,3 +47,36 @@ class PassesNameFromParentContextComponent(Component): def get_context_data(self, parent_context): return {"name": parent_context["name"]} + + +class SectionWithHeadingAndParagraphComponent(Component): + template_name = "components/section.html" + + def __init__(self, heading: "HeadingComponent", content: "ParagraphComponent"): + super().__init__() + self.heading = heading + self.content = content + + def get_context_data(self, parent_context=None): + return { + "heading": self.heading, + "content": self.content, + } + + +class HeadingComponent(Component): + def __init__(self, text: str): + super().__init__() + self.text = text + + def render_html(self, parent_context=None): + return format_html("

{}

\n", self.text) + + +class ParagraphComponent(Component): + def __init__(self, text: str): + super().__init__() + self.text = text + + def render_html(self, parent_context=None): + return format_html("

{}

\n", self.text) diff --git a/laces/test/example/templates/components/section.html b/laces/test/example/templates/components/section.html new file mode 100644 index 0000000..641fc96 --- /dev/null +++ b/laces/test/example/templates/components/section.html @@ -0,0 +1,5 @@ +{% load laces %} +
+ {% component heading %} + {% component content %} +
diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 14c93d5..95d311c 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -14,5 +14,6 @@ {% with name="Erin" %} {% component passes_name_from_parent_context %} {% endwith %} + {% component section_with_heading_and_paragraph %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 00c8136..2cbe64d 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -2,11 +2,14 @@ from laces.test.example.components import ( DataclassAsDictContextComponent, + HeadingComponent, + ParagraphComponent, PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, PassesNameFromParentContextComponent, RendersTemplateWithFixedContentComponent, ReturnsFixedContentComponent, + SectionWithHeadingAndParagraphComponent, ) @@ -17,6 +20,10 @@ def kitchen_sink(request): passes_instance_attr_name = PassesInstanceAttributeToContextComponent(name="Bob") dataclass_attr_name = DataclassAsDictContextComponent(name="Charlie") passes_name_from_parent_context = PassesNameFromParentContextComponent() + section_with_heading_and_paragraph = SectionWithHeadingAndParagraphComponent( + heading=HeadingComponent(text="Hello"), + content=ParagraphComponent(text="World"), + ) return render( request, @@ -29,5 +36,6 @@ def kitchen_sink(request): "dataclass_attr_name": dataclass_attr_name, "passes_name_from_parent_context": passes_name_from_parent_context, "name": "Dan", + "section_with_heading_and_paragraph": section_with_heading_and_paragraph, }, ) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 5d67ae1..c0c47fc 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -5,11 +5,14 @@ from laces.test.example.components import ( DataclassAsDictContextComponent, + HeadingComponent, + ParagraphComponent, PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, PassesNameFromParentContextComponent, RendersTemplateWithFixedContentComponent, ReturnsFixedContentComponent, + SectionWithHeadingAndParagraphComponent, ) @@ -140,3 +143,39 @@ def test_render_html(self): self.component.render_html(parent_context={"name": "Dan"}), "

Hello Dan

\n", ) + + +class TestSectionWithHeadingAndParagraphComponent(SimpleTestCase): + def setUp(self): + self.heading = HeadingComponent(text="Heading") + self.content = ParagraphComponent(text="Paragraph") + self.component = SectionWithHeadingAndParagraphComponent( + heading=self.heading, + content=self.content, + ) + + def test_template_name(self): + self.assertEqual( + self.component.template_name, + "components/section.html", + ) + + def test_get_context_data(self): + self.assertEqual( + self.component.get_context_data(), + { + "heading": self.heading, + "content": self.content, + }, + ) + + def test_render_html(self): + self.assertHTMLEqual( + self.component.render_html(), + """ +
+

Heading

+

Paragraph

+
+ """, + ) diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 0e07d50..25d74c5 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -13,10 +13,20 @@ def test_get(self): response = kitchen_sink(request) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, "

Hello World

") - self.assertContains(response, "

Hello World Return

") - self.assertContains(response, "

Hello Alice

") - self.assertContains(response, "

Hello Bob

") - self.assertContains(response, "

Hello Charlie

") - self.assertContains(response, "

Hello Dan

") - self.assertContains(response, "

Hello Erin

") + response_html = response.content.decode("utf-8") + self.assertInHTML("

Hello World

", response_html) + self.assertInHTML("

Hello World Return

", response_html) + self.assertInHTML("

Hello Alice

", response_html) + self.assertInHTML("

Hello Bob

", response_html) + self.assertInHTML("

Hello Charlie

", response_html) + self.assertInHTML("

Hello Dan

", response_html) + self.assertInHTML("

Hello Erin

", response_html) + self.assertInHTML( + """ +
+

Hello

+

World

+
+ """, + response_html, + ) From 94bad4f16c7e95a1f890b3396f7a11f046f25b4b Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 19:01:48 -0800 Subject: [PATCH 092/109] Add example component with list of child components --- laces/test/example/components.py | 24 +++++++++++ .../templates/components/list-section.html | 9 ++++ .../example/templates/pages/kitchen-sink.html | 1 + laces/test/example/views.py | 11 +++++ laces/test/tests/test_components.py | 41 +++++++++++++++++++ laces/test/tests/test_views.py | 19 +++++++++ 6 files changed, 105 insertions(+) create mode 100644 laces/test/example/templates/components/list-section.html diff --git a/laces/test/example/components.py b/laces/test/example/components.py index c2b0007..e3ae0bc 100644 --- a/laces/test/example/components.py +++ b/laces/test/example/components.py @@ -80,3 +80,27 @@ def __init__(self, text: str): def render_html(self, parent_context=None): return format_html("

{}

\n", self.text) + + +class ListSectionComponent(Component): + template_name = "components/list-section.html" + + def __init__(self, heading: "HeadingComponent", items: "list[Component]"): + super().__init__() + self.heading = heading + self.items = items + + def get_context_data(self, parent_context=None): + return { + "heading": self.heading, + "items": self.items, + } + + +class BlockquoteComponent(Component): + def __init__(self, text: str): + super().__init__() + self.text = text + + def render_html(self, parent_context=None): + return format_html("
{}
\n", self.text) diff --git a/laces/test/example/templates/components/list-section.html b/laces/test/example/templates/components/list-section.html new file mode 100644 index 0000000..e48382a --- /dev/null +++ b/laces/test/example/templates/components/list-section.html @@ -0,0 +1,9 @@ +{% load laces %} +
+ {% component heading %} +
    + {% for item in items %} +
  • {% component item %}
  • + {% endfor %} +
+
diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 95d311c..8d8de56 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -15,5 +15,6 @@ {% component passes_name_from_parent_context %} {% endwith %} {% component section_with_heading_and_paragraph %} + {% component list_section %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 2cbe64d..bd0cde5 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -1,8 +1,10 @@ from django.shortcuts import render from laces.test.example.components import ( + BlockquoteComponent, DataclassAsDictContextComponent, HeadingComponent, + ListSectionComponent, ParagraphComponent, PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, @@ -24,6 +26,14 @@ def kitchen_sink(request): heading=HeadingComponent(text="Hello"), content=ParagraphComponent(text="World"), ) + list_section = ListSectionComponent( + heading=HeadingComponent(text="Heading"), + items=[ + ParagraphComponent(text="Item 1"), + BlockquoteComponent(text="Item 2"), + ParagraphComponent(text="Item 3"), + ], + ) return render( request, @@ -37,5 +47,6 @@ def kitchen_sink(request): "passes_name_from_parent_context": passes_name_from_parent_context, "name": "Dan", "section_with_heading_and_paragraph": section_with_heading_and_paragraph, + "list_section": list_section, }, ) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index c0c47fc..ee0cdea 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -6,6 +6,7 @@ from laces.test.example.components import ( DataclassAsDictContextComponent, HeadingComponent, + ListSectionComponent, ParagraphComponent, PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, @@ -179,3 +180,43 @@ def test_render_html(self): """, ) + + +class TestListSection(SimpleTestCase): + def setUp(self): + self.heading = HeadingComponent(text="Heading") + self.item = ParagraphComponent(text="Paragraph") + self.component = ListSectionComponent( + heading=self.heading, + items=[self.item], + ) + + def test_template_name(self): + self.assertEqual( + self.component.template_name, + "components/list-section.html", + ) + + def test_get_context_data(self): + self.assertEqual( + self.component.get_context_data(), + { + "heading": self.heading, + "items": [self.item], + }, + ) + + def test_render_html(self): + self.assertHTMLEqual( + self.component.render_html(), + """ +
+

Heading

+
    +
  • +

    Paragraph

    +
  • +
+
+ """, + ) diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 25d74c5..0f87f9f 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -30,3 +30,22 @@ def test_get(self): """, response_html, ) + self.assertInHTML( + """ +
+

Heading

+
    +
  • +

    Item 1

    +
  • +
  • +
    Item 2
    +
  • +
  • +

    Item 3

    +
  • +
+
+ """, + response_html, + ) From a87bfcac2b0bdcffaeddf5c8a82f40ebcaad797e Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 19:16:30 -0800 Subject: [PATCH 093/109] Example that passes self to the context --- laces/test/example/components.py | 11 +++++++++ .../templates/components/hello-self-name.html | 1 + .../example/templates/pages/kitchen-sink.html | 1 + laces/test/example/views.py | 3 +++ laces/test/tests/test_components.py | 24 +++++++++++++++++++ laces/test/tests/test_views.py | 1 + 6 files changed, 41 insertions(+) create mode 100644 laces/test/example/templates/components/hello-self-name.html diff --git a/laces/test/example/components.py b/laces/test/example/components.py index e3ae0bc..b8e1bd6 100644 --- a/laces/test/example/components.py +++ b/laces/test/example/components.py @@ -32,6 +32,17 @@ def get_context_data(self, parent_context=None): return {"name": self.name} +class PassesSelfToContextComponent(Component): + template_name = "components/hello-self-name.html" + + def __init__(self, name, **kwargs): + super().__init__(**kwargs) + self.name = name + + def get_context_data(self, parent_context=None): + return {"self": self} + + @dataclass class DataclassAsDictContextComponent(Component): template_name = "components/hello-name.html" diff --git a/laces/test/example/templates/components/hello-self-name.html b/laces/test/example/templates/components/hello-self-name.html new file mode 100644 index 0000000..4bc30e3 --- /dev/null +++ b/laces/test/example/templates/components/hello-self-name.html @@ -0,0 +1 @@ +

Hello {{ self.name }}'s self

diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 8d8de56..0ce3387 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -9,6 +9,7 @@ {% component fixed_content_return %} {% component passes_fixed_name %} {% component passes_instance_attr_name %} + {% component passes_self %} {% component dataclass_attr_name %} {% component passes_name_from_parent_context %} {% with name="Erin" %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index bd0cde5..59d4413 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -9,6 +9,7 @@ PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, PassesNameFromParentContextComponent, + PassesSelfToContextComponent, RendersTemplateWithFixedContentComponent, ReturnsFixedContentComponent, SectionWithHeadingAndParagraphComponent, @@ -20,6 +21,7 @@ def kitchen_sink(request): fixed_content_return = ReturnsFixedContentComponent() passes_fixed_name = PassesFixedNameToContextComponent() passes_instance_attr_name = PassesInstanceAttributeToContextComponent(name="Bob") + passes_self = PassesSelfToContextComponent(name="Carol") dataclass_attr_name = DataclassAsDictContextComponent(name="Charlie") passes_name_from_parent_context = PassesNameFromParentContextComponent() section_with_heading_and_paragraph = SectionWithHeadingAndParagraphComponent( @@ -43,6 +45,7 @@ def kitchen_sink(request): "fixed_content_return": fixed_content_return, "passes_fixed_name": passes_fixed_name, "passes_instance_attr_name": passes_instance_attr_name, + "passes_self": passes_self, "dataclass_attr_name": dataclass_attr_name, "passes_name_from_parent_context": passes_name_from_parent_context, "name": "Dan", diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index ee0cdea..312d3af 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -11,6 +11,7 @@ PassesFixedNameToContextComponent, PassesInstanceAttributeToContextComponent, PassesNameFromParentContextComponent, + PassesSelfToContextComponent, RendersTemplateWithFixedContentComponent, ReturnsFixedContentComponent, SectionWithHeadingAndParagraphComponent, @@ -100,6 +101,29 @@ def test_render_html(self): ) +class TestPassesSelfToContextComponent(SimpleTestCase): + def setUp(self): + self.component = PassesSelfToContextComponent(name="Carol") + + def test_template_name(self): + self.assertEqual( + self.component.template_name, + "components/hello-self-name.html", + ) + + def test_get_context_data(self): + self.assertEqual( + self.component.get_context_data(), + {"self": self.component}, + ) + + def test_render_html(self): + self.assertEqual( + self.component.render_html(), + "

Hello Carol's self

\n", + ) + + class TestDataclassAsDictContextComponent(SimpleTestCase): def setUp(self): self.component = DataclassAsDictContextComponent(name="Charlie") diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 0f87f9f..2d70c12 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -18,6 +18,7 @@ def test_get(self): self.assertInHTML("

Hello World Return

", response_html) self.assertInHTML("

Hello Alice

", response_html) self.assertInHTML("

Hello Bob

", response_html) + self.assertInHTML("

Hello Carol's self

", response_html) self.assertInHTML("

Hello Charlie

", response_html) self.assertInHTML("

Hello Dan

", response_html) self.assertInHTML("

Hello Erin

", response_html) From bd16d98e632dffe8e63713ceb2f8cc1c96cce388 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 19:27:06 -0800 Subject: [PATCH 094/109] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 67408b4..db24ffa 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,7 @@ $ coverage report ``` When the tests are run with `tox`, the coverage report is combined for all environments. -This is done by using the `--apend` flag when running coverage in `tox`. +This is done by using the `--append` flag when running coverage in `tox`. This means it will also include previous results. To get a clean report, you can run `coverage erase` before running `tox`. From cc8266a7a7b7ada314f1e00a82974e2638e90a93 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 19:29:02 -0800 Subject: [PATCH 095/109] Disabmbiguate dostring --- laces/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laces/components.py b/laces/components.py index b55ffe3..ba8f545 100644 --- a/laces/components.py +++ b/laces/components.py @@ -14,7 +14,7 @@ class Component(metaclass=MediaDefiningClass): A component uses the `MetaDefiningClass` metaclass to add a `media` property, which allows the definitions of CSS and JavaScript assets that are associated with the - component. This is practically the same as `Media` class used by Django forms. + component. This works the same as `Media` class used by Django forms. See also: https://docs.djangoproject.com/en/4.2/topics/forms/media/ """ From 86218806600c629ae2014cddbdc5fd6ba4de58c0 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 19:40:53 -0800 Subject: [PATCH 096/109] Add explaining comment --- laces/tests/test_components.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 57d0221..05b7588 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -47,6 +47,8 @@ def test_media(self): """ self.assertIsInstance(self.component.media, Media) empty_media = Media() + # We need to compare the internal dicts and lists as the `Media` class does not + # implement `__eq__`. self.assertEqual(self.component.media._css, empty_media._css) self.assertEqual(self.component.media._js, empty_media._js) From e5ba47c0886ec5e3ee21b23934c13f90cfb5b349 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 19:44:10 -0800 Subject: [PATCH 097/109] Fix typo --- laces/tests/test_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 05b7588..65e4773 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -132,7 +132,7 @@ def test_render_html_when_get_context_data_returns_None(self): why this specific check is needed. By default, the `get_context_data` method provides and empty dict. If an override wanted to `get_context_data` return `None`, it should be expected that no context data is available during - rendering. The underlying `template.render` method does not seem to case about + rendering. The underlying `template.render` method does not seem to care about `None` as the context. """ From 74d55c39994fe3655340e9fbefe45b854392b1d5 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Sun, 31 Dec 2023 19:48:11 -0800 Subject: [PATCH 098/109] Ensure test matches name --- laces/tests/test_components.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 65e4773..5fb0c6c 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -220,15 +220,17 @@ class ExampleClass: media = Media(css={"all": ["example.css"]}, js=["example.js"]) # ----------------------------------------------------------------------------- - example = ExampleClass() - self.media_container.append(example) + example_1 = ExampleClass() + example_2 = ExampleClass() + self.media_container.append(example_1) + self.media_container.append(example_2) result = self.media_container.media self.assertIsInstance(result, Media) - self.assertEqual(result._css, example.media._css) + self.assertEqual(result._css, example_1.media._css) self.assertEqual(result._css, {"all": ["example.css"]}) - self.assertEqual(result._js, example.media._js) + self.assertEqual(result._js, example_1.media._js) self.assertEqual(result._js, ["example.js"]) def test_two_members_of_different_classes(self): From bb5950e602df1388de6a27d09203eecc191f835f Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 1 Jan 2024 18:14:01 -0800 Subject: [PATCH 099/109] Add docstrings for example app modules --- laces/test/example/components.py | 6 ++++++ laces/test/example/views.py | 1 + laces/test/tests/test_components.py | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/laces/test/example/components.py b/laces/test/example/components.py index b8e1bd6..43ea303 100644 --- a/laces/test/example/components.py +++ b/laces/test/example/components.py @@ -1,3 +1,9 @@ +""" +Examples of how components might be defined. + +This is unlikely to be an exhaustive list of examples, but it should be enough to +demonstrate the basic concepts of how components work. +""" from dataclasses import asdict, dataclass from django.utils.html import format_html diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 59d4413..75dad7f 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -17,6 +17,7 @@ def kitchen_sink(request): + """Render a page with all example components.""" fixed_content_template = RendersTemplateWithFixedContentComponent() fixed_content_return = ReturnsFixedContentComponent() passes_fixed_name = PassesFixedNameToContextComponent() diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 312d3af..5876a55 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -1,5 +1,8 @@ """ -Tests for a variety of different ways how components may be used. +Tests for the example components. + +These tests are very basic and only ensure that the examples are configured as +desired. More thorough tests can be found in the `laces.tests.test_components` module. """ from django.test import SimpleTestCase From c56a2977684d894ef76f31da95716405a5279dea Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 1 Jan 2024 18:20:36 -0800 Subject: [PATCH 100/109] Remove useless test docstrings These did not really say anything anyhow. --- laces/test/tests/test_components.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index 5876a55..ba75534 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -22,8 +22,6 @@ class TestRendersTemplateWithFixedContentComponent(SimpleTestCase): - """Test that the template is rendered.""" - def setUp(self): self.component = RendersTemplateWithFixedContentComponent() @@ -41,8 +39,6 @@ def test_render_html(self): class TestReturnsFixedContentComponent(SimpleTestCase): - """Test the component that returns fixed content.""" - def setUp(self): self.component = ReturnsFixedContentComponent() @@ -57,8 +53,6 @@ def test_render_html(self): class TestPassesFixedNameToContextComponent(SimpleTestCase): - """Test that the context is used to render the template.""" - def setUp(self): self.component = PassesFixedNameToContextComponent() From 85059f03d60a17c22c2ebdacef53311fe7634069 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 1 Jan 2024 18:25:19 -0800 Subject: [PATCH 101/109] Add extra assertions for clarity --- laces/test/tests/test_components.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/laces/test/tests/test_components.py b/laces/test/tests/test_components.py index ba75534..ccb041b 100644 --- a/laces/test/tests/test_components.py +++ b/laces/test/tests/test_components.py @@ -86,6 +86,10 @@ def test_template_name(self): ) def test_get_context_data(self): + self.assertEqual( + self.component.name, + "Bob", + ) self.assertEqual( self.component.get_context_data(), {"name": "Bob"}, @@ -132,6 +136,10 @@ def test_template_name(self): ) def test_get_context_data(self): + self.assertEqual( + self.component.name, + "Charlie", + ) self.assertEqual( self.component.get_context_data(), {"name": "Charlie"}, From cbc91e980bd8f913c524a7b3ee9579c97f391033 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 1 Jan 2024 18:30:09 -0800 Subject: [PATCH 102/109] More comments and docstrings --- laces/test/example/templates/pages/kitchen-sink.html | 3 +++ laces/test/example/views.py | 2 +- laces/test/tests/test_views.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/laces/test/example/templates/pages/kitchen-sink.html b/laces/test/example/templates/pages/kitchen-sink.html index 0ce3387..a880c25 100644 --- a/laces/test/example/templates/pages/kitchen-sink.html +++ b/laces/test/example/templates/pages/kitchen-sink.html @@ -13,6 +13,9 @@ {% component dataclass_attr_name %} {% component passes_name_from_parent_context %} {% with name="Erin" %} + {% comment %} + Override parent context through the with-block. The same component receives a different parent context. + {% endcomment %} {% component passes_name_from_parent_context %} {% endwith %} {% component section_with_heading_and_paragraph %} diff --git a/laces/test/example/views.py b/laces/test/example/views.py index 75dad7f..d295c15 100644 --- a/laces/test/example/views.py +++ b/laces/test/example/views.py @@ -49,7 +49,7 @@ def kitchen_sink(request): "passes_self": passes_self, "dataclass_attr_name": dataclass_attr_name, "passes_name_from_parent_context": passes_name_from_parent_context, - "name": "Dan", + "name": "Dan", # Provide as an example of parent context. "section_with_heading_and_paragraph": section_with_heading_and_paragraph, "list_section": list_section, }, diff --git a/laces/test/tests/test_views.py b/laces/test/tests/test_views.py index 2d70c12..9adeeb4 100644 --- a/laces/test/tests/test_views.py +++ b/laces/test/tests/test_views.py @@ -1,3 +1,4 @@ +"""Tests for the example views that demonstrate the use of components.""" from http import HTTPStatus from django.test import RequestFactory, TestCase From ab408d584b6943510e392f1bd2357cc7c589cb38 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 1 Jan 2024 18:33:30 -0800 Subject: [PATCH 103/109] Fix typo and clarify comments and docstrings --- laces/tests/test_components.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index 5fb0c6c..d991730 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -20,8 +20,9 @@ def setUp(self): def test_render_html(self): """Test the `render_html` method.""" - # The default Component does not specify a `tempalte_name` attribute which is - # required for `render_html`. + # The default Component does not specify a `template_name` attribute which is + # required for `render_html`. So calling the method on the Component class + # will raise an error. with self.assertRaises(AttributeError): self.component.render_html() @@ -30,7 +31,7 @@ def test_get_context_data_parent_context_empty_context(self): Test the default get_context_data. The parent context should not matter, but we use it as it is used in - `render_html`. + `render_html` (which passes a `Context` object). """ result = self.component.get_context_data(parent_context=Context()) @@ -59,7 +60,8 @@ class TestComponentSubclasses(SimpleTestCase): Most functionality of the Component class is only unlocked through subclassing and definition of certain attributes (like `template_name`) or overriding of the - existing methods. + existing methods. This test class tests the functionality that is unlocked through + subclassing. """ @classmethod From c55aeb3a67c2b65b226cc78e8919c4fe6aff3040 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 1 Jan 2024 18:37:03 -0800 Subject: [PATCH 104/109] Clarify test docstring --- laces/tests/test_components.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/laces/tests/test_components.py b/laces/tests/test_components.py index d991730..0fd10cc 100644 --- a/laces/tests/test_components.py +++ b/laces/tests/test_components.py @@ -89,7 +89,7 @@ def set_example_template_content(self, content: str): def test_render_html_with_template_name_set(self): """ - Test the `render_html` with a set `template_name` attribute. + Test `render_html` method with a set `template_name` attribute. """ # ----------------------------------------------------------------------------- @@ -108,7 +108,8 @@ class ExampleComponent(Component): def test_render_html_with_template_name_set_and_data_from_get_context_data(self): """ - Test the `render_html` with `get_context_data` providing data for the context. + Test `render_html` method with `get_context_data` providing data for the + context. """ # ----------------------------------------------------------------------------- @@ -128,14 +129,15 @@ def get_context_data(self, parent_context): def test_render_html_when_get_context_data_returns_None(self): """ - Test the `render_html` method when `get_context_data` returns `None`. - - This behavior was present when the class was extracted. It is not totally clear - why this specific check is needed. By default, the `get_context_data` method - provides and empty dict. If an override wanted to `get_context_data` return - `None`, it should be expected that no context data is available during - rendering. The underlying `template.render` method does not seem to care about - `None` as the context. + Test `render_html` method when `get_context_data` returns `None`. + + The `render_html` method raises a `TypeError` when `None` is returned from + `get_context_method`. This behavior was present when the class was extracted + from Wagtail. It is not totally clear why this specific check is needed. By + default, the `get_context_data` method provides and empty dict. If an override + wanted to `get_context_data` return `None`, it should be expected that no + context data is available during rendering. The underlying `template.render` + method does not seem to be ok with `None` the provided context. """ # ----------------------------------------------------------------------------- From fd514a36932bd5622142528995aca0e7bc7b53cf Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 4 Jan 2024 18:55:49 -0800 Subject: [PATCH 105/109] Swap statements for better clarity and consistency with other tests --- laces/tests/test_templatetags/test_laces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 08bdc0b..1e18770 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -41,8 +41,8 @@ def assertRenderHTMLCalledWith(self, context: dict): self.assertTrue(self.component.render_html.called_with(Context(context))) def test_render_html_return_in_parent_template(self): - self.set_parent_template("Before {% component my_component %} After") self.assertEqual(self.component.render_html(), "Rendered HTML") + self.set_parent_template("Before {% component my_component %} After") result = self.render_parent_template_with_context( {"my_component": self.component}, From 93326a3f08b75ed872f6951188ed5b72c643921f Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 4 Jan 2024 18:57:10 -0800 Subject: [PATCH 106/109] Clarify comment --- laces/tests/test_templatetags/test_laces.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index 1e18770..fa67f22 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -48,7 +48,8 @@ def test_render_html_return_in_parent_template(self): {"my_component": self.component}, ) - # This matches the return value of the `render_html` method. + # This matches the return value of the `render_html` method inserted into the + # parent template. self.assertEqual(result, "Before Rendered HTML After") def test_render_html_return_is_escaped(self): From 3948d0d843a99eef20b8108ce65909cdeee74dfa Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 4 Jan 2024 19:02:27 -0800 Subject: [PATCH 107/109] Clarify comment --- laces/tests/test_templatetags/test_laces.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/laces/tests/test_templatetags/test_laces.py b/laces/tests/test_templatetags/test_laces.py index fa67f22..46c58f8 100644 --- a/laces/tests/test_templatetags/test_laces.py +++ b/laces/tests/test_templatetags/test_laces.py @@ -153,8 +153,9 @@ def test_render_html_parent_context_when_with_only_keyword_limits_extra_context( ) # The `other` variable from the parent's rendering context is not included in - # the context that is passed to the `render_html` method. This is because of the - # `only` keyword. + # the context that is passed to the `render_html` method. The `test` variable, + # that was defined with the with-keyword, is present though. Both of these + # effects come form the `only` keyword. self.assertRenderHTMLCalledWith({"test": "nothing else"}) def test_render_html_parent_context_when_with_block_overrides_context(self): From c1f96ff856aeba65077b148bdaa2719ddfa61992 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 4 Jan 2024 19:10:49 -0800 Subject: [PATCH 108/109] Remove extra blank line --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 69617c0..2049123 100644 --- a/tox.ini +++ b/tox.ini @@ -44,7 +44,6 @@ deps = django4.2: Django>=4.2,<4.3 djangomain: git+https://github.com/django/django.git@main#egg=Django - [testenv:interactive] basepython = python3.10 From 221ecff6648efa79a7f5817411fc59e2e6a88906 Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Thu, 4 Jan 2024 19:58:35 -0800 Subject: [PATCH 109/109] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cff896..ae4d1f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- ... +- Add more tests and example usage. ### Changed