diff --git a/dev/example_project/example_project/templates/child_test.html b/dev/example_project/example_project/templates/child_test.html deleted file mode 100644 index ec03be8..0000000 --- a/dev/example_project/example_project/templates/child_test.html +++ /dev/null @@ -1,3 +0,0 @@ - - d - \ No newline at end of file diff --git a/dev/example_project/example_project/templates/cotton/benchmarks/cotton.html b/dev/example_project/example_project/templates/cotton/benchmarks/cotton_extends_equivalent.html similarity index 100% rename from dev/example_project/example_project/templates/cotton/benchmarks/cotton.html rename to dev/example_project/example_project/templates/cotton/benchmarks/cotton_extends_equivalent.html index 57996ab..e133dfa 100644 --- a/dev/example_project/example_project/templates/cotton/benchmarks/cotton.html +++ b/dev/example_project/example_project/templates/cotton/benchmarks/cotton_extends_equivalent.html @@ -1,8 +1,8 @@ - I'm default I'm top + I'm default I'm bottom diff --git a/dev/example_project/example_project/templates/cotton/child.html b/dev/example_project/example_project/templates/cotton/child.html deleted file mode 100644 index 9f989fa..0000000 --- a/dev/example_project/example_project/templates/cotton/child.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/dev/example_project/example_project/templates/cotton/named_slot_component.html b/dev/example_project/example_project/templates/cotton/named_slot_component.html deleted file mode 100644 index e324e2c..0000000 --- a/dev/example_project/example_project/templates/cotton/named_slot_component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- {{ name }} -
\ No newline at end of file diff --git a/dev/example_project/example_project/templates/cotton/parent.html b/dev/example_project/example_project/templates/cotton/parent.html deleted file mode 100644 index ccf4342..0000000 --- a/dev/example_project/example_project/templates/cotton/parent.html +++ /dev/null @@ -1,3 +0,0 @@ -
- {{ slot }} -
\ No newline at end of file diff --git a/dev/example_project/example_project/templates/cotton/vars_test_component.html b/dev/example_project/example_project/templates/cotton/vars_test_component.html deleted file mode 100644 index f7a1a50..0000000 --- a/dev/example_project/example_project/templates/cotton/vars_test_component.html +++ /dev/null @@ -1,12 +0,0 @@ - - -
- {{ testy }} -

var1: '{{ var1 }}'

-

attr1: '{{ attr1 }}'

-

empty_var: '{{ empty_var }}'

-

var_with_default: '{{ var_with_default }}'

-

slot: '{{ slot }}'

-

named_slot: '{{ named_slot }}'

-

attrs: '{{ attrs }}'

-
diff --git a/dev/example_project/example_project/templates/form_test.html b/dev/example_project/example_project/templates/form_test.html deleted file mode 100644 index 6c70273..0000000 --- a/dev/example_project/example_project/templates/form_test.html +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/dev/example_project/example_project/templates/index.html b/dev/example_project/example_project/templates/index.html deleted file mode 100644 index be843b5..0000000 --- a/dev/example_project/example_project/templates/index.html +++ /dev/null @@ -1,3 +0,0 @@ -{% for i in '123456789' %} - -{% endfor %} \ No newline at end of file diff --git a/dev/example_project/example_project/templates/named_slot_in_loop.html b/dev/example_project/example_project/templates/named_slot_in_loop.html deleted file mode 100644 index 136d7c1..0000000 --- a/dev/example_project/example_project/templates/named_slot_in_loop.html +++ /dev/null @@ -1,7 +0,0 @@ -{% for item in items %} - - - item name: {{ item.name }} - - -{% endfor %} \ No newline at end of file diff --git a/dev/example_project/example_project/templates/parent_test.html b/dev/example_project/example_project/templates/parent_test.html deleted file mode 100644 index 9922300..0000000 --- a/dev/example_project/example_project/templates/parent_test.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dev/example_project/example_project/templates/self_closing_test.html b/dev/example_project/example_project/templates/self_closing_test.html deleted file mode 100644 index 9c99f37..0000000 --- a/dev/example_project/example_project/templates/self_closing_test.html +++ /dev/null @@ -1,4 +0,0 @@ -{% load static %} - - - \ No newline at end of file diff --git a/dev/example_project/example_project/templates/simple_cotton.html b/dev/example_project/example_project/templates/simple_cotton.html index 911b49a..d3e9873 100644 --- a/dev/example_project/example_project/templates/simple_cotton.html +++ b/dev/example_project/example_project/templates/simple_cotton.html @@ -1 +1 @@ -{% for d in data %}{% endfor %} \ No newline at end of file +{% for d in data %}{% endfor %} \ No newline at end of file diff --git a/dev/example_project/example_project/templates/simple_native.html b/dev/example_project/example_project/templates/simple_native.html index 615d7e9..d3d6655 100644 --- a/dev/example_project/example_project/templates/simple_native.html +++ b/dev/example_project/example_project/templates/simple_native.html @@ -1 +1 @@ -{% for d in data %}

{{ d }}

{% endfor %} +{% for d in data %}

{% include 'benchmarks/native_include.html' %}

{% endfor %} diff --git a/dev/example_project/example_project/templates/vars_test.html b/dev/example_project/example_project/templates/vars_test.html deleted file mode 100644 index 9f16cca..0000000 --- a/dev/example_project/example_project/templates/vars_test.html +++ /dev/null @@ -1,3 +0,0 @@ - - default slot - \ No newline at end of file diff --git a/dev/example_project/render_load_test.py b/dev/example_project/render_load_test.py index 98f6846..232f75f 100644 --- a/dev/example_project/render_load_test.py +++ b/dev/example_project/render_load_test.py @@ -19,8 +19,18 @@ settings.configure( { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": ["example_project/templates"], - "APP_DIRS": False, + "APP_DIRS": True, "OPTIONS": { + "loaders": [ + ( + "django.template.loaders.cached.Loader", + [ + "django_cotton.loader.CottonLoader", + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ], + ) + ], "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", @@ -37,38 +47,59 @@ settings.configure( django.setup() -def template_bench(template_name, iterations=500): +def template_bench(template_name, iterations=1000): start_time = time.time() for _ in range(iterations): render_to_string(template_name) end_time = time.time() - return end_time - start_time, render_to_string(template_name) + duration = round((end_time - start_time) * 1000, 2) + + return duration, render_to_string(template_name) -def template_bench_alt(template_name, iterations=500): +def template_bench_alt(template_name, iterations=1000): data = list(range(1, iterations)) start_time = time.time() render_to_string(template_name, context={"data": data}) end_time = time.time() - return end_time - start_time, render_to_string(template_name) + duration = round((end_time - start_time) * 1000, 2) + return duration, render_to_string(template_name) + + +# warm caches +template_bench_alt("simple_native.html", iterations=1) +template_bench_alt("simple_cotton.html", iterations=1) simple_native, _ = template_bench_alt("simple_native.html") simple_cotton, _ = template_bench_alt("simple_cotton.html") -print(f"Native Django Template: {simple_native} seconds") -print(f"Cotton Template: {simple_cotton} seconds") +print("---") +print(f"Native Django {{% for %}} loop: {simple_native} ms") +print(f"Cotton {{% for %}} loop: {simple_cotton} ms") + +# warm caches +template_bench("benchmarks/native_include.html", iterations=1) +template_bench("cotton/benchmarks/cotton_include.html", iterations=1) time_native_include, _ = template_bench("benchmarks/native_include.html") time_cotton_include, _ = template_bench("cotton/benchmarks/cotton_include.html") -print(f"Native {{% include %}}: {time_native_include} seconds") -print(f"Cotton for include:: {time_cotton_include} seconds") +print("---") +print(f"Native {{% include %}}: {time_native_include} ms") +print(f"Cotton for include: {time_cotton_include} ms") + +# warm caches +template_bench("benchmarks/native_extends.html", iterations=1) +template_bench("cotton/benchmarks/cotton_compiled.html", iterations=1) +template_bench("cotton/benchmarks/cotton_extends_equivalent.html", iterations=1) time_native_extends, _ = template_bench("benchmarks/native_extends.html") time_compiled_cotton, _ = template_bench("cotton/benchmarks/cotton_compiled.html") -time_cotton, _ = template_bench("cotton/benchmarks/cotton.html") +time_cotton, _ = template_bench("cotton/benchmarks/cotton_extends_equivalent.html") -print(f"Native {{% block %}} and {{% extends %}}: {time_native_extends} seconds") -print(f"Uncompiled Cotton Template: {time_cotton} seconds") -print(f"Compiled Cotton Template: {time_compiled_cotton} seconds") + +print("---") +print(f"Native {{% block %}} and {{% extends %}}: {time_native_extends} ms") +print(f"Compiled Cotton Template: {time_compiled_cotton} ms") +print(f"Uncompiled Cotton Template: {time_cotton} ms") diff --git a/dev/example_project/unspecified_app_directory/templates/cotton/app_outside_of_dirs.html b/dev/example_project/unspecified_app_directory/templates/cotton/app_outside_of_dirs.html new file mode 100644 index 0000000..ad1ba63 --- /dev/null +++ b/dev/example_project/unspecified_app_directory/templates/cotton/app_outside_of_dirs.html @@ -0,0 +1 @@ +My template path was not specified in settings! \ No newline at end of file diff --git a/dev/example_project/unspecified_app_directory/templates/cotton/unspecified_component.html b/dev/example_project/unspecified_app_directory/templates/cotton/unspecified_component.html deleted file mode 100644 index 51f8237..0000000 --- a/dev/example_project/unspecified_app_directory/templates/cotton/unspecified_component.html +++ /dev/null @@ -1 +0,0 @@ -My template was not specified in settings! \ No newline at end of file diff --git a/dev/example_project/unspecified_app_directory/templates/unspecified_view.html b/dev/example_project/unspecified_app_directory/templates/unspecified_view.html deleted file mode 100644 index 12b3ec5..0000000 --- a/dev/example_project/unspecified_app_directory/templates/unspecified_view.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/django_cotton/templates/attribute_merging_test.html b/django_cotton/templates/attribute_merging_test.html deleted file mode 100644 index ebcf664..0000000 --- a/django_cotton/templates/attribute_merging_test.html +++ /dev/null @@ -1,3 +0,0 @@ - - ss - \ No newline at end of file diff --git a/django_cotton/templates/attribute_passing_test.html b/django_cotton/templates/attribute_passing_test.html deleted file mode 100644 index a7bc9fc..0000000 --- a/django_cotton/templates/attribute_passing_test.html +++ /dev/null @@ -1,3 +0,0 @@ - - ss - \ No newline at end of file diff --git a/django_cotton/templates/child_test.html b/django_cotton/templates/child_test.html deleted file mode 100644 index ec03be8..0000000 --- a/django_cotton/templates/child_test.html +++ /dev/null @@ -1,3 +0,0 @@ - - d - \ No newline at end of file diff --git a/django_cotton/templates/cotton/child.html b/django_cotton/templates/cotton/child.html deleted file mode 100644 index 9f989fa..0000000 --- a/django_cotton/templates/cotton/child.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/django_cotton/templates/cotton/container.html b/django_cotton/templates/cotton/container.html deleted file mode 100644 index 835e5c4..0000000 --- a/django_cotton/templates/cotton/container.html +++ /dev/null @@ -1,11 +0,0 @@ -
- Header: - {{ header }} -
- -
- Content: - {{ slot }} -
- - diff --git a/django_cotton/templates/cotton/eval_attributes_test_component.html b/django_cotton/templates/cotton/eval_attributes_test_component.html deleted file mode 100644 index f84dbe3..0000000 --- a/django_cotton/templates/cotton/eval_attributes_test_component.html +++ /dev/null @@ -1,27 +0,0 @@ -{% if none is None %} -

none is None

-{% endif %} - -{% if number == 1 %} -

number is 1

-{% endif %} - -{% if boolean_true is True %} -

boolean_true is True

-{% endif %} - -{% if boolean_false is False %} -

boolean_false is False

-{% endif %} - -{% if dict.key == 'value' %} -

dict.key is 'value'

-{% endif %} - -{% if list.0 == 1 %} -

list.0 is 1

-{% endif %} - -{% if listdict.0.key == 'value' %} -

listdict.0.key is 'value'

-{% endif %} \ No newline at end of file diff --git a/django_cotton/templates/cotton/eval_vars_test_component.html b/django_cotton/templates/cotton/eval_vars_test_component.html deleted file mode 100644 index 4235bee..0000000 --- a/django_cotton/templates/cotton/eval_vars_test_component.html +++ /dev/null @@ -1,29 +0,0 @@ - - -{% if none is None %} -

none is None

-{% endif %} - -{% if number == 1 %} -

number is 1

-{% endif %} - -{% if boolean_true is True %} -

boolean_true is True

-{% endif %} - -{% if boolean_false is False %} -

boolean_false is False

-{% endif %} - -{% if dict.key == 'value' %} -

dict.key is 'value'

-{% endif %} - -{% if list.0 == 1 %} -

list.0 is 1

-{% endif %} - -{% if listdict.0.key == 'value' %} -

listdict.0.key is 'value'

-{% endif %} \ No newline at end of file diff --git a/django_cotton/templates/cotton/merges_attributes.html b/django_cotton/templates/cotton/merges_attributes.html deleted file mode 100644 index ef33d94..0000000 --- a/django_cotton/templates/cotton/merges_attributes.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ No newline at end of file diff --git a/django_cotton/templates/cotton/named_slot_component.html b/django_cotton/templates/cotton/named_slot_component.html deleted file mode 100644 index e324e2c..0000000 --- a/django_cotton/templates/cotton/named_slot_component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- {{ name }} -
\ No newline at end of file diff --git a/django_cotton/templates/cotton/native_tags_in_attributes.html b/django_cotton/templates/cotton/native_tags_in_attributes.html deleted file mode 100644 index d3e3cc5..0000000 --- a/django_cotton/templates/cotton/native_tags_in_attributes.html +++ /dev/null @@ -1,4 +0,0 @@ -Attribute 1 says: '{{ attr1 }}' -Attribute 2 says: '{{ attr2 }}' -Attribute 3 says: '{{ attr3 }}' -attrs tag is: '{{ attrs }}' \ No newline at end of file diff --git a/django_cotton/templates/cotton/parent.html b/django_cotton/templates/cotton/parent.html deleted file mode 100644 index c534580..0000000 --- a/django_cotton/templates/cotton/parent.html +++ /dev/null @@ -1,3 +0,0 @@ -
- {{slot}} -
\ No newline at end of file diff --git a/django_cotton/templates/cotton/receives_attributes.html b/django_cotton/templates/cotton/receives_attributes.html deleted file mode 100644 index 8d463b4..0000000 --- a/django_cotton/templates/cotton/receives_attributes.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ No newline at end of file diff --git a/django_cotton/templates/cotton/test_component.html b/django_cotton/templates/cotton/test_component.html deleted file mode 100644 index e1ba26c..0000000 --- a/django_cotton/templates/cotton/test_component.html +++ /dev/null @@ -1,13 +0,0 @@ - - -

slot: '{{ slot }}'

- -

attr1: '{{ attr1 }}'

-

attr2: '{{ attr2 }}'

- -

var1: '{{ var1 }}'

-

default_var: '{{ default_var }}'

- -

named_slot: '{{ named_slot }}'

- -

attrs: '{{ attrs }}'

diff --git a/django_cotton/templates/django_syntax_decoding_test.html b/django_cotton/templates/django_syntax_decoding_test.html deleted file mode 100644 index ee3d784..0000000 --- a/django_cotton/templates/django_syntax_decoding_test.html +++ /dev/null @@ -1 +0,0 @@ -
Hello, World!
\ No newline at end of file diff --git a/django_cotton/templates/eval_attributes_test_view.html b/django_cotton/templates/eval_attributes_test_view.html deleted file mode 100644 index d65a6c9..0000000 --- a/django_cotton/templates/eval_attributes_test_view.html +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/django_cotton/templates/eval_vars_test_view.html b/django_cotton/templates/eval_vars_test_view.html deleted file mode 100644 index 086a4bd..0000000 --- a/django_cotton/templates/eval_vars_test_view.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/django_cotton/templates/form_test.html b/django_cotton/templates/form_test.html deleted file mode 100644 index 6c70273..0000000 --- a/django_cotton/templates/form_test.html +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/django_cotton/templates/named_slot_in_loop.html b/django_cotton/templates/named_slot_in_loop.html deleted file mode 100644 index 136d7c1..0000000 --- a/django_cotton/templates/named_slot_in_loop.html +++ /dev/null @@ -1,7 +0,0 @@ -{% for item in items %} - - - item name: {{ item.name }} - - -{% endfor %} \ No newline at end of file diff --git a/django_cotton/templates/native_tags_in_attributes_view.html b/django_cotton/templates/native_tags_in_attributes_view.html deleted file mode 100644 index 3445c45..0000000 --- a/django_cotton/templates/native_tags_in_attributes_view.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - test - diff --git a/django_cotton/templates/parent_test.html b/django_cotton/templates/parent_test.html deleted file mode 100644 index 9922300..0000000 --- a/django_cotton/templates/parent_test.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/django_cotton/templates/self_closing_test.html b/django_cotton/templates/self_closing_test.html deleted file mode 100644 index 9c99f37..0000000 --- a/django_cotton/templates/self_closing_test.html +++ /dev/null @@ -1,4 +0,0 @@ -{% load static %} - - - \ No newline at end of file diff --git a/django_cotton/templates/string_with_spaces.html b/django_cotton/templates/string_with_spaces.html deleted file mode 100644 index 510f8c2..0000000 --- a/django_cotton/templates/string_with_spaces.html +++ /dev/null @@ -1,5 +0,0 @@ - - - named_slot with spaces - - \ No newline at end of file diff --git a/django_cotton/templates/variable_parsing_test.html b/django_cotton/templates/variable_parsing_test.html deleted file mode 100644 index 8402bd4..0000000 --- a/django_cotton/templates/variable_parsing_test.html +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/django_cotton/templatetags/_component.py b/django_cotton/templatetags/_component.py index 56877c8..1323662 100644 --- a/django_cotton/templatetags/_component.py +++ b/django_cotton/templatetags/_component.py @@ -83,7 +83,19 @@ class CottonComponentNode(Node): template_path = self._generate_component_template_path(attrs) - return get_template(template_path).render(ctx) + # Use render_context for caching the template + cache = context.render_context.get(self) + if cache is None: + cache = context.render_context[self] = {} + + tpl = cache.get(template_path) + if tpl is None: + tpl = get_template(template_path) + cache[template_path] = tpl + + return tpl.render(ctx) + + # return get_template(template_path).render(ctx) def _build_attrs(self, context): """ diff --git a/django_cotton/tests/inline_test_case.py b/django_cotton/tests/inline_test_case.py deleted file mode 100644 index f49ae24..0000000 --- a/django_cotton/tests/inline_test_case.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import sys -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 -from django.conf import settings -from django.test import TestCase - - -class DynamicURLModule: - def __init__(self): - self.urlpatterns = [] - - def __call__(self): - return self.urlpatterns - - -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 - - # 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 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 tearDown(self): - """Clear cache between tests so that we can use the same file names for simplicity""" - cache.clear() - - def create_template(self, name, content, url=None, context={}): - """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) - - if url: - # Create a dynamic class-based view - class DynamicTemplateView(TemplateView): - template_name = name - - def get_context_data(self, **kwargs): - dynamic_context = super().get_context_data(**kwargs) - dynamic_context.update(context) - return dynamic_context - - self.register_path(url, DynamicTemplateView.as_view(template_name=name)) - - return path - - def make_view(self, template_name): - """Make a view that renders the given template""" - return TemplateView.as_view(template_name=template_name) - - def register_path(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 - - def setUp(self): - super().setUp() - self.url_module.urlpatterns = [] - - def get_url_conf(self): - return self.url_module_name diff --git a/django_cotton/tests/test_attributes.py b/django_cotton/tests/test_attributes.py new file mode 100644 index 0000000..d0a5cd9 --- /dev/null +++ b/django_cotton/tests/test_attributes.py @@ -0,0 +1,485 @@ +from django_cotton.tests.utils import CottonInlineTestCase +from django_cotton.tests.utils import get_compiled + + +class AttributeHandlingTests(CottonInlineTestCase): + def test_dynamic_attributes_on_components(self): + self.create_template( + "eval_attributes_on_component_view.html", + """ + + """, + "view/", + context={"variable": 111, "dummy": 222}, + ) + + self.create_template( + "cotton/dynamic_attributes_component.html", + """ + {% if none is None %} +

none is None

+ {% endif %} + + {% if number == 1 %} +

number is 1

+ {% endif %} + + {% if boolean_true is True %} +

boolean_true is True

+ {% endif %} + + {% if boolean_false is False %} +

boolean_false is False

+ {% endif %} + + {% if dict.key == 'value' %} +

dict.key is 'value'

+ {% endif %} + + {% if list.0 == 1 %} +

list.0 is 1

+ {% endif %} + + {% if listdict.0.key == 'value' %} +

listdict.0.key is 'value'

+ {% endif %} + + {% if variable == 111 %} +

variable is 111

+ {% endif %} + + {% if template_string_lit.dummy == 222 %} +

template_string_lit.dummy is 222

+ {% endif %} + """, + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, "none is None") + self.assertContains(response, "number is 1") + self.assertContains(response, "boolean_true is True") + self.assertContains(response, "boolean_false is False") + self.assertContains(response, "list.0 is 1") + self.assertContains(response, "dict.key is 'value'") + self.assertContains(response, "listdict.0.key is 'value'") + self.assertContains(response, "variable is 111") + self.assertContains(response, "template_string_lit.dummy is 222") + + def test_dynamic_attributes_in_cvars(self): + self.create_template( + "eval_attributes_in_cvars_view.html", + """ + + """, + "view/", + ) + + self.create_template( + "cotton/dynamic_attributes_cvars.html", + """ + + + {% if none is None %} +

none is None

+ {% endif %} + + {% if number == 1 %} +

number is 1

+ {% endif %} + + {% if boolean_true is True %} +

boolean_true is True

+ {% endif %} + + {% if boolean_false is False %} +

boolean_false is False

+ {% endif %} + + {% if dict.key == 'value' %} +

dict.key is 'value'

+ {% endif %} + + {% if list.0 == 1 %} +

list.0 is 1

+ {% endif %} + + {% if listdict.0.key == 'value' %} +

listdict.0.key is 'value'

+ {% endif %} + + {% if variable == 111 %} +

variable is 111

+ {% endif %} + """, + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, "none is None") + self.assertContains(response, "number is 1") + self.assertContains(response, "boolean_true is True") + self.assertContains(response, "boolean_false is False") + self.assertContains(response, "list.0 is 1") + self.assertContains(response, "dict.key is 'value'") + self.assertContains(response, "listdict.0.key is 'value'") + self.assertContains(response, "variable is 111") + + def test_we_can_govern_whole_attributes_in_html_elements(self): + self.create_template( + "cotton/attribute_govern.html", + """ +
+ """, + ) + + self.create_template( + "attribute_govern_view.html", + """ + + """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, 'class="second"') + self.assertNotContains(response, 'class="first"') + + def test_attribute_names_on_component_containing_hyphens_are_converted_to_underscores( + self, + ): + self.create_template( + "cotton/hyphens.html", + """ +
+ """, + ) + + self.create_template( + "hyphens_view.html", + """ + + """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.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/cvar_hyphens.html", + """ + + +
+ """, + ) + + self.create_template( + "cvar_hyphens_view.html", + """ + + """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, 'x-data="{}" x-init="do_something()"') + + def test_equals_in_attribute_values(self): + self.create_template( + "cotton/equals.html", + """ +
+ """, + ) + + self.create_template( + "equals_view.html", + """ + + """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, '@click="this=test"') + + def test_spaces_are_maintained_around_expressions_inside_attributes(self): + self.create_template( + "maintain_spaces_in_attributes_view.html", + """ +
+ """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, "some_attribute__something") + + def test_boolean_attributes(self): + self.create_template( + "cotton/boolean_attribute.html", + """ + {% if is_something is True %} + It's True + {% endif %} + """, + ) + + self.create_template( + "boolean_attribute_view.html", + """ + + """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, "It's True") + + def test_attributes_without_colons_are_not_evaluated(self): + self.create_template( + "cotton/static_attrs.html", + """ + {% if something == "1,234" %} + All good + {% endif %} + + {% if something == "(1, 234)" %} + "1,234" was evaluated as a tuple + {% endif %} + """, + ) + + self.create_template( + "static_attrs_view.html", + """ + + """, + "view/", + context={"something": "1,234"}, + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, "All good") + + def test_unprocessable_dynamic_attributes_fallback_to_cvars_defaults(self): + self.create_template( + "cotton/unprocessable_dynamic_attribute.html", + """ + + {{ color }} + """, + ) + + self.create_template( + "unprocessable_dynamic_attribute_view.html", + """ + + """, + "view/", + context={}, + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertTrue("gray" in response.content.decode()) + + def test_attribute_merging(self): + self.create_template( + "cotton/merges_attributes.html", + """ +
+ """, + ) + + self.create_template( + "attribute_merging_view.html", + """ + ss + """, + "view/", + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, 'class="form-group another-class-with:colon extra-class"') + + def test_attributes_can_contain_django_native_tags(self): + self.create_template( + "native_tags_in_attributes_view.html", + """ + + test + + """, + "view/", + context={"name": "Will", "test": "world"}, + ) + + self.create_template( + "cotton/native_tags_in_attributes.html", + """ + Attribute 1 says: '{{ attr1 }}' + Attribute 2 says: '{{ attr2 }}' + Attribute 3 says: '{{ attr3 }}' + attrs tag is: '{{ attrs }}' + """, + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, "Attribute 1 says: 'Hello Will'") + self.assertContains(response, "Attribute 2 says: 'world'") + self.assertContains(response, "Attribute 3 says: 'cowabonga!'") + + self.assertContains( + response, + """attrs tag is: 'attr1="Hello Will" attr2="world" attr3="cowabonga!"'""", + ) + + def test_strings_with_spaces_can_be_passed(self): + self.create_template( + "string_with_spaces_view.html", + """ + + + named_slot with spaces + + + """, + "view/", + ) + + self.create_template( + "cotton/string_test.html", + """ + + + slot: '{{ slot }}' + attr1: '{{ attr1 }}' + attr2: '{{ attr2 }}' + var1: '{{ var1 }}' + default_var: '{{ default_var }}' + named_slot: '{{ named_slot }}' + attrs: '{{ attrs }}' + """, + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, "attr1: 'I have spaces'") + self.assertContains(response, "var1: 'string with space'") + self.assertContains(response, "default_var: 'default var'") + self.assertContains(response, "named_slot: '") + self.assertContains(response, "named_slot with spaces") + self.assertContains(response, """attrs: 'attr1="I have spaces"'""") + + def test_attrs_do_not_contain_cvars(self): + self.create_template( + "cvars_test_view.html", + """ + + default slot + + """, + "view/", + ) + + self.create_template( + "cotton/cvars_test_component.html", + """ + + +
+ {{ testy }} +

var1: '{{ var1 }}'

+

attr1: '{{ attr1 }}'

+

empty_var: '{{ empty_var }}'

+

var_with_default: '{{ var_with_default }}'

+

slot: '{{ slot }}'

+

named_slot: '{{ named_slot }}'

+

attrs: '{{ attrs }}'

+
+ """, + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, "attr1: 'im an attr'") + self.assertContains(response, "var1: 'im a var'") + self.assertContains(response, """attrs: 'attr1="im an attr"'""") + + def test_attribute_passing(self): + self.create_template( + "attribute_passing_view.html", + """ + + """, + "view/", + ) + self.create_template("cotton/attribute_passing.html", """
""") + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains( + response, '
' + ) + + def test_loader_preserves_duplicate_attributes(self): + compiled = get_compiled("""hello""") + + self.assertEquals( + compiled, + """hello""", + ) diff --git a/django_cotton/tests/test_basic.py b/django_cotton/tests/test_basic.py new file mode 100644 index 0000000..7b9c1f1 --- /dev/null +++ b/django_cotton/tests/test_basic.py @@ -0,0 +1,104 @@ +from django_cotton.tests.utils import CottonInlineTestCase + + +class BasicComponentTests(CottonInlineTestCase): + def test_component_is_rendered(self): + self.create_template( + "cotton/render.html", + """
{{ slot }}
""", + ) + + self.create_template( + "view.html", + """Hello, World!""", + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, '
') + self.assertContains(response, "Hello, World!") + + def test_nested_rendering(self): + self.create_template( + "cotton/parent.html", + """ +
+ {{ slot }} +
+ """, + ) + + self.create_template( + "cotton/child.html", + """ +
+ """, + ) + + self.create_template( + "cotton/nested_render_view.html", + """ + + d + + """, + "view/", + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, '
') + self.assertContains(response, '
') + + def test_cotton_directory_can_be_configured(self): + custom_dir = "components" + + self.create_template( + f"{custom_dir}/custom_directory.html", + """
{{ slot }}
""", + ) + + self.create_template( + "custom_directory_view.html", + """Hello, World!""", + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf(), COTTON_DIR=custom_dir): + response = self.client.get("/view/") + self.assertContains(response, '
') + self.assertContains(response, "Hello, World!") + + def test_self_closing_is_rendered(self): + self.create_template("cotton/self_closing.html", """I self closed!""") + self.create_template( + "self_closing_view.html", + """ + 1: + 2: + 3: + """, + "view/", + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, "1: I self closed!") + self.assertContains(response, "2: I self closed!") + self.assertContains(response, "3: I self closed!") + + def test_loader_scans_all_app_directories(self): + self.create_template( + "app_outside_of_dirs_view.html", """""", "view/" + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertContains( + response, + """My template path was not specified in settings!""", + ) diff --git a/django_cotton/tests/test_cotton.py b/django_cotton/tests/test_cotton.py deleted file mode 100644 index 279a87b..0000000 --- a/django_cotton/tests/test_cotton.py +++ /dev/null @@ -1,543 +0,0 @@ -from django.test import TestCase - -from django_cotton.tests.inline_test_case import CottonInlineTestCase -from django_cotton.tests.utils import get_compiled, get_rendered - - -class InlineTestCase(CottonInlineTestCase): - def test_component_is_rendered(self): - self.create_template( - "cotton/render.html", - """
{{ slot }}
""", - ) - - self.create_template( - "view.html", - """Hello, World!""", - "view/", - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - self.assertContains(response, '
') - self.assertContains(response, "Hello, World!") - - def test_new_lines_in_attributes_are_preserved(self): - self.create_template( - "cotton/preserved.html", - """
{{ slot }}
""", - ) - - self.create_template( - "preserved_view.html", - """ - - """, - "view/", - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - - self.assertTrue( - """{ - attr1: 'im an attr', - var1: 'im a var', - method() { - return 'im a method'; - } - }""" - in response.content.decode() - ) - - def test_attributes_that_end_or_start_with_quotes_are_preserved(self): - self.create_template( - "cotton/preserve_quotes.html", - """ -
- """, - ) - - self.create_template( - "preserve_quotes_view.html", - """ - - """, - "view/", - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - - self.assertContains(response, '''"var ? 'this' : 'that'"''') - - def test_we_can_govern_whole_attributes_in_html_elements(self): - self.create_template( - "cotton/attribute_govern.html", - """ -
- """, - ) - - self.create_template( - "attribute_govern_view.html", - """ - - """, - "view/", - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - self.assertContains(response, 'class="second"') - self.assertNotContains(response, 'class="first"') - - def test_attribute_names_on_component_containing_hyphens_are_converted_to_underscores( - self, - ): - self.create_template( - "cotton/hyphens.html", - """ -
- """, - ) - - self.create_template( - "hyphens_view.html", - """ - - """, - "view/", - ) - - # 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/cvar_hyphens.html", - """ - - -
- """, - ) - - self.create_template( - "cvar_hyphens_view.html", - """ - - """, - "view/", - ) - - # 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_cotton_directory_can_be_configured(self): - custom_dir = "components" - - self.create_template( - f"{custom_dir}/custom_directory.html", - """
{{ slot }}
""", - ) - - self.create_template( - "custom_directory_view.html", - """Hello, World!""", - "view/", - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf(), COTTON_DIR=custom_dir): - response = self.client.get("/view/") - self.assertContains(response, '
') - self.assertContains(response, "Hello, World!") - - def test_equals_in_attribute_values(self): - self.create_template( - "cotton/equals.html", - """ -
- """, - ) - - self.create_template( - "equals_view.html", - """ - - """, - "view/", - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - - self.assertContains(response, '@click="this=test"') - - def test_dynamic_components_via_string(self): - self.create_template( - "cotton/dynamic_component.html", - """ -
I am dynamic
- """, - ) - - html = """ - - """ - - rendered = get_rendered(html, {"is": "dynamic-component"}) - - self.assertTrue("I am dynamic" in rendered) - - def test_dynamic_components_via_variable(self): - self.create_template( - "cotton/dynamic_component.html", - """ -
I am dynamic
- """, - ) - - html = """ - - """ - - rendered = get_rendered(html, {"is": "dynamic-component"}) - - self.assertTrue("I am dynamic" in rendered) - - def test_dynamic_components_via_expression_attribute(self): - self.create_template( - "cotton/dynamic_component_expression.html", - """ -
I am dynamic component from expression
- """, - ) - - html = """ - - """ - - rendered = get_rendered(html, {"is": "component-expression"}) - - self.assertTrue("I am dynamic component from expression" in rendered) - - def test_dynamic_components_in_subfolders(self): - self.create_template( - "cotton/subfolder/dynamic_component_expression.html", - """ -
I am dynamic component from expression
- """, - ) - - html = """ - - """ - - rendered = get_rendered(html, {"is": "dynamic-component-expression"}) - - self.assertTrue("I am dynamic component from expression" in rendered) - - def test_spaces_are_maintained_around_expressions_inside_attributes(self): - self.create_template( - "maintain_spaces_in_attributes_view.html", - """ -
- """, - "view/", - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - - self.assertContains(response, "some_attribute__something") - - def test_dynamic_attributes_are_also_template_parsed(self): - self.create_template( - "cotton/dynamic_attribute_template_parsing.html", - """ - {% for image in images %} - {{ forloop.counter }}: {{ image }} - {% endfor %} - """, - ) - - self.create_template( - "dynamic_attributes_parsing_view.html", - """ - - """, - "view/", - context={"image1": "1.jpg", "image2": "2.jpg"}, - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - self.assertContains(response, "1: 1.jpg") - self.assertContains(response, "2: 2.jpg") - - def test_boolean_attributes(self): - self.create_template( - "cotton/boolean_attribute.html", - """ - {% if is_something is True %} - It's True - {% endif %} - """, - ) - - self.create_template( - "boolean_attribute_view.html", - """ - - """, - "view/", - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - self.assertContains(response, "It's True") - - def test_attributes_without_colons_are_not_evaluated(self): - self.create_template( - "cotton/static_attrs.html", - """ - {% if something == "1,234" %} - All good - {% endif %} - - {% if something == "(1, 234)" %} - "1,234" was evaluated as a tuple - {% endif %} - """, - ) - - self.create_template( - "static_attrs_view.html", - """ - - """, - "view/", - context={"something": "1,234"}, - ) - - # Override URLconf - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - self.assertContains(response, "All good") - - def test_unprocessable_dynamic_attributes_fallback_to_cvars_defaults(self): - self.create_template( - "cotton/unprocessable_dynamic_attribute.html", - """ - - {{ color }} - """, - ) - - self.create_template( - "unprocessable_dynamic_attribute_view.html", - """ - - """, - "view/", - context={}, - ) - - with self.settings(ROOT_URLCONF=self.get_url_conf()): - response = self.client.get("/view/") - self.assertTrue("gray" in response.content.decode()) - - -class CottonTestCase(TestCase): - def test_parent_component_is_rendered(self): - response = self.client.get("/parent") - self.assertContains(response, '
') - - def test_child_is_rendered(self): - response = self.client.get("/child") - self.assertContains(response, '
') - self.assertContains(response, '
') - - def test_self_closing_is_rendered(self): - response = self.client.get("/self-closing") - self.assertContains(response, '
') - - def test_named_slots_correctly_display_in_loop(self): - response = self.client.get("/named-slot-in-loop") - self.assertContains(response, "item name: Item 1") - self.assertContains(response, "item name: Item 2") - self.assertContains(response, "item name: Item 3") - - def test_attribute_passing(self): - response = self.client.get("/attribute-passing") - self.assertContains( - response, '
' - ) - - def test_attribute_merging(self): - response = self.client.get("/attribute-merging") - self.assertContains(response, 'class="form-group another-class-with:colon extra-class"') - - def test_django_syntax_decoding(self): - response = self.client.get("/django-syntax-decoding") - self.assertContains(response, "some-class") - - def test_vars_are_converted_to_vars_frame_tags(self): - compiled = get_compiled( - """ - - - content - """ - ) - - self.assertEquals( - compiled, - """{% cotton_vars_frame var1=var1|default:"string with space" %}content{% endcotton_vars_frame %}""", - ) - - def test_loader_preserves_duplicate_attributes(self): - compiled = get_compiled("""hello""") - - self.assertEquals( - compiled, - """hello""", - ) - - def test_attrs_do_not_contain_vars(self): - response = self.client.get("/vars-test") - self.assertContains(response, "attr1: 'im an attr'") - self.assertContains(response, "var1: 'im a var'") - self.assertContains(response, """attrs: 'attr1="im an attr"'""") - - def test_strings_with_spaces_can_be_passed(self): - response = self.client.get("/string-with-spaces") - self.assertContains(response, "attr1: 'I have spaces'") - self.assertContains(response, "var1: 'string with space'") - self.assertContains(response, "default_var: 'default var'") - self.assertContains(response, "named_slot: '") - self.assertContains(response, "named_slot with spaces") - self.assertContains(response, """attrs: 'attr1="I have spaces"'""") - - def test_named_slots_dont_bleed_into_sibling_components(self): - html = """ - - component1 - named slot 1 - - - component2 - - """ - - rendered = get_rendered(html) - - self.assertTrue("named_slot: 'named slot 1'" in rendered) - self.assertTrue("named_slot: ''" in rendered) - - def test_template_variables_are_not_parsed(self): - html = """ - - - test - - - """ - - rendered = get_rendered(html, {"variable": 1}) - - self.assertTrue("attr1: 'variable'" in rendered) - self.assertTrue("attr2: '1'" in rendered) - - def test_component_attributes_can_converted_to_python_types(self): - response = self.client.get("/test/eval-attributes") - - self.assertContains(response, "none is None") - self.assertContains(response, "number is 1") - self.assertContains(response, "boolean_true is True") - self.assertContains(response, "boolean_false is False") - self.assertContains(response, "list.0 is 1") - self.assertContains(response, "dict.key is 'value'") - self.assertContains(response, "listdict.0.key is 'value'") - - def test_cvars_can_be_converted_to_python_types(self): - response = self.client.get("/test/eval-vars") - - self.assertContains(response, "none is None") - self.assertContains(response, "number is 1") - self.assertContains(response, "boolean_true is True") - self.assertContains(response, "boolean_false is False") - self.assertContains(response, "list.0 is 1") - self.assertContains(response, "dict.key is 'value'") - self.assertContains(response, "listdict.0.key is 'value'") - - def test_attributes_can_contain_django_native_tags(self): - response = self.client.get("/test/native-tags-in-attributes") - - self.assertContains(response, "Attribute 1 says: 'Hello Will'") - self.assertContains(response, "Attribute 2 says: 'world'") - self.assertContains(response, "Attribute 3 says: 'cowabonga!'") - - self.assertContains( - response, - """attrs tag is: 'normal="normal" attr1="Hello Will" attr2="world" attr3="cowabonga!"'""", - ) - - def test_loader_scans_all_app_directories(self): - response = self.client.get("/test/unspecified-app-directory-template") - - self.assertContains( - response, - """My template was not specified in settings!""", - ) - - def test_expression_tags_close_to_tag_elements_doesnt_corrupt_the_tag(self): - html = """ -
- """ - - rendered = get_compiled(html) - - self.assertFalse("" in rendered, "Tag corrupted") - self.assertTrue("
" in rendered, "
not found in rendered string") - - def test_conditionals_evaluation_inside_elements(self): - html = """ - - - - """ - - rendered = get_rendered(html, {"my_obj": {"selection": 1}}) - - self.assertTrue('' in rendered) - self.assertTrue('' not in rendered) diff --git a/django_cotton/tests/test_dynamic_components.py b/django_cotton/tests/test_dynamic_components.py new file mode 100644 index 0000000..e163046 --- /dev/null +++ b/django_cotton/tests/test_dynamic_components.py @@ -0,0 +1,68 @@ +from django_cotton.tests.utils import CottonInlineTestCase +from django_cotton.tests.utils import get_rendered + + +class DynamicComponentTests(CottonInlineTestCase): + def test_dynamic_components_via_string(self): + self.create_template( + "cotton/dynamic_component_via_string.html", + """ +
I am dynamic
+ """, + ) + + html = """ + + """ + + rendered = get_rendered(html) + + self.assertTrue("I am dynamic" in rendered) + + def test_dynamic_components_via_variable(self): + self.create_template( + "cotton/dynamic_component_via_variable.html", + """ +
I am dynamic
+ """, + ) + + html = """ + + """ + + rendered = get_rendered(html, {"is": "dynamic-component-via-variable"}) + + self.assertTrue("I am dynamic" in rendered) + + def test_dynamic_components_via_expression(self): + self.create_template( + "cotton/dynamic_component_expression.html", + """ +
I am dynamic component from expression
+ """, + ) + + html = """ + + """ + + rendered = get_rendered(html, {"is": "component-expression"}) + + self.assertTrue("I am dynamic component from expression" in rendered) + + def test_dynamic_components_in_subfolders(self): + self.create_template( + "cotton/subfolder/dynamic_component_expression.html", + """ +
I am dynamic component from expression
+ """, + ) + + html = """ + + """ + + rendered = get_rendered(html, {"is": "dynamic-component-expression"}) + + self.assertTrue("I am dynamic component from expression" in rendered) diff --git a/django_cotton/tests/test_slots.py b/django_cotton/tests/test_slots.py new file mode 100644 index 0000000..23be248 --- /dev/null +++ b/django_cotton/tests/test_slots.py @@ -0,0 +1,77 @@ +from django_cotton.tests.utils import CottonInlineTestCase +from django_cotton.tests.utils import get_compiled + + +class SlotAndContentTests(CottonInlineTestCase): + def test_named_slots_correctly_display_in_loop(self): + self.create_template( + "named_slot_in_loop_view.html", + """ + {% for item in items %} + + + item name: {{ item.name }} + + + {% endfor %} + """, + "view/", + context={ + "items": [ + {"name": "Item 1"}, + {"name": "Item 2"}, + {"name": "Item 3"}, + ] + }, + ) + + self.create_template( + "cotton/named_slot_component.html", + """, +
+ {{ name }} +
+ """, + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, "item name: Item 1") + self.assertContains(response, "item name: Item 2") + self.assertContains(response, "item name: Item 3") + + def test_named_slots_dont_bleed_into_sibling_components(self): + self.create_template( + "slot_bleed_view.html", + """ + + named slot 1 + + + """, + "view/", + ) + + self.create_template( + "cotton/slot_bleed.html", """named_slot {{ id }}: '{{ named_slot }}'

""" + ) + + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertTrue("named_slot 1: 'named slot 1'" in response.content.decode()) + self.assertTrue("named_slot 2: ''" in response.content.decode()) + + def test_vars_are_converted_to_vars_frame_tags(self): + compiled = get_compiled( + """ + + + content + """ + ) + + self.assertEquals( + compiled, + """{% cotton_vars_frame var1=var1|default:"string with space" %}content{% endcotton_vars_frame %}""", + ) diff --git a/django_cotton/tests/test_template_rendering.py b/django_cotton/tests/test_template_rendering.py new file mode 100644 index 0000000..97fa0e6 --- /dev/null +++ b/django_cotton/tests/test_template_rendering.py @@ -0,0 +1,91 @@ +from django_cotton.tests.utils import CottonInlineTestCase +from django_cotton.tests.utils import get_compiled + + +class TemplateRenderingTests(CottonInlineTestCase): + def test_new_lines_in_attributes_are_preserved(self): + self.create_template( + "cotton/preserved.html", + """
{{ slot }}
""", + ) + + self.create_template( + "preserved_view.html", + """ + + """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertTrue( + """{ + attr1: 'im an attr', + var1: 'im a var', + method() { + return 'im a method'; + } + }""" + in response.content.decode() + ) + + def test_attributes_that_end_or_start_with_quotes_are_preserved(self): + self.create_template( + "cotton/preserve_quotes.html", + """ +
+ """, + ) + + self.create_template( + "preserve_quotes_view.html", + """ + + """, + "view/", + ) + + # Override URLconf + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + + self.assertContains(response, '''"var ? 'this' : 'that'"''') + + def test_expression_tags_close_to_tag_elements_doesnt_corrupt_the_tag(self): + html = """ +
+ """ + + rendered = get_compiled(html) + + self.assertFalse("" in rendered, "Tag corrupted") + self.assertTrue("
" in rendered, "
not found in rendered string") + + def test_conditionals_evaluation_inside_tags(self): + self.create_template("cotton/conditionals_in_tags.html", """
{{ slot }}
""") + self.create_template( + "conditionals_in_tags_view.html", + """ + + + + """, + "view/", + context={"my_obj": {"selection": 1}}, + ) + with self.settings(ROOT_URLCONF=self.url_conf()): + response = self.client.get("/view/") + self.assertContains(response, '') + self.assertNotContains(response, '') diff --git a/django_cotton/tests/utils.py b/django_cotton/tests/utils.py index 50273b2..36fde4f 100644 --- a/django_cotton/tests/utils.py +++ b/django_cotton/tests/utils.py @@ -1,8 +1,106 @@ -from django.template import Context, Template +import os +import sys +import shutil +import tempfile +from django.urls import path +from django.conf import settings +from django.test import TestCase +from django.core.cache import cache +from django.test import override_settings +from django.template import Context, Template +from django.views.generic import TemplateView from django_cotton.cotton_loader import Loader as CottonLoader +class DynamicURLModule: + def __init__(self): + self.urlpatterns = [] + + def __call__(self): + return self.urlpatterns + + +class FileAlreadyExistsError(Exception): + pass + + +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 + + # 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 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 tearDown(self): + """Clear cache between tests so that we can use the same file names for simplicity""" + cache.clear() + + def create_template(self, name, content, url=None, context={}): + """Create a template file in the temporary directory and return the path""" + path = os.path.join(self.temp_dir, name) + + if os.path.exists(path): + raise FileAlreadyExistsError( + f"A file named '{name}' already exists in the temporary directory." + ) + + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + f.write(content) + + if url: + # Create a dynamic class-based view + class DynamicTemplateView(TemplateView): + template_name = name + + def get_context_data(self, **kwargs): + dynamic_context = super().get_context_data(**kwargs) + dynamic_context.update(context) + return dynamic_context + + self.register_path(url, DynamicTemplateView.as_view(template_name=name)) + + return path + + def make_view(self, template_name): + """Make a view that renders the given template""" + return TemplateView.as_view(template_name=template_name) + + def register_path(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 + + def setUp(self): + super().setUp() + self.url_module.urlpatterns = [] + + def url_conf(self): + return self.url_module_name + + def get_compiled(template_string): return CottonLoader(engine=None).cotton_compiler.process(template_string, "test_key") diff --git a/django_cotton/urls.py b/django_cotton/urls.py index 73dd31f..cc28b58 100644 --- a/django_cotton/urls.py +++ b/django_cotton/urls.py @@ -1,58 +1,13 @@ from . import views from django.urls import path -from django.contrib import admin from django.views.generic import TemplateView app_name = "django_cotton" -class NamedSlotInLoop(TemplateView): - template_name = "named_slot_in_loop.html" - - def get_context_data(self, **kwargs): - return { - "items": [ - {"name": "Item 1"}, - {"name": "Item 2"}, - {"name": "Item 3"}, - ] - } - - urlpatterns = [ - path("admin/", admin.site.urls), - path("", TemplateView.as_view(template_name="index.html")), - path("parent", TemplateView.as_view(template_name="parent_test.html")), - path("child", TemplateView.as_view(template_name="child_test.html")), - path( - "self-closing", - TemplateView.as_view(template_name="self_closing_test.html"), - ), path("include", TemplateView.as_view(template_name="cotton_include.html")), - path("playground", TemplateView.as_view(template_name="playground.html")), - path("tag", TemplateView.as_view(template_name="tag.html")), - path("named-slot-in-loop", NamedSlotInLoop.as_view()), path("test/compiled-cotton", views.compiled_cotton_test_view), - path("test/cotton", views.cotton_test_view), path("test/native-extends", views.native_extends_test_view), path("test/native-include", views.native_include_test_view), - path("attribute-merging", views.attribute_merging_test_view), - path("attribute-passing", views.attribute_passing_test_view), - path("django-syntax-decoding", views.django_syntax_decoding_test_view), - path( - "string-with-spaces", - TemplateView.as_view(template_name="string_with_spaces.html"), - ), - path("vars-test", TemplateView.as_view(template_name="vars_test.html")), - path("variable-parsing", views.variable_parsing_test_view), - path("test/eval-vars", views.eval_vars_test_view), - path("test/eval-attributes", views.eval_attributes_test_view), - path( - "test/native-tags-in-attributes", - TemplateView.as_view(template_name="native_tags_in_attributes_view.html"), - ), - path( - "test/unspecified-app-directory-template", - TemplateView.as_view(template_name="unspecified_view.html"), - ), ] diff --git a/django_cotton/views.py b/django_cotton/views.py index d61bfd5..f9d30ca 100644 --- a/django_cotton/views.py +++ b/django_cotton/views.py @@ -1,50 +1,14 @@ from django.shortcuts import render -# benchmark tests - +# benchmarks def compiled_cotton_test_view(request): return render(request, "compiled_cotton_test.html") -def cotton_test_view(request): - return render(request, "cotton_test.html") - - def native_extends_test_view(request): return render(request, "native_extends_test.html") def native_include_test_view(request): return render(request, "native_include_test.html") - - -# Django tests - - -def attribute_merging_test_view(request): - return render(request, "attribute_merging_test.html") - - -def attribute_passing_test_view(request): - return render(request, "attribute_passing_test.html") - - -def django_syntax_decoding_test_view(request): - return render(request, "django_syntax_decoding_test.html") - - -def variable_parsing_test_view(request): - return render(request, "variable_parsing_test.html", {"variable": "some-class"}) - - -def valueless_attributes_test_view(request): - return render(request, "valueless_attributes_test_view.html") - - -def eval_vars_test_view(request): - return render(request, "eval_vars_test_view.html") - - -def eval_attributes_test_view(request): - return render(request, "eval_attributes_test_view.html")