completed tests, version bump, check render test

This commit is contained in:
Will Abbott 2025-03-11 14:50:20 +00:00
parent 37e49caba5
commit 5d5d3aa6af
8 changed files with 47 additions and 137 deletions

View file

@ -44,6 +44,7 @@ def configure_django():
},
],
DEBUG=False,
COTTON_ENABLE_CONTEXT_ISOLATION=False,
)
import django

View file

@ -67,17 +67,18 @@ class CottonComponentNode(Node):
template = self._get_cached_template(context, component_data["attrs"])
# Isolate context if needed
if self.only:
# Complete isolation
output = template.render(Context(component_state))
else:
new_context = self._create_isolated_context(context, component_state)
if getattr(settings, "COTTON_ENABLE_CONTEXT_ISOLATION", True):
# Default - partial isolation
new_context = self._create_partial_context(context, component_state)
output = template.render(new_context)
# with context.push(component_state):
# print(context.flatten())
# output = template.render(context)
else:
# Legacy - no isolation
with context.push(component_state):
output = template.render(context)
cotton_data["stack"].pop()
@ -115,30 +116,20 @@ class CottonComponentNode(Node):
cache[fallback_path] = template
return template
def _create_isolated_context(self, original_context, component_state):
def _create_partial_context(self, original_context, component_state):
# Get the request object from the original context
request = original_context.get("request")
if request:
# Create a new RequestContext with only the default processors
# Create a new RequestContext
new_context = RequestContext(request)
# Update the new context with any custom context processors
# for processor in original_context.get("_processors", []):
# new_context.update(processor(request))
# Add the component_state to the new context
new_context.update(component_state)
else:
# If there's no request object, create a simple Context
new_context = Context(component_state)
# If not using 'only', include all variables from the original context
# if not self.only:
# for key, value in original_context.flatten().items():
# if key not in new_context:
# new_context[key] = value
return new_context
@staticmethod

View file

@ -100,9 +100,9 @@ class ContextIsolationTests(CottonTestCase):
self.create_template(
"cotton/receiver.html",
"""
{{ global }}
{{ direct }}
{{ from_context_processor }}
Global Scope: {{ global }}
Direct attribute: {{ direct }}
Custom context processor: {{ from_context_processor }}
Some context from django builtins:
csrf: "{{ csrf_token }}"
@ -110,7 +110,6 @@ class ContextIsolationTests(CottonTestCase):
messages: "{{ messages }}"
user: "{{ user }}"
perms: "{{ perms }}"
debug: "{{ debug }}"
""",
)
@ -124,9 +123,11 @@ class ContextIsolationTests(CottonTestCase):
# with example_processor added and 'logo' in the context
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertNotContains(response, "shouldnotbeseen")
self.assertContains(response, "hello")
self.assertContains(response, "logo.png")
self.assertNotContains(response, "Global Scope: shouldnotbeseen")
self.assertContains(response, "Direct attribute: hello")
self.assertContains(response, "Custom context processor: logo.png")
self.assertNotContains(response, 'csrf: ""')
self.assertContains(response, 'user: "AnonymousUser"')
self.assertNotContains(response, 'perms: ""')
self.assertNotContains(response, 'request: "<WSGIRequest')
self.assertNotContains(response, 'messages: ""')
self.assertContains(response, 'perms: "PermWrapper')

View file

@ -37,94 +37,6 @@ class MiscComponentTests(CottonTestCase):
"""My template path was not specified in settings!""",
)
def test_context_isolation_by_default(self):
"""Context should be isolated by default, but still include context from custom and built in processors."""
pass
def test_only_gives_isolated_context(self):
self.create_template(
"cotton/only.html",
"""
<a class="{{ class|default:"donttouch" }}">test</a>
""",
)
self.create_template(
"no_only_view.html",
"""
<c-only />
""",
"no_only_view/",
context={"class": "herebedragons"}, # this should pass to `class` successfully
)
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/no_only_view/")
self.assertNotContains(response, "donttouch")
self.assertContains(response, "herebedragons")
self.create_template(
"only_view.html",
"""
<c-only only />
""",
"only_view/",
context={
"class": "herebedragons"
}, # this should not pass to `class` due to `only` being present
)
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/only_view/")
self.assertNotContains(response, "herebedragons")
self.assertContains(response, "donttouch")
self.create_template(
"only_view2.html",
"""
<c-only class="october" only />
""",
"only_view2/",
context={
"class": "herebedragons"
}, # this should not pass to `class` due to `only` being present
)
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/only_view2/")
self.assertNotContains(response, "herebedragons")
self.assertNotContains(response, "donttouch")
self.assertContains(response, "october")
def test_only_with_dynamic_components(self):
self.create_template(
"cotton/dynamic_only.html",
"""
From parent comp scope: '{{ class }}'
From view context scope: '{{ view_item }}'
Direct attribute: '{{ direct }}'
""",
)
self.create_template(
"cotton/middle_component.html",
"""
<c-component is="{{ comp }}" only direct="yes" />
""",
)
self.create_template(
"dynamic_only_view.html",
"""<c-middle-component class="mb-5" />""",
"view/",
context={"comp": "dynamic_only", "view_item": "blee"},
)
with self.settings(ROOT_URLCONF=self.url_conf()):
response = self.client.get("/view/")
self.assertContains(response, "From parent comp scope: ''")
self.assertContains(response, "From view context scope: ''")
self.assertContains(response, "Direct attribute: 'yes'")
@override_settings(COTTON_SNAKE_CASED_NAMES=False)
def test_hyphen_naming_convention(self):
self.create_template(

View file

@ -70,6 +70,7 @@ TEMPLATES = [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
"builtins": [
"django.templatetags.static",

View file

@ -327,21 +327,18 @@ context = { 'today': Weather.objects.get(...) }
<h2>9. Context Isolation</h2>
<p>Cotton is inspired by patterns found in frontend frameworks like React, Vue and Svelte. When working with these
patterns, state is not typically shared between components. This ensures data from other components does not 'leak' into
others which can cause side effects that are difficult to trace.</p>
<p>Therefore, each component's context only contains, by default:</p>
<p>Cotton follows the component isolation approach used in React, Vue, and Svelte, where components don't share state by default. This prevents data leaks and hard-to-trace side effects between components.</p>
<p>By default, each component's context only contains:</p>
<c-ul>
<li>Attributes directly declared on the component</li>
<li>Any data set using `cvars`</li>
<li>All data from enabled context processors (from custom or built-in processors which typically provide: `user`, `request`, `messages`, `perms` etc.)</li>
<li>Any data set using <code>{{ "<c-vars />"|force_escape }}</code></li>
<li>All data from enabled context processors (from custom or built-in processors which typically provide: <code>user</code>, <code>request</code>, <code>messages</code>, <code>perms</code> etc.)</li>
</c-ul>
<h3>Further isolation using 'only'</h3>
<p>You can pass the <c-highlight>only</c-highlight> attribute to the component, which will prevent it from adopting any context (incl. context processors) other than it's direct attributes.</p>
<p>You can pass the <c-highlight>only</c-highlight> attribute to the component, which will prevent it from adopting any context other than its direct attributes.</p>
<c-hr id="alpine-js-support" />

View file

@ -13,16 +13,6 @@
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div>
<code>COTTON_ENABLE_CONTEXT_ISOLATION</code>
<div>boolean (default: True)</div>
</div>
<div>
Set to `False` to allow global context to be available through all components. (see <a href="{% url 'components' %}#context-isolation">context isolation</a> for more information.
</div>
</div>
<c-hr />
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
@ -64,5 +54,22 @@
</div>
</div>
{#
<c-hr />
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div>
<code>COTTON_ENABLE_CONTEXT_ISOLATION</code>
<div>bool (default: True)</div>
</div>
<div>
Limits component context to only direct attributes and context processor data.
When set to <code>False</code>, all components can access the entire view's context.
See <a href="{% url 'components' %}#context-isolation">context isolation</a> for more details.
</div>
</div>
#}
</c-layouts.with-sidebar>

View file

@ -4,12 +4,12 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "django-cotton"
version = "1.6.0"
version = "2.0.0"
description = "Bringing component based design to Django templates."
authors = [ "Will Abbott <willabb83@gmail.com>",]
license = "MIT"
readme = "README.md"
classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Framework :: Django",]
classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Framework :: Django",]
keywords = [ "django", "components", "ui",]
exclude = [ "dev", "docs", "django_cotton/tests", "django_cotton/templates",]