mirror of
https://github.com/wrabit/django-cotton.git
synced 2025-07-24 09:53:48 +00:00
completed tests, version bump, check render test
This commit is contained in:
parent
37e49caba5
commit
5d5d3aa6af
8 changed files with 47 additions and 137 deletions
|
@ -44,6 +44,7 @@ def configure_django():
|
|||
},
|
||||
],
|
||||
DEBUG=False,
|
||||
COTTON_ENABLE_CONTEXT_ISOLATION=False,
|
||||
)
|
||||
|
||||
import django
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",]
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue