From b13280c60831e6b6b4df0bd4b86533a4a7a8c8f6 Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sat, 6 Jul 2024 08:56:18 +0100 Subject: [PATCH 1/6] converts hyphen var names to uderscores for access --- django_cotton/cotton_loader.py | 14 +++- django_cotton/templatetags/_component.py | 20 +++--- django_cotton/templatetags/_vars_frame.py | 24 ++++--- django_cotton/tests/inline_test_case.py | 14 ++-- django_cotton/tests/test_cotton.py | 80 +++++++++++++++++------ 5 files changed, 107 insertions(+), 45 deletions(-) diff --git a/django_cotton/cotton_loader.py b/django_cotton/cotton_loader.py index 62f112f..c29c6e7 100755 --- a/django_cotton/cotton_loader.py +++ b/django_cotton/cotton_loader.py @@ -83,6 +83,8 @@ def get_template_sources(self, template_name): class UnsortedAttributes(HTMLFormatter): + """This keeps BS4 from re-ordering attributes""" + def attributes(self, tag): for k, v in tag.attrs.items(): yield k, v @@ -181,16 +183,22 @@ def _wrap_with_cotton_vars_frame(self, soup, cvars_el): vars_with_defaults = [] for var, value in cvars_el.attrs.items(): + # Attributes in context at this point will already have been formatted in _component to be accessible, so in order to cascade match the style. + accessible_var = var.replace("-", "_") + if value is None: - vars_with_defaults.append(f"{var}={var}") + vars_with_defaults.append(f"{var}={accessible_var}") elif var.startswith(":"): # If ':' is present, the user wants to parse a literal string as the default value, # i.e. "['a', 'b']", "{'a': 'b'}", "True", "False", "None" or "1". var = var[1:] # Remove the ':' prefix - vars_with_defaults.append(f'{var}={var}|eval_default:"{value}"') + accessible_var = accessible_var[1:] # Remove the ':' prefix + vars_with_defaults.append( + f'{var}={accessible_var}|eval_default:"{value}"' + ) else: # Assuming value is already a string that represents the default value - vars_with_defaults.append(f'{var}={var}|default:"{value}"') + vars_with_defaults.append(f'{var}={accessible_var}|default:"{value}"') cvars_el.decompose() diff --git a/django_cotton/templatetags/_component.py b/django_cotton/templatetags/_component.py index 23c11e9..28ca965 100644 --- a/django_cotton/templatetags/_component.py +++ b/django_cotton/templatetags/_component.py @@ -59,26 +59,28 @@ def render(self, context): all_slots = context.get("cotton_slots", {}) component_slots = all_slots.get(self.component_key, {}) local_context.update(component_slots) - local_context.update(attrs) # We need to check if any dynamic attributes are present in the component slots and move them over to attrs if "ctn_template_expression_attrs" in component_slots: for expression_attr in component_slots["ctn_template_expression_attrs"]: attrs[expression_attr] = component_slots[expression_attr] - # Make the attrs available in the context for the vars frame - local_context["attrs_dict"] = attrs - - # Reset the component's slots in context to prevent bleeding into sibling components - all_slots[self.component_key] = {} - - # Provide all of the attrs as a string to pass to the component - local_context.update(attrs) + # Build attrs string before formatting any '-' to '_' in attr names attrs_string = " ".join( f"{key}={ensure_quoted(value)}" for key, value in attrs.items() ) local_context["attrs"] = mark_safe(attrs_string) + # Make the attrs available in the context for the vars frame, also before formatting the attr names + local_context["attrs_dict"] = attrs + + # Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }} + attrs = {key.replace("-", "_"): value for key, value in attrs.items()} + local_context.update(attrs) + + # Reset the component's slots in context to prevent bleeding into sibling components + all_slots[self.component_key] = {} + return render_to_string(self.template_path, local_context) def process_dynamic_attribute(self, value, context): diff --git a/django_cotton/templatetags/_vars_frame.py b/django_cotton/templatetags/_vars_frame.py index a4ad16d..bd03258 100644 --- a/django_cotton/templatetags/_vars_frame.py +++ b/django_cotton/templatetags/_vars_frame.py @@ -1,5 +1,4 @@ from django import template -from django.template.base import token_kwargs from django.utils.safestring import mark_safe from django_cotton.utils import ensure_quoted @@ -16,7 +15,11 @@ def cotton_vars_frame(parser, token): """ bits = token.split_contents()[1:] # Skip the tag name - tag_kwargs = token_kwargs(bits, parser) + # We dont use token_kwargs because it doesn't allow for hyphens in key names, i.e. x-data="" + tag_kwargs = {} + for bit in bits: + key, value = bit.split("=") + tag_kwargs[key] = parser.compile_filter(value) nodelist = parser.parse(("endcotton_vars_frame",)) parser.delete_first_token() @@ -45,17 +48,18 @@ def render(self, context): # Overwrite 'attrs' in the local context by excluding keys that are identified as vars attrs_without_vars = {k: v for k, v in component_attrs.items() if k not in vars} - context["attrs_dict"] = attrs_without_vars - # Provide all of the attrs as a string to pass to the component + # Provide all of the attrs as a string to pass to the component before any '-' to '_' replacing attrs = " ".join( - [ - f"{key}={ensure_quoted(value)}" - for key, value in attrs_without_vars.items() - ] + f"{key}={ensure_quoted(value)}" for key, value in attrs_without_vars.items() ) - context["attrs"] = mark_safe(attrs) - context.update(vars) + + context["attrs_dict"] = attrs_without_vars + + # Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }} + formatted_vars = {key.replace("-", "_"): value for key, value in vars.items()} + + context.update(formatted_vars) return self.nodelist.render(context) diff --git a/django_cotton/tests/inline_test_case.py b/django_cotton/tests/inline_test_case.py index 3f9d70f..73e8a58 100644 --- a/django_cotton/tests/inline_test_case.py +++ b/django_cotton/tests/inline_test_case.py @@ -22,39 +22,45 @@ class CottonInlineTestCase(TestCase): @classmethod def setUpClass(cls): super().setUpClass() + + # Set tmp dir and register a url module for our tmp files cls.temp_dir = tempfile.mkdtemp() cls.url_module = DynamicURLModule() cls.url_module_name = f"dynamic_urls_{cls.__name__}" sys.modules[cls.url_module_name] = cls.url_module - # Create a new TEMPLATES setting + # Register our temp directory as a TEMPLATES path cls.new_templates_setting = settings.TEMPLATES.copy() cls.new_templates_setting[0]["DIRS"] = [ cls.temp_dir ] + cls.new_templates_setting[0]["DIRS"] - # Apply the new setting + # Apply the setting cls.templates_override = override_settings(TEMPLATES=cls.new_templates_setting) cls.templates_override.enable() @classmethod def tearDownClass(cls): + """Remove temporary directory and clean up modules""" cls.templates_override.disable() shutil.rmtree(cls.temp_dir, ignore_errors=True) del sys.modules[cls.url_module_name] super().tearDownClass() def create_template(self, name, content): + """Create a template file in the temporary directory and return the path""" path = os.path.join(self.temp_dir, name) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as f: f.write(content) return path - def create_view(self, template_name): + def make_view(self, template_name): + """Make a view that renders the given template""" return TemplateView.as_view(template_name=template_name) - def create_url(self, url, view): + def register_url(self, url, view): + """Register a URL pattern and returns path""" url_pattern = path(url, view) self.url_module.urlpatterns.append(url_pattern) return url_pattern diff --git a/django_cotton/tests/test_cotton.py b/django_cotton/tests/test_cotton.py index d8e369f..00f37b4 100644 --- a/django_cotton/tests/test_cotton.py +++ b/django_cotton/tests/test_cotton.py @@ -8,24 +8,16 @@ class InlineTestCase(CottonInlineTestCase): def test_component_is_rendered(self): self.create_template( "cotton/component.html", - """ -
- {{ slot }} -
- """, + """
{{ slot }}
""", ) self.create_template( "view.html", - """ - - Hello, World! - - """, + """Hello, World!""", ) - # Create URL - self.create_url("view/", self.create_view("view.html")) + # Register Url + self.register_url("view/", self.make_view("view.html")) # Override URLconf with self.settings(ROOT_URLCONF=self.get_url_conf()): @@ -36,11 +28,7 @@ def test_component_is_rendered(self): def test_new_lines_in_attributes_are_preserved(self): self.create_template( "cotton/component.html", - """ -
- {{ slot }} -
- """, + """
{{ slot }}
""", ) self.create_template( @@ -56,8 +44,8 @@ def test_new_lines_in_attributes_are_preserved(self): """, ) - # Create URL - self.create_url("view/", self.create_view("view.html")) + # Register Url + self.register_url("view/", self.make_view("view.html")) # Override URLconf with self.settings(ROOT_URLCONF=self.get_url_conf()): @@ -74,6 +62,60 @@ def test_new_lines_in_attributes_are_preserved(self): in response.content.decode() ) + def test_attribute_names_on_component_containing_hyphens_are_converted_to_underscores( + self, + ): + self.create_template( + "cotton/component.html", + """ +
+ """, + ) + + self.create_template( + "view.html", + """ + + """, + ) + + # Register Url + self.register_url("view/", self.make_view("view.html")) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.get_url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, 'x-data="{}" x-init="do_something()"') + + def test_attribute_names_on_cvars_containing_hyphens_are_converted_to_underscores( + self, + ): + self.create_template( + "cotton/component.html", + """ + + +
+ """, + ) + + self.create_template( + "view.html", + """ + + """, + ) + + # Register Url + self.register_url("view/", self.make_view("view.html")) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.get_url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, 'x-data="{}" x-init="do_something()"') + class CottonTestCase(TestCase): def test_parent_component_is_rendered(self): From c31bed719c9633c4d89a8b43b984acca8e60e577 Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sat, 6 Jul 2024 09:01:05 +0100 Subject: [PATCH 2/6] fixed test --- django_cotton/tests/test_cotton.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/django_cotton/tests/test_cotton.py b/django_cotton/tests/test_cotton.py index 00f37b4..070d772 100644 --- a/django_cotton/tests/test_cotton.py +++ b/django_cotton/tests/test_cotton.py @@ -34,13 +34,8 @@ def test_new_lines_in_attributes_are_preserved(self): self.create_template( "view.html", """ - + """, ) @@ -53,12 +48,7 @@ def test_new_lines_in_attributes_are_preserved(self): self.assertTrue( """{ - attr1: 'im an attr', - var1: 'im a var', - method() { - return 'im a method'; - } - }""" +}""" in response.content.decode() ) From edeb9930ab2e8d72d79e1d01b395401170e10d12 Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sat, 6 Jul 2024 09:03:37 +0100 Subject: [PATCH 3/6] test debug --- django_cotton/tests/test_cotton.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_cotton/tests/test_cotton.py b/django_cotton/tests/test_cotton.py index 070d772..67fdc20 100644 --- a/django_cotton/tests/test_cotton.py +++ b/django_cotton/tests/test_cotton.py @@ -22,6 +22,7 @@ def test_component_is_rendered(self): # Override URLconf with self.settings(ROOT_URLCONF=self.get_url_conf()): response = self.client.get("/view/") + print(response.content.decode()) self.assertContains(response, '
') self.assertContains(response, "Hello, World!") From d4f96dff0e536e6fbe7f08d92c94e674bde7200a Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sat, 6 Jul 2024 09:08:24 +0100 Subject: [PATCH 4/6] test debug --- django_cotton/tests/test_cotton.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/django_cotton/tests/test_cotton.py b/django_cotton/tests/test_cotton.py index 67fdc20..de55e12 100644 --- a/django_cotton/tests/test_cotton.py +++ b/django_cotton/tests/test_cotton.py @@ -36,6 +36,8 @@ def test_new_lines_in_attributes_are_preserved(self): "view.html", """ """, ) @@ -48,8 +50,8 @@ def test_new_lines_in_attributes_are_preserved(self): response = self.client.get("/view/") self.assertTrue( - """{ -}""" + """test +test""" in response.content.decode() ) From c1496952c7aeed08d4ac1ee21de5634af4837236 Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sat, 6 Jul 2024 09:13:27 +0100 Subject: [PATCH 5/6] test debug --- django_cotton/tests/inline_test_case.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/django_cotton/tests/inline_test_case.py b/django_cotton/tests/inline_test_case.py index 73e8a58..4d5e13d 100644 --- a/django_cotton/tests/inline_test_case.py +++ b/django_cotton/tests/inline_test_case.py @@ -3,6 +3,7 @@ import shutil import tempfile +from django.core.cache import cache from django.urls import path from django.test import override_settings from django.views.generic import TemplateView @@ -47,6 +48,10 @@ def tearDownClass(cls): del sys.modules[cls.url_module_name] super().tearDownClass() + def tearDown(self): + """Clear cache between tests""" + cache.clear() + def create_template(self, name, content): """Create a template file in the temporary directory and return the path""" path = os.path.join(self.temp_dir, name) From d0b9f0d8df11db605ae0cdc3a083cc88d533237e Mon Sep 17 00:00:00 2001 From: Will Abbott Date: Sat, 6 Jul 2024 09:15:15 +0100 Subject: [PATCH 6/6] reverted debugs --- django_cotton/tests/inline_test_case.py | 2 +- django_cotton/tests/test_cotton.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/django_cotton/tests/inline_test_case.py b/django_cotton/tests/inline_test_case.py index 4d5e13d..5965412 100644 --- a/django_cotton/tests/inline_test_case.py +++ b/django_cotton/tests/inline_test_case.py @@ -49,7 +49,7 @@ def tearDownClass(cls): super().tearDownClass() def tearDown(self): - """Clear cache between tests""" + """Clear cache between tests so that we can use the same file names for simplicity""" cache.clear() def create_template(self, name, content): diff --git a/django_cotton/tests/test_cotton.py b/django_cotton/tests/test_cotton.py index de55e12..00f37b4 100644 --- a/django_cotton/tests/test_cotton.py +++ b/django_cotton/tests/test_cotton.py @@ -22,7 +22,6 @@ def test_component_is_rendered(self): # Override URLconf with self.settings(ROOT_URLCONF=self.get_url_conf()): response = self.client.get("/view/") - print(response.content.decode()) self.assertContains(response, '
') self.assertContains(response, "Hello, World!") @@ -35,10 +34,13 @@ def test_new_lines_in_attributes_are_preserved(self): self.create_template( "view.html", """ - + """, ) @@ -50,8 +52,13 @@ def test_new_lines_in_attributes_are_preserved(self): response = self.client.get("/view/") self.assertTrue( - """test -test""" + """{ + attr1: 'im an attr', + var1: 'im a var', + method() { + return 'im a method'; + } + }""" in response.content.decode() )