This commit is contained in:
Will Abbott 2024-06-19 21:56:26 +01:00
parent 553afeab44
commit e83200401a
12 changed files with 116 additions and 12 deletions

View file

@ -3,6 +3,8 @@
![Build Status](https://img.shields.io/badge/build-passing-brightgreen)
![License](https://img.shields.io/badge/license-MIT-blue)
**Whilst we are still in 0.9.x versions, there could be breaking changes.**
Bringing component-based design to Django templates.
- <a href="https://django-cotton.com" target="_blank">Document site</a>

View file

@ -179,6 +179,11 @@ class Loader(BaseLoader):
for var, value in c_vars.attrs.items():
if value is None:
vars_with_defaults.append(f"{var}={var}")
elif var.startswith(":"):
# If ':' is present, the user wants to parse a literal string as the default value,
# i.e. "['a', 'b']", "{'a': 'b'}", "True", "False", "None" or "1".
var = var[1:] # Remove the ':' prefix
vars_with_defaults.append(f'{var}={var}|eval_default:"{value}"')
else:
# Assuming value is already a string that represents the default value
vars_with_defaults.append(f'{var}={var}|default:"{value}"')
@ -259,7 +264,7 @@ class CottonTemplateCacheHandler:
"""Handles caching of cotton templates so the html parsing is only done on first load of each view or component."""
def __init__(self):
self.enabled = getattr(settings, "TEMPLATE_CACHING_ENABLED", True)
self.enabled = getattr(settings, "COTTON_TEMPLATE_CACHING_ENABLED", True)
def get_cache_key(self, template_name, mtime):
template_hash = hashlib.sha256(template_name.encode()).hexdigest()

View file

@ -0,0 +1,23 @@
{% 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 %}

View file

@ -0,0 +1,25 @@
<c-vars :none="None" :number="1" :boolean_true="True" :boolean_false="False" :dict="{key: 'value'}" :list="[1, 2, 3]" />
{% 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 %}

View file

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

View file

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

View file

@ -6,10 +6,12 @@ register = template.Library()
def cotton_vars_frame(parser, token):
"""The job of the vars frame is to filter component kwargs (attributes) against declared vars. Because we
desire to declare vars (<c-vars />) inside the same component that wants the vars in their context andbecause the
component can not manipulate its own context from within it's own template, instead we wrap the vars frame around
the contents of the component"""
"""The job of the vars frame is:
1. to filter out attributes declared as vars inside {{ attrs }} string.
2. to provide default values to attributes. We want to be able to declare these in the same file as the component
definition. Because we're effecting variables inside the same component, which is not possible usually, we we wrap
the vars frame around the contents of the component so we can govern the attributes and vars that are available.
"""
bits = token.split_contents()[1:] # Skip the tag name
# Parse token kwargs while maintaining token order
@ -27,21 +29,21 @@ class CottonVarsFrameNode(template.Node):
def render(self, context):
# Assume 'attrs' are passed from the parent and are available in the context
parent_attrs = context.get("attrs_dict", {})
component_attrs = context.get("attrs_dict", {})
# Initialize vars based on the frame's kwargs and parent attrs
vars = {}
for key, value in self.kwargs.items():
# Attempt to resolve each kwarg value (which may include template variables)
resolved_value = value.resolve(context)
# Check if the var exists in parent attrs; if so, use it, otherwise use the resolved default
if key in parent_attrs:
vars[key] = parent_attrs[key]
# Check if the var exists in component attrs; if so, use it, otherwise use the resolved default
if key in component_attrs:
vars[key] = component_attrs[key]
else:
# Attempt to resolve each kwarg value (which may include template variables)
resolved_value = value.resolve(context)
vars[key] = resolved_value
# Overwrite 'attrs' in the local context by excluding keys that are identified as vars
attrs_without_vars = {k: v for k, v in parent_attrs.items() if k not in vars}
attrs_without_vars = {k: v for k, v in component_attrs.items() if k not in vars}
context["attrs_dict"] = attrs_without_vars
# Provide all of the attrs as a string to pass to the component

View file

@ -4,6 +4,7 @@ from django.utils.html import format_html_join
from django_cotton.templatetags._component import cotton_component
from django_cotton.templatetags._slot import cotton_slot
from django_cotton.templatetags._vars_frame import cotton_vars_frame
from django_cotton.utils import eval_string
register = template.Library()
register.tag("cotton_component", cotton_component)
@ -22,3 +23,8 @@ def merge(attrs, args):
else:
attrs[key] = value
return format_html_join(" ", '{0}="{1}"', attrs.items())
@register.filter
def eval_default(value, arg):
return value or eval_string(arg)

View file

@ -102,3 +102,21 @@ class CottonTestCase(TestCase):
response = self.client.get("/test/valueless-attributes")
self.assertContains(response, "It's True")
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")
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")

View file

@ -43,4 +43,6 @@ urlpatterns = [
),
path("vars-test", TemplateView.as_view(template_name="vars_test.cotton.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),
]

11
django_cotton/utils.py Normal file
View file

@ -0,0 +1,11 @@
import ast
def eval_string(value):
"""
Evaluate a string representation of a constant, list, or dictionary to the actual Python object.
"""
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError):
return value

View file

@ -42,3 +42,11 @@ def variable_parsing_test_view(request):
def valueless_attributes_test_view(request):
return render(request, "valueless_attributes_test_view.cotton.html")
def eval_vars_test_view(request):
return render(request, "eval_vars_test_view.cotton.html")
def eval_attributes_test_view(request):
return render(request, "eval_attributes_test_view.cotton.html")