diff --git a/dev/example_project/render_load_test.py b/dev/example_project/render_load_test.py index cfe8b1d..829079c 100644 --- a/dev/example_project/render_load_test.py +++ b/dev/example_project/render_load_test.py @@ -44,6 +44,7 @@ def configure_django(): }, ], DEBUG=False, + COTTON_ENABLE_CONTEXT_ISOLATION=False, ) import django diff --git a/django_cotton/templatetags/_component.py b/django_cotton/templatetags/_component.py index f61a7c4..2bbfea8 100644 --- a/django_cotton/templatetags/_component.py +++ b/django_cotton/templatetags/_component.py @@ -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) - - output = template.render(new_context) - - # with context.push(component_state): - # print(context.flatten()) - # output = template.render(context) + 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) + 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 diff --git a/django_cotton/tests/test_context_isolation.py b/django_cotton/tests/test_context_isolation.py index 6667260..21779e5 100644 --- a/django_cotton/tests/test_context_isolation.py +++ b/django_cotton/tests/test_context_isolation.py @@ -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: "test - """, - ) - self.create_template( - "no_only_view.html", - """ - - """, - "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", - """ - - """, - "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", - """ - - """, - "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", - """ - - """, - ) - - self.create_template( - "dynamic_only_view.html", - """""", - "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( diff --git a/docs/docs_project/docs_project/settings.py b/docs/docs_project/docs_project/settings.py index eb8570b..04c0c24 100644 --- a/docs/docs_project/docs_project/settings.py +++ b/docs/docs_project/docs_project/settings.py @@ -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", diff --git a/docs/docs_project/docs_project/templates/components.html b/docs/docs_project/docs_project/templates/components.html index 60d71d2..7e96319 100644 --- a/docs/docs_project/docs_project/templates/components.html +++ b/docs/docs_project/docs_project/templates/components.html @@ -327,21 +327,18 @@ context = { 'today': Weather.objects.get(...) }

9. Context Isolation

-

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.

- -

Therefore, each component's context only contains, by default:

+

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.

+

By default, each component's context only contains:

  • Attributes directly declared on the component
  • -
  • Any data set using `cvars`
  • -
  • All data from enabled context processors (from custom or built-in processors which typically provide: `user`, `request`, `messages`, `perms` etc.)
  • +
  • Any data set using {{ ""|force_escape }}
  • +
  • All data from enabled context processors (from custom or built-in processors which typically provide: user, request, messages, perms etc.)
  • Further isolation using 'only'

    -

    You can pass the only attribute to the component, which will prevent it from adopting any context (incl. context processors) other than it's direct attributes.

    +

    You can pass the only attribute to the component, which will prevent it from adopting any context other than its direct attributes.

    diff --git a/docs/docs_project/docs_project/templates/configuration.html b/docs/docs_project/docs_project/templates/configuration.html index 64fc051..6d85768 100644 --- a/docs/docs_project/docs_project/templates/configuration.html +++ b/docs/docs_project/docs_project/templates/configuration.html @@ -13,16 +13,6 @@ -
    -
    - COTTON_ENABLE_CONTEXT_ISOLATION -
    boolean (default: True)
    -
    -
    - Set to `False` to allow global context to be available through all components. (see context isolation for more information. -
    -
    -
    @@ -64,5 +54,22 @@
    + {# + + +
    +
    + COTTON_ENABLE_CONTEXT_ISOLATION +
    bool (default: True)
    +
    +
    + Limits component context to only direct attributes and context processor data. + When set to False, all components can access the entire view's context. + See context isolation for more details. +
    +
    + #} + + diff --git a/pyproject.toml b/pyproject.toml index bb55466..5a4c32d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 ",] 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",]