This commit is contained in:
Will Abbott 2024-09-07 17:36:14 +01:00
parent 514be20faf
commit f1638be9c4
52 changed files with 986 additions and 928 deletions

View file

@ -1,3 +0,0 @@
<c-parent>
<c-child>d</c-child>
</c-parent>

View file

@ -1,8 +1,8 @@
<c-benchmarks.partials.main>
I'm default
<c-slot name="top">
I'm top
</c-slot>
I'm default
<c-slot name="bottom">
I'm bottom
</c-slot>

View file

@ -1 +0,0 @@
<div class="i-am-child"></div>

View file

@ -1,3 +0,0 @@
<div>
{{ name }}
</div>

View file

@ -1,3 +0,0 @@
<div class="i-am-parent">
{{ slot }}
</div>

View file

@ -1,12 +0,0 @@
<c-vars var1="sds" prop_with_default="1" />
<div>
{{ testy }}
<p>var1: '{{ var1 }}'</p>
<p>attr1: '{{ attr1 }}'</p>
<p>empty_var: '{{ empty_var }}'</p>
<p>var_with_default: '{{ var_with_default }}'</p>
<p>slot: '{{ slot }}'</p>
<p>named_slot: '{{ named_slot }}'</p>
<p>attrs: '{{ attrs }}'</p>
</div>

View file

@ -1,3 +0,0 @@
<c-parent>
<c-forms.input name="test" style="width: 100%" silica:model="first_name"/>
</c-parent>

View file

@ -1,3 +0,0 @@
{% for i in '123456789' %}
<c-parent />
{% endfor %}

View file

@ -1,7 +0,0 @@
{% for item in items %}
<c-named-slot-component>
<c-slot name="name">
item name: {{ item.name }}
</c-slot>
</c-named-slot-component>
{% endfor %}

View file

@ -1 +0,0 @@
<c-parent></c-parent>

View file

@ -1,4 +0,0 @@
{% load static %}
<c-parent/>

View file

@ -1 +1 @@
{% for d in data %}<c-test :name="'Taylor'" class="mt-4" />{% endfor %}
{% for d in data %}<c-benchmarks.cotton-include />{% endfor %}

View file

@ -1 +1 @@
{% for d in data %}<h2>{{ d }}</h2>{% endfor %}
{% for d in data %}<h2>{% include 'benchmarks/native_include.html' %}</h2>{% endfor %}

View file

@ -1,3 +0,0 @@
<c-vars-test-component var1="im a var" attr1="im an attr">
default slot
</c-vars-test-component>

View file

@ -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")

View file

@ -0,0 +1 @@
My template path was not specified in settings!

View file

@ -1 +0,0 @@
My template was not specified in settings!

View file

@ -1 +0,0 @@
<c-unspecified-component />

View file

@ -1,3 +0,0 @@
<c-merges-attributes class="extra-class" silica:model="test" another="test">
ss
</c-merges-attributes>

View file

@ -1,3 +0,0 @@
<c-receives-attributes attribute_1="hello" and-another="woo1" thirdForLuck="yes">
ss
</c-receives-attributes>

View file

@ -1,3 +0,0 @@
<c-parent>
<c-child>d</c-child>
</c-parent>

View file

@ -1 +0,0 @@
<div class="i-am-child"></div>

View file

@ -1,11 +0,0 @@
<div>
Header:
{{ header }}
</div>
<div>
Content:
{{ slot }}
</div>

View file

@ -1,27 +0,0 @@
{% if none is None %}
<p>none is None</p>
{% endif %}
{% if number == 1 %}
<p>number is 1</p>
{% endif %}
{% if boolean_true is True %}
<p>boolean_true is True</p>
{% endif %}
{% if boolean_false is False %}
<p>boolean_false is False</p>
{% endif %}
{% if dict.key == 'value' %}
<p>dict.key is 'value'</p>
{% endif %}
{% if list.0 == 1 %}
<p>list.0 is 1</p>
{% endif %}
{% if listdict.0.key == 'value' %}
<p>listdict.0.key is 'value'</p>
{% endif %}

View file

@ -1,29 +0,0 @@
<c-vars :none="None" :number="1" :boolean_true="True" :boolean_false="False" :dict="{'key': 'value'}" :list="[1, 2, 3]" :listdict="[{'key': 'value'}]" />
{% if none is None %}
<p>none is None</p>
{% endif %}
{% if number == 1 %}
<p>number is 1</p>
{% endif %}
{% if boolean_true is True %}
<p>boolean_true is True</p>
{% endif %}
{% if boolean_false is False %}
<p>boolean_false is False</p>
{% endif %}
{% if dict.key == 'value' %}
<p>dict.key is 'value'</p>
{% endif %}
{% if list.0 == 1 %}
<p>list.0 is 1</p>
{% endif %}
{% if listdict.0.key == 'value' %}
<p>listdict.0.key is 'value'</p>
{% endif %}

View file

@ -1,3 +0,0 @@
<div {{ attrs_dict|merge:'class:form-group another-class-with:colon' }}>
</div>

View file

@ -1,3 +0,0 @@
<div>
{{ name }}
</div>

View file

@ -1,4 +0,0 @@
Attribute 1 says: '{{ attr1 }}'
Attribute 2 says: '{{ attr2 }}'
Attribute 3 says: '{{ attr3 }}'
attrs tag is: '{{ attrs }}'

View file

@ -1,3 +0,0 @@
<div class="i-am-parent">
{{slot}}
</div>

View file

@ -1,3 +0,0 @@
<div {{ attrs }}>
</div>

View file

@ -1,13 +0,0 @@
<c-vars var1 default_var="default var" />
<p>slot: '{{ slot }}'</p>
<p>attr1: '{{ attr1 }}'</p>
<p>attr2: '{{ attr2 }}'</p>
<p>var1: '{{ var1 }}'</p>
<p>default_var: '{{ default_var }}'</p>
<p>named_slot: '{{ named_slot }}'</p>
<p>attrs: '{{ attrs }}'</p>

View file

@ -1 +0,0 @@
<div class="{% if 1 < 2 %} some-class {% endif %}">Hello, World!</div>

View file

@ -1,9 +0,0 @@
<c-eval-attributes-test-component
:none="None"
:number="1"
:boolean_true="True"
:boolean_false="False"
:dict="{'key': 'value'}"
:list="[1, 2, 3]"
:listdict="[{'key': 'value'}]"
/>

View file

@ -1 +0,0 @@
<c-eval-vars-test-component />

View file

@ -1,3 +0,0 @@
<c-parent>
<c-forms.input name="test" style="width: 100%" silica:model="first_name"/>
</c-parent>

View file

@ -1,7 +0,0 @@
{% for item in items %}
<c-named-slot-component>
<c-slot name="name">
item name: {{ item.name }}
</c-slot>
</c-named-slot-component>
{% endfor %}

View file

@ -1,12 +0,0 @@
<c-vars test="world" name="Will"></c-vars>
<!-- create different types of native tag examples -->
<c-native-tags-in-attributes
attr1="Hello {{ name }}"
attr2="{{ test|default:"none" }}"
attr3="{% if 1 == 1 %}cowabonga!{% endif %}"
normal="normal"
>
<c-slot name="named">test</c-slot>
</c-native-tags-in-attributes>

View file

@ -1 +0,0 @@
<c-parent></c-parent>

View file

@ -1,4 +0,0 @@
{% load static %}
<c-parent/>

View file

@ -1,5 +0,0 @@
<c-test-component var1="string with space" attr1="I have spaces">
<c-slot name="named_slot">
named_slot with spaces
</c-slot>
</c-test-component>

View file

@ -1,2 +0,0 @@
<c-test-component attr1="variable" :attr2="variable">
</c-test-component>

View file

@ -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):
"""

View file

@ -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

View file

@ -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",
"""
<c-dynamic-attributes-component
:none="None"
:number="1"
:boolean_true="True"
:boolean_false="False"
:dict="{'key': 'value'}"
:list="[1, 2, 3]"
:listdict="[{'key': 'value'}]"
:variable="variable"
:template_string_lit="{'dummy': {{ dummy }}}"
/>
""",
"view/",
context={"variable": 111, "dummy": 222},
)
self.create_template(
"cotton/dynamic_attributes_component.html",
"""
{% if none is None %}
<p>none is None</p>
{% endif %}
{% if number == 1 %}
<p>number is 1</p>
{% endif %}
{% if boolean_true is True %}
<p>boolean_true is True</p>
{% endif %}
{% if boolean_false is False %}
<p>boolean_false is False</p>
{% endif %}
{% if dict.key == 'value' %}
<p>dict.key is 'value'</p>
{% endif %}
{% if list.0 == 1 %}
<p>list.0 is 1</p>
{% endif %}
{% if listdict.0.key == 'value' %}
<p>listdict.0.key is 'value'</p>
{% endif %}
{% if variable == 111 %}
<p>variable is 111</p>
{% endif %}
{% if template_string_lit.dummy == 222 %}
<p>template_string_lit.dummy is 222</p>
{% 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",
"""
<c-dynamic-attributes-cvars />
""",
"view/",
)
self.create_template(
"cotton/dynamic_attributes_cvars.html",
"""
<c-vars
:none="None"
:number="1"
:boolean_true="True"
:boolean_false="False"
:dict="{'key': 'value'}"
:list="[1, 2, 3]"
:listdict="[{'key': 'value'}]"
:variable="111"
/>
{% if none is None %}
<p>none is None</p>
{% endif %}
{% if number == 1 %}
<p>number is 1</p>
{% endif %}
{% if boolean_true is True %}
<p>boolean_true is True</p>
{% endif %}
{% if boolean_false is False %}
<p>boolean_false is False</p>
{% endif %}
{% if dict.key == 'value' %}
<p>dict.key is 'value'</p>
{% endif %}
{% if list.0 == 1 %}
<p>list.0 is 1</p>
{% endif %}
{% if listdict.0.key == 'value' %}
<p>listdict.0.key is 'value'</p>
{% endif %}
{% if variable == 111 %}
<p>variable is 111</p>
{% 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",
"""
<div {% if something %} class="first" {% else %} class="second" {% endif %}><div>
""",
)
self.create_template(
"attribute_govern_view.html",
"""
<c-attribute-govern :something="False" />
""",
"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",
"""
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
""",
)
self.create_template(
"hyphens_view.html",
"""
<c-hyphens x-data="{}" x-init="do_something()" />
""",
"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",
"""
<c-vars x-data="{}" x-init="do_something()" />
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
""",
)
self.create_template(
"cvar_hyphens_view.html",
"""
<c-cvar-hyphens />
""",
"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",
"""
<div {{ attrs }}><div>
""",
)
self.create_template(
"equals_view.html",
"""
<c-equals
@click="this=test"
/>
""",
"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",
"""
<div some_attribute_{{ id }}_something="true"></div>
""",
"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",
"""
<c-boolean-attribute is_something />
""",
"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",
"""
<c-static-attrs something="{{ something }}" />
""",
"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",
"""
<c-vars color="gray" />
{{ color }}
""",
)
self.create_template(
"unprocessable_dynamic_attribute_view.html",
"""
<c-unprocessable-dynamic-attribute :color="button.color" />
""",
"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",
"""
<div {{ attrs_dict|merge:'class:form-group another-class-with:colon' }}></div>
""",
)
self.create_template(
"attribute_merging_view.html",
"""
<c-merges-attributes class="extra-class" silica:model="test" another="test">ss</c-merges-attributes>
""",
"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",
"""
<c-native-tags-in-attributes
attr1="Hello {{ name }}"
attr2="{{ test|default:"none" }}"
attr3="{% if 1 == 1 %}cowabonga!{% endif %}"
>
<c-slot name="named">test</c-slot>
</c-native-tags-in-attributes>
""",
"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",
"""
<c-string-test var1="string with space" attr1="I have spaces">
<c-slot name="named_slot">
named_slot with spaces
</c-slot>
</c-string-test>
""",
"view/",
)
self.create_template(
"cotton/string_test.html",
"""
<c-vars var1 default_var="default var" />
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",
"""
<c-cvars-test-component var1="im a var" attr1="im an attr">
default slot
</c-cvars-test-component>
""",
"view/",
)
self.create_template(
"cotton/cvars_test_component.html",
"""
<c-vars var1="sds" prop_with_default="1" />
<div>
{{ testy }}
<p>var1: '{{ var1 }}'</p>
<p>attr1: '{{ attr1 }}'</p>
<p>empty_var: '{{ empty_var }}'</p>
<p>var_with_default: '{{ var_with_default }}'</p>
<p>slot: '{{ slot }}'</p>
<p>named_slot: '{{ named_slot }}'</p>
<p>attrs: '{{ attrs }}'</p>
</div>
""",
)
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",
"""
<c-attribute-passing attribute_1="hello" and-another="woo1" thirdForLuck="yes" />
""",
"view/",
)
self.create_template("cotton/attribute_passing.html", """<div {{ attrs }}></div>""")
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertContains(
response, '<div attribute_1="hello" and-another="woo1" thirdforluck="yes">'
)
def test_loader_preserves_duplicate_attributes(self):
compiled = get_compiled("""<a href="#" class="test" class="test2">hello</a>""")
self.assertEquals(
compiled,
"""<a href="#" class="test" class="test2">hello</a>""",
)

View file

@ -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",
"""<div class="i-am-component">{{ slot }}</div>""",
)
self.create_template(
"view.html",
"""<c-render>Hello, World!</c-render>""",
"view/",
)
# Override URLconf
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertContains(response, '<div class="i-am-component">')
self.assertContains(response, "Hello, World!")
def test_nested_rendering(self):
self.create_template(
"cotton/parent.html",
"""
<div class="i-am-parent">
{{ slot }}
</div>
""",
)
self.create_template(
"cotton/child.html",
"""
<div class="i-am-child"></div>
""",
)
self.create_template(
"cotton/nested_render_view.html",
"""
<c-parent>
<c-child>d</c-child>
</c-parent>
""",
"view/",
)
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertContains(response, '<div class="i-am-parent">')
self.assertContains(response, '<div class="i-am-child">')
def test_cotton_directory_can_be_configured(self):
custom_dir = "components"
self.create_template(
f"{custom_dir}/custom_directory.html",
"""<div class="i-am-component">{{ slot }}</div>""",
)
self.create_template(
"custom_directory_view.html",
"""<c-custom-directory>Hello, World!</c-custom-directory>""",
"view/",
)
# Override URLconf
with self.settings(ROOT_URLCONF=self.url_conf(), COTTON_DIR=custom_dir):
response = self.client.get("/view/")
self.assertContains(response, '<div class="i-am-component">')
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: <c-self-closing/>
2: <c-self-closing />
3: <c-self-closing />
""",
"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", """<c-app-outside-of-dirs />""", "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!""",
)

View file

@ -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",
"""<div class="i-am-component">{{ slot }}</div>""",
)
self.create_template(
"view.html",
"""<c-render>Hello, World!</c-render>""",
"view/",
)
# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
response = self.client.get("/view/")
self.assertContains(response, '<div class="i-am-component">')
self.assertContains(response, "Hello, World!")
def test_new_lines_in_attributes_are_preserved(self):
self.create_template(
"cotton/preserved.html",
"""<div {{ attrs }}>{{ slot }}</div>""",
)
self.create_template(
"preserved_view.html",
"""
<c-preserved x-data="{
attr1: 'im an attr',
var1: 'im a var',
method() {
return 'im a method';
}
}" />
""",
"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",
"""
<div {{ attrs }}><div>
""",
)
self.create_template(
"preserve_quotes_view.html",
"""
<c-preserve-quotes something="var ? 'this' : 'that'" />
""",
"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",
"""
<div {% if something %} class="first" {% else %} class="second" {% endif %}><div>
""",
)
self.create_template(
"attribute_govern_view.html",
"""
<c-attribute-govern :something="False" />
""",
"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",
"""
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
""",
)
self.create_template(
"hyphens_view.html",
"""
<c-hyphens x-data="{}" x-init="do_something()" />
""",
"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",
"""
<c-vars x-data="{}" x-init="do_something()" />
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
""",
)
self.create_template(
"cvar_hyphens_view.html",
"""
<c-cvar-hyphens />
""",
"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",
"""<div class="i-am-component">{{ slot }}</div>""",
)
self.create_template(
"custom_directory_view.html",
"""<c-custom-directory>Hello, World!</c-custom-directory>""",
"view/",
)
# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf(), COTTON_DIR=custom_dir):
response = self.client.get("/view/")
self.assertContains(response, '<div class="i-am-component">')
self.assertContains(response, "Hello, World!")
def test_equals_in_attribute_values(self):
self.create_template(
"cotton/equals.html",
"""
<div {{ attrs }}><div>
""",
)
self.create_template(
"equals_view.html",
"""
<c-equals
@click="this=test"
/>
""",
"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",
"""
<div>I am dynamic<div>
""",
)
html = """
<c-component is="dynamic-component" />
"""
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",
"""
<div>I am dynamic<div>
""",
)
html = """
<c-component :is="is" />
"""
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",
"""
<div>I am dynamic component from expression<div>
""",
)
html = """
<c-component is="dynamic-{{ is }}" />
"""
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",
"""
<div>I am dynamic component from expression<div>
""",
)
html = """
<c-component is="subfolder.{{ is }}" />
"""
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",
"""
<div some_attribute_{{ id }}_something="true"></div>
""",
"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",
"""
<c-dynamic-attribute-template-parsing :images="['{{ image1 }}', '{{ image2 }}']" />
""",
"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",
"""
<c-boolean-attribute is_something />
""",
"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",
"""
<c-static-attrs something="{{ something }}" />
""",
"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",
"""
<c-vars color="gray" />
{{ color }}
""",
)
self.create_template(
"unprocessable_dynamic_attribute_view.html",
"""
<c-unprocessable-dynamic-attribute :color="button.color" />
""",
"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, '<div class="i-am-parent">')
def test_child_is_rendered(self):
response = self.client.get("/child")
self.assertContains(response, '<div class="i-am-parent">')
self.assertContains(response, '<div class="i-am-child">')
def test_self_closing_is_rendered(self):
response = self.client.get("/self-closing")
self.assertContains(response, '<div class="i-am-parent">')
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, '<div attribute_1="hello" and-another="woo1" thirdforluck="yes">'
)
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(
"""
<c-vars var1="string with space" />
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("""<a href="#" class="test" class="test2">hello</a>""")
self.assertEquals(
compiled,
"""<a href="#" class="test" class="test2">hello</a>""",
)
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 = """
<c-test-component>
component1
<c-slot name="named_slot">named slot 1</c-slot>
</c-test-component>
<c-test-component>
component2
</c-test-component>
"""
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 = """
<c-test-component attr1="variable" :attr2="variable">
<c-slot name="named_slot">
<a href="#" silica:click.prevent="variable = 'lineage'">test</a>
</c-slot>
</c-test-component>
"""
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 = """
<div{% if 1 = 1 %} attr1="variable" {% endif %}></div>
"""
rendered = get_compiled(html)
self.assertFalse("</div{% if 1 = 1 %}>" in rendered, "Tag corrupted")
self.assertTrue("</div>" in rendered, "</div> not found in rendered string")
def test_conditionals_evaluation_inside_elements(self):
html = """
<c-test-component>
<select>
<option value="1" {% if my_obj.selection == 1 %}selected{% endif %}>Value 1</option>
<option value="2" {% if my_obj.selection == 2 %}selected{% endif %}>Value 2</option>
</select>
</c-test-component>
"""
rendered = get_rendered(html, {"my_obj": {"selection": 1}})
self.assertTrue('<option value="1" selected>Value 1</option>' in rendered)
self.assertTrue('<option value="2" selected>Value 2</option>' not in rendered)

View file

@ -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",
"""
<div>I am dynamic<div>
""",
)
html = """
<c-component is="dynamic-component-via-string" />
"""
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",
"""
<div>I am dynamic<div>
""",
)
html = """
<c-component :is="is" />
"""
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",
"""
<div>I am dynamic component from expression<div>
""",
)
html = """
<c-component is="dynamic-{{ is }}" />
"""
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",
"""
<div>I am dynamic component from expression<div>
""",
)
html = """
<c-component is="subfolder.{{ is }}" />
"""
rendered = get_rendered(html, {"is": "dynamic-component-expression"})
self.assertTrue("I am dynamic component from expression" in rendered)

View file

@ -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 %}
<c-named-slot-component>
<c-slot name="name">
item name: {{ item.name }}
</c-slot>
</c-named-slot-component>
{% endfor %}
""",
"view/",
context={
"items": [
{"name": "Item 1"},
{"name": "Item 2"},
{"name": "Item 3"},
]
},
)
self.create_template(
"cotton/named_slot_component.html",
""",
<div>
{{ name }}
</div>
""",
)
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",
"""
<c-slot-bleed id="1">
<c-slot name="named_slot">named slot 1</c-slot>
</c-slot-bleed>
<c-slot-bleed id="2"></c-slot-bleed>
""",
"view/",
)
self.create_template(
"cotton/slot_bleed.html", """named_slot {{ id }}: '{{ named_slot }}'</p>"""
)
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(
"""
<c-vars var1="string with space" />
content
"""
)
self.assertEquals(
compiled,
"""{% cotton_vars_frame var1=var1|default:"string with space" %}content{% endcotton_vars_frame %}""",
)

View file

@ -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",
"""<div {{ attrs }}>{{ slot }}</div>""",
)
self.create_template(
"preserved_view.html",
"""
<c-preserved x-data="{
attr1: 'im an attr',
var1: 'im a var',
method() {
return 'im a method';
}
}" />
""",
"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",
"""
<div {{ attrs }}><div>
""",
)
self.create_template(
"preserve_quotes_view.html",
"""
<c-preserve-quotes something="var ? 'this' : 'that'" />
""",
"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 = """
<div{% if 1 = 1 %} attr1="variable" {% endif %}></div>
"""
rendered = get_compiled(html)
self.assertFalse("</div{% if 1 = 1 %}>" in rendered, "Tag corrupted")
self.assertTrue("</div>" in rendered, "</div> not found in rendered string")
def test_conditionals_evaluation_inside_tags(self):
self.create_template("cotton/conditionals_in_tags.html", """<div>{{ slot }}</div>""")
self.create_template(
"conditionals_in_tags_view.html",
"""
<c-conditionals-in-tags>
<select>
<option value="1" {% if my_obj.selection == 1 %}selected{% endif %}>Value 1</option>
<option value="2" {% if my_obj.selection == 2 %}selected{% endif %}>Value 2</option>
</select>
</c-conditionals-in-tags>
""",
"view/",
context={"my_obj": {"selection": 1}},
)
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertContains(response, '<option value="1" selected>Value 1</option>')
self.assertNotContains(response, '<option value="2" selected>Value 2</option>')

View file

@ -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")

View file

@ -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"),
),
]

View file

@ -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")