mirror of
https://github.com/django-components/django-components.git
synced 2025-09-19 20:29:44 +00:00
feat:forward context processors variables in context in ISOLATED mode (#975)
* feat:forward context processors variables in context in ISOLATED mode provide context_processors_data property to Component to access those variables in Component * refactor: internalize RequestContext and pass HttpRequest internally * docs: document HttpRequest and context processors --------- Co-authored-by: Juro Oravec <juraj.oravec.josefson@gmail.com>
This commit is contained in:
parent
5535f3bad8
commit
8b5579d2be
7 changed files with 653 additions and 18 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -1,5 +1,29 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
|
## v0.130
|
||||||
|
|
||||||
|
#### Feat
|
||||||
|
|
||||||
|
- Access the HttpRequest object under `Component.request`.
|
||||||
|
|
||||||
|
To pass the request object to a component, either:
|
||||||
|
- Render a template or component with `RequestContext`,
|
||||||
|
- Or set the `request` kwarg to `Component.render()` or `Component.render_to_response()`.
|
||||||
|
|
||||||
|
Read more on [HttpRequest](https://django-components.github.io/django-components/0.130/concepts/fundamentals/http_request/).
|
||||||
|
|
||||||
|
- Access the context processors data under `Component.context_processors_data`.
|
||||||
|
|
||||||
|
Context processors data is available only when the component has access to the `request` object,
|
||||||
|
either by:
|
||||||
|
- Passing the request to `Component.render()` or `Component.render_to_response()`,
|
||||||
|
- Or by rendering a template or component with `RequestContext`,
|
||||||
|
- Or being nested in another component that has access to the request object.
|
||||||
|
|
||||||
|
The data from context processors is automatically available within the component's template.
|
||||||
|
|
||||||
|
Read more on [HttpRequest](https://django-components.github.io/django-components/0.130/concepts/fundamentals/http_request/).
|
||||||
|
|
||||||
## v0.129
|
## v0.129
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
101
docs/concepts/fundamentals/http_request.md
Normal file
101
docs/concepts/fundamentals/http_request.md
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
---
|
||||||
|
title: HTTP Request
|
||||||
|
weight: 11
|
||||||
|
---
|
||||||
|
|
||||||
|
The most common use of django-components is to render HTML for a given request. As such,
|
||||||
|
there are a few features that are dependent on the request object.
|
||||||
|
|
||||||
|
## Passing and accessing HttpRequest
|
||||||
|
|
||||||
|
In regular Django templates, the request object is available only within the `RequestContext`.
|
||||||
|
|
||||||
|
In Components, you can either use `RequestContext`, or pass the `request` object
|
||||||
|
explicitly to [`Component.render()`](../../../reference/api#django_components.Component.render) and
|
||||||
|
[`Component.render_to_response()`](../../../reference/api#django_components.Component.render_to_response).
|
||||||
|
|
||||||
|
When a component is nested in another, the child component uses parent's `request` object.
|
||||||
|
|
||||||
|
You can access the request object under [`Component.request`](../../../reference/api#django_components.Component.request):
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_context_data(self):
|
||||||
|
return {
|
||||||
|
'user_id': self.request.GET['user_id'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# ✅ With request
|
||||||
|
MyComponent.render(request=request)
|
||||||
|
MyComponent.render(context=RequestContext(request, {}))
|
||||||
|
|
||||||
|
# ❌ Without request
|
||||||
|
MyComponent.render()
|
||||||
|
MyComponent.render(context=Context({}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context Processors
|
||||||
|
|
||||||
|
Components support Django's [context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
|
||||||
|
|
||||||
|
In regular Django templates, the context processors are applied only when the template is rendered with `RequestContext`.
|
||||||
|
|
||||||
|
Components allow you to pass the `request` object explicitly. Thus, the context processors are applied to components either when:
|
||||||
|
|
||||||
|
- The component is rendered with `RequestContext` (Regular Django behavior)
|
||||||
|
- The component is rendered with a regular `Context` (or none), but you set the `request` kwarg
|
||||||
|
of [`Component.render()`](../../../reference/api#django_components.Component.render).
|
||||||
|
- The component is nested in another component that matches one of the two conditions above.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ No context processors
|
||||||
|
rendered = MyComponent.render()
|
||||||
|
rendered = MyComponent.render(Context({}))
|
||||||
|
|
||||||
|
# ✅ With context processors
|
||||||
|
rendered = MyComponent.render(request=request)
|
||||||
|
rendered = MyComponent.render(Context({}), request=request)
|
||||||
|
rendered = MyComponent.render(RequestContext(request, {}))
|
||||||
|
```
|
||||||
|
|
||||||
|
When a component is rendered within a template with [`{% component %}`](../../../reference/template_tags#component) tag, context processors are available depending on whether the template is rendered with `RequestContext` or not.
|
||||||
|
|
||||||
|
```python
|
||||||
|
template = Template("""
|
||||||
|
<div>
|
||||||
|
{% component "MyComponent" / %}
|
||||||
|
</div>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# ❌ No context processors
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
# ✅ With context processors
|
||||||
|
rendered = template.render(RequestContext(request, {}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing context processors data
|
||||||
|
|
||||||
|
The data from context processors is automatically available within the component's template.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyComponent(Component):
|
||||||
|
template = """
|
||||||
|
<div>
|
||||||
|
{{ csrf_token }}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
MyComponent.render(request=request)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also access the context processors data from within [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data) and other methods under [`Component.context_processors_data`](../../../reference/api#django_components.Component.context_processors_data).
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_context_data(self):
|
||||||
|
csrf_token = self.context_processors_data['csrf_token']
|
||||||
|
return {
|
||||||
|
'csrf_token': csrf_token,
|
||||||
|
}
|
||||||
|
```
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: Subclassing components
|
title: Subclassing components
|
||||||
weight: 11
|
weight: 12
|
||||||
---
|
---
|
||||||
|
|
||||||
In larger projects, you might need to write multiple components with similar behavior.
|
In larger projects, you might need to write multiple components with similar behavior.
|
||||||
|
|
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1434" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1549" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ can access only the data that was explicitly passed to it:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L577" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L612" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ from django_components.slots import (
|
||||||
)
|
)
|
||||||
from django_components.template import cached_template
|
from django_components.template import cached_template
|
||||||
from django_components.util.component_highlight import apply_component_highlight
|
from django_components.util.component_highlight import apply_component_highlight
|
||||||
from django_components.util.context import snapshot_context
|
from django_components.util.context import gen_context_processors_data, snapshot_context
|
||||||
from django_components.util.django_monkeypatch import is_template_cls_patched
|
from django_components.util.django_monkeypatch import is_template_cls_patched
|
||||||
from django_components.util.exception import component_error_message
|
from django_components.util.exception import component_error_message
|
||||||
from django_components.util.logger import trace_component_msg
|
from django_components.util.logger import trace_component_msg
|
||||||
|
@ -114,6 +114,7 @@ class MetadataItem(Generic[ArgsType, KwargsType, SlotsType]):
|
||||||
render_id: str
|
render_id: str
|
||||||
input: RenderInput[ArgsType, KwargsType, SlotsType]
|
input: RenderInput[ArgsType, KwargsType, SlotsType]
|
||||||
is_filled: Optional[SlotIsFilled]
|
is_filled: Optional[SlotIsFilled]
|
||||||
|
request: Optional[HttpRequest]
|
||||||
|
|
||||||
|
|
||||||
class ViewFn(Protocol):
|
class ViewFn(Protocol):
|
||||||
|
@ -233,6 +234,7 @@ class ComponentContext:
|
||||||
fills: Dict[SlotName, Slot]
|
fills: Dict[SlotName, Slot]
|
||||||
outer_context: Optional[Context]
|
outer_context: Optional[Context]
|
||||||
registry: ComponentRegistry
|
registry: ComponentRegistry
|
||||||
|
request: Optional[HttpRequest]
|
||||||
# When we render a component, the root component, together with all the nested Components,
|
# When we render a component, the root component, together with all the nested Components,
|
||||||
# shares this dictionary for storing callbacks that are called from within `component_post_render`.
|
# shares this dictionary for storing callbacks that are called from within `component_post_render`.
|
||||||
# This is so that we can pass them all in when the root component is passed to `component_post_render`.
|
# This is so that we can pass them all in when the root component is passed to `component_post_render`.
|
||||||
|
@ -663,6 +665,8 @@ class Component(
|
||||||
"""
|
"""
|
||||||
Input holds the data (like arg, kwargs, slots) that were passed to
|
Input holds the data (like arg, kwargs, slots) that were passed to
|
||||||
the current execution of the `render` method.
|
the current execution of the `render` method.
|
||||||
|
|
||||||
|
Raises `RuntimeError` if accessed outside of rendering execution.
|
||||||
"""
|
"""
|
||||||
if not len(self._metadata_stack):
|
if not len(self._metadata_stack):
|
||||||
raise RuntimeError(f"{self.name}: Tried to access Component input while outside of rendering execution")
|
raise RuntimeError(f"{self.name}: Tried to access Component input while outside of rendering execution")
|
||||||
|
@ -678,6 +682,8 @@ class Component(
|
||||||
|
|
||||||
This attribute is available for use only within the template as `{{ component_vars.is_filled.slot_name }}`,
|
This attribute is available for use only within the template as `{{ component_vars.is_filled.slot_name }}`,
|
||||||
and within `on_render_before` and `on_render_after` hooks.
|
and within `on_render_before` and `on_render_after` hooks.
|
||||||
|
|
||||||
|
Raises `RuntimeError` if accessed outside of rendering execution.
|
||||||
"""
|
"""
|
||||||
if not len(self._metadata_stack):
|
if not len(self._metadata_stack):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
@ -693,6 +699,93 @@ class Component(
|
||||||
|
|
||||||
return ctx.is_filled
|
return ctx.is_filled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request(self) -> Optional[HttpRequest]:
|
||||||
|
"""
|
||||||
|
[HTTPRequest](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest)
|
||||||
|
object passed to this component.
|
||||||
|
|
||||||
|
In regular Django templates, you have to use
|
||||||
|
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
|
||||||
|
to pass the `HttpRequest` object to the template.
|
||||||
|
|
||||||
|
But in Components, you can either use `RequestContext`, or pass the `request` object
|
||||||
|
explicitly via [`Component.render()`](../api#django_components.Component.render) and
|
||||||
|
[`Component.render_to_response()`](../api#django_components.Component.render_to_response).
|
||||||
|
|
||||||
|
When a component is nested in another, the child component uses parent's `request` object.
|
||||||
|
|
||||||
|
Raises `RuntimeError` if accessed outside of rendering execution.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_context_data(self):
|
||||||
|
user_id = self.request.GET['user_id']
|
||||||
|
return {
|
||||||
|
'user_id': user_id,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if not len(self._metadata_stack):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{self.name}: Tried to access Component's `request` attribute " "while outside of rendering execution"
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = self._metadata_stack[-1]
|
||||||
|
return ctx.request
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context_processors_data(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Retrieve data injected by
|
||||||
|
[`context_processors`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#configuring-an-engine).
|
||||||
|
|
||||||
|
This data is also available from within the component's template, without having to
|
||||||
|
return this data from
|
||||||
|
[`get_context_data()`](../api#django_components.Component.get_context_data).
|
||||||
|
|
||||||
|
Unlike regular Django templates, the context processors are applied to components either when:
|
||||||
|
|
||||||
|
- The component is rendered with `RequestContext` (Regular Django behavior)
|
||||||
|
- The component is rendered with a regular `Context` (or none), but the `request` kwarg
|
||||||
|
of [`Component.render()`](../api#django_components.Component.render) is set.
|
||||||
|
- The component is nested in another component that matches one of the two conditions above.
|
||||||
|
|
||||||
|
See
|
||||||
|
[`Component.request`](../api#django_components.Component.request)
|
||||||
|
on how the `request`
|
||||||
|
([HTTPRequest](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest))
|
||||||
|
object is passed to and within the components.
|
||||||
|
|
||||||
|
Raises `RuntimeError` if accessed outside of rendering execution.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_context_data(self):
|
||||||
|
user = self.context_processors_data['user']
|
||||||
|
return {
|
||||||
|
'is_logged_in': user.is_authenticated,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if not len(self._metadata_stack):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{self.name}: Tried to access Component's `context_processors_data` attribute "
|
||||||
|
"while outside of rendering execution"
|
||||||
|
)
|
||||||
|
|
||||||
|
context = self.input.context
|
||||||
|
request = self.request
|
||||||
|
|
||||||
|
if request is None:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
return gen_context_processors_data(context, request)
|
||||||
|
|
||||||
# NOTE: We cache the Template instance. When the template is taken from a file
|
# NOTE: We cache the Template instance. When the template is taken from a file
|
||||||
# via `get_template_name`, then we leverage Django's template caching with `get_template()`.
|
# via `get_template_name`, then we leverage Django's template caching with `get_template()`.
|
||||||
# Otherwise, we use our own `cached_template()` to cache the template.
|
# Otherwise, we use our own `cached_template()` to cache the template.
|
||||||
|
@ -859,8 +952,7 @@ class Component(
|
||||||
or to the end of the `<body>` tag. CSS dependencies are inserted into
|
or to the end of the `<body>` tag. CSS dependencies are inserted into
|
||||||
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
|
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
|
||||||
- `request` - The request object. This is only required when needing to use RequestContext,
|
- `request` - The request object. This is only required when needing to use RequestContext,
|
||||||
e.g. to enable template `context_processors`. Unused if context is already an instance
|
e.g. to enable template `context_processors`.
|
||||||
of `Context`
|
|
||||||
|
|
||||||
Any additional args and kwargs are passed to the `response_class`.
|
Any additional args and kwargs are passed to the `response_class`.
|
||||||
|
|
||||||
|
@ -929,8 +1021,7 @@ class Component(
|
||||||
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
|
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
|
||||||
- `render_dependencies` - Set this to `False` if you want to insert the resulting HTML into another component.
|
- `render_dependencies` - Set this to `False` if you want to insert the resulting HTML into another component.
|
||||||
- `request` - The request object. This is only required when needing to use RequestContext,
|
- `request` - The request object. This is only required when needing to use RequestContext,
|
||||||
e.g. to enable template `context_processors`. Unused if context is already an instance of
|
e.g. to enable template `context_processors`.
|
||||||
`Context`
|
|
||||||
Example:
|
Example:
|
||||||
```py
|
```py
|
||||||
MyComponent.render(
|
MyComponent.render(
|
||||||
|
@ -991,11 +1082,26 @@ class Component(
|
||||||
# wraps them in functions.
|
# wraps them in functions.
|
||||||
self._validate_inputs(args or (), kwargs or {}, slots or {})
|
self._validate_inputs(args or (), kwargs or {}, slots or {})
|
||||||
|
|
||||||
|
# Allow to pass down Request object via context.
|
||||||
|
# `context` may be passed explicitly via `Component.render()` and `Component.render_to_response()`,
|
||||||
|
# or implicitly via `{% component %}` tag.
|
||||||
|
if request is None and context:
|
||||||
|
# If the context is `RequestContext`, it has `request` attribute
|
||||||
|
request = getattr(context, "request", None)
|
||||||
|
# Check if this is a nested component and whether parent has request
|
||||||
|
if request is None:
|
||||||
|
parent_id = context.get(_COMPONENT_CONTEXT_KEY, None)
|
||||||
|
if parent_id:
|
||||||
|
parent_comp_ctx = component_context_cache[parent_id]
|
||||||
|
request = parent_comp_ctx.request
|
||||||
|
|
||||||
# Allow to provide no args/kwargs/slots/context
|
# Allow to provide no args/kwargs/slots/context
|
||||||
args = cast(ArgsType, args or ())
|
args = cast(ArgsType, args or ())
|
||||||
kwargs = cast(KwargsType, kwargs or {})
|
kwargs = cast(KwargsType, kwargs or {})
|
||||||
slots_untyped = self._normalize_slot_fills(slots or {}, escape_slots_content)
|
slots_untyped = self._normalize_slot_fills(slots or {}, escape_slots_content)
|
||||||
slots = cast(SlotsType, slots_untyped)
|
slots = cast(SlotsType, slots_untyped)
|
||||||
|
# Use RequestContext if request is provided, so that child non-component template tags
|
||||||
|
# can access the request object too.
|
||||||
context = context or (RequestContext(request) if request else Context())
|
context = context or (RequestContext(request) if request else Context())
|
||||||
|
|
||||||
# Allow to provide a dict instead of Context
|
# Allow to provide a dict instead of Context
|
||||||
|
@ -1027,6 +1133,7 @@ class Component(
|
||||||
render_dependencies=render_dependencies,
|
render_dependencies=render_dependencies,
|
||||||
),
|
),
|
||||||
is_filled=None,
|
is_filled=None,
|
||||||
|
request=request,
|
||||||
)
|
)
|
||||||
|
|
||||||
# We pass down the components the info about the component's parent.
|
# We pass down the components the info about the component's parent.
|
||||||
|
@ -1071,6 +1178,8 @@ class Component(
|
||||||
default_slot=None,
|
default_slot=None,
|
||||||
outer_context=snapshot_context(self.outer_context) if self.outer_context is not None else None,
|
outer_context=snapshot_context(self.outer_context) if self.outer_context is not None else None,
|
||||||
registry=self.registry,
|
registry=self.registry,
|
||||||
|
# Pass down Request object via context
|
||||||
|
request=request,
|
||||||
post_render_callbacks=post_render_callbacks,
|
post_render_callbacks=post_render_callbacks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1083,6 +1192,7 @@ class Component(
|
||||||
|
|
||||||
# Allow to access component input and metadata like component ID from within these hook
|
# Allow to access component input and metadata like component ID from within these hook
|
||||||
with self._with_metadata(metadata):
|
with self._with_metadata(metadata):
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
context_data = self.get_context_data(*args, **kwargs)
|
context_data = self.get_context_data(*args, **kwargs)
|
||||||
# TODO - enable JS and CSS vars - EXPOSE AND DOCUMENT AND MAKE NON-NULL
|
# TODO - enable JS and CSS vars - EXPOSE AND DOCUMENT AND MAKE NON-NULL
|
||||||
js_data = self.get_js_data(*args, **kwargs) if hasattr(self, "get_js_data") else {} # type: ignore
|
js_data = self.get_js_data(*args, **kwargs) if hasattr(self, "get_js_data") else {} # type: ignore
|
||||||
|
@ -1107,6 +1217,8 @@ class Component(
|
||||||
|
|
||||||
with context.update(
|
with context.update(
|
||||||
{
|
{
|
||||||
|
# Make data from context processors available inside templates
|
||||||
|
**context_processors_data,
|
||||||
# Private context fields
|
# Private context fields
|
||||||
_COMPONENT_CONTEXT_KEY: render_id,
|
_COMPONENT_CONTEXT_KEY: render_id,
|
||||||
# NOTE: Public API for variables accessible from within a component's template
|
# NOTE: Public API for variables accessible from within a component's template
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
import copy
|
import copy
|
||||||
from typing import List
|
import sys
|
||||||
|
from typing import Any, Callable, Dict, List, Tuple
|
||||||
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
from django.template import Context
|
from django.http import HttpRequest
|
||||||
|
from django.template import Engine
|
||||||
|
from django.template.context import BaseContext, Context
|
||||||
from django.template.loader_tags import BlockContext
|
from django.template.loader_tags import BlockContext
|
||||||
|
|
||||||
|
# We cache the context processors data for each request, so that we don't have to
|
||||||
|
# generate it for each component.
|
||||||
|
# NOTE: Can't be used as generic in Python 3.8
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
context_processors_data: WeakKeyDictionary[HttpRequest, Dict[str, Any]] = WeakKeyDictionary()
|
||||||
|
else:
|
||||||
|
context_processors_data = WeakKeyDictionary()
|
||||||
|
|
||||||
|
|
||||||
class CopiedDict(dict):
|
class CopiedDict(dict):
|
||||||
"""Dict subclass to identify dictionaries that have been copied with `snapshot_context`"""
|
"""Dict subclass to identify dictionaries that have been copied with `snapshot_context`"""
|
||||||
|
@ -112,3 +124,33 @@ def _copy_block_context(block_context: BlockContext) -> BlockContext:
|
||||||
# need to modify the Nodes, but we need to make a copy of the lists.
|
# need to modify the Nodes, but we need to make a copy of the lists.
|
||||||
block_context_copy.blocks[key] = val.copy()
|
block_context_copy.blocks[key] = val.copy()
|
||||||
return block_context_copy
|
return block_context_copy
|
||||||
|
|
||||||
|
|
||||||
|
# Django's logic for generating context processors data. The gist is the same as
|
||||||
|
# `RequestContext.bind_template()`, but without depending on a Template object.
|
||||||
|
# See https://github.com/django/django/blame/2d34ebe49a25d0974392583d5bbd954baf742a32/django/template/context.py#L255
|
||||||
|
def gen_context_processors_data(context: BaseContext, request: HttpRequest) -> Dict[str, Any]:
|
||||||
|
if request in context_processors_data:
|
||||||
|
return context_processors_data[request]
|
||||||
|
|
||||||
|
# TODO_REMOVE_IN_V2 - In v2, if we still support context processors,
|
||||||
|
# it should be set on our settings, so we wouldn't have to get the Engine for that.
|
||||||
|
# In v2 it should be also possible to remove RequestContext, and use only Context,
|
||||||
|
# since we're internalized the behaviour of RequestContext.
|
||||||
|
default_engine = Engine.get_default()
|
||||||
|
|
||||||
|
# NOTE: Compatibility with `RequestContext`, which accepts an optional
|
||||||
|
# `processors` argument.
|
||||||
|
request_context_processors: Tuple[Callable[..., Any], ...] = getattr(context, "_processors", ())
|
||||||
|
|
||||||
|
# This part is same as in `RequestContext.bind_template()`
|
||||||
|
processors = default_engine.template_context_processors + request_context_processors
|
||||||
|
processors_data = {}
|
||||||
|
for processor in processors:
|
||||||
|
data = processor(request)
|
||||||
|
try:
|
||||||
|
processors_data.update(data)
|
||||||
|
except TypeError as e:
|
||||||
|
raise TypeError(f"Context processor {processor.__qualname__} didn't return a " "dictionary.") from e
|
||||||
|
|
||||||
|
return processors_data
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
from django.template import Context, Template
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.template import Context, RequestContext, Template
|
||||||
|
|
||||||
from django_components import Component, register, registry, types
|
from django_components import Component, register, registry, types
|
||||||
|
|
||||||
|
@ -437,7 +440,7 @@ class IsolatedContextTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
||||||
self.assertIn("outer_value", rendered, rendered)
|
self.assertIn("outer_value", rendered)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_simple_component_cannot_use_outer_context(self):
|
def test_simple_component_cannot_use_outer_context(self):
|
||||||
|
@ -447,7 +450,7 @@ class IsolatedContextTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
||||||
self.assertNotIn("outer_value", rendered, rendered)
|
self.assertNotIn("outer_value", rendered)
|
||||||
|
|
||||||
|
|
||||||
class IsolatedContextSettingTests(BaseTestCase):
|
class IsolatedContextSettingTests(BaseTestCase):
|
||||||
|
@ -465,7 +468,7 @@ class IsolatedContextSettingTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
rendered = template.render(Context({"variable": "outer_value"}))
|
rendered = template.render(Context({"variable": "outer_value"}))
|
||||||
self.assertIn("outer_value", rendered, rendered)
|
self.assertIn("outer_value", rendered)
|
||||||
|
|
||||||
@parametrize_context_behavior(["isolated"])
|
@parametrize_context_behavior(["isolated"])
|
||||||
def test_component_tag_excludes_variable_with_isolated_context_from_settings(
|
def test_component_tag_excludes_variable_with_isolated_context_from_settings(
|
||||||
|
@ -477,7 +480,7 @@ class IsolatedContextSettingTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
rendered = template.render(Context({"variable": "outer_value"}))
|
rendered = template.render(Context({"variable": "outer_value"}))
|
||||||
self.assertNotIn("outer_value", rendered, rendered)
|
self.assertNotIn("outer_value", rendered)
|
||||||
|
|
||||||
@parametrize_context_behavior(["isolated"])
|
@parametrize_context_behavior(["isolated"])
|
||||||
def test_component_includes_variable_with_isolated_context_from_settings(
|
def test_component_includes_variable_with_isolated_context_from_settings(
|
||||||
|
@ -490,7 +493,7 @@ class IsolatedContextSettingTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
rendered = template.render(Context({"variable": "outer_value"}))
|
rendered = template.render(Context({"variable": "outer_value"}))
|
||||||
self.assertIn("outer_value", rendered, rendered)
|
self.assertIn("outer_value", rendered)
|
||||||
|
|
||||||
@parametrize_context_behavior(["isolated"])
|
@parametrize_context_behavior(["isolated"])
|
||||||
def test_component_excludes_variable_with_isolated_context_from_settings(
|
def test_component_excludes_variable_with_isolated_context_from_settings(
|
||||||
|
@ -503,7 +506,360 @@ class IsolatedContextSettingTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
rendered = template.render(Context({"variable": "outer_value"}))
|
rendered = template.render(Context({"variable": "outer_value"}))
|
||||||
self.assertNotIn("outer_value", rendered, rendered)
|
self.assertNotIn("outer_value", rendered)
|
||||||
|
|
||||||
|
|
||||||
|
class ContextProcessorsTests(BaseTestCase):
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_request_context_in_template(self):
|
||||||
|
context_processors_data: Optional[Dict] = None
|
||||||
|
inner_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal inner_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
inner_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
request = HttpRequest()
|
||||||
|
request_context = RequestContext(request)
|
||||||
|
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(request_context)
|
||||||
|
|
||||||
|
self.assertIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(inner_request, request)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_request_context_in_template_nested(self):
|
||||||
|
context_processors_data = None
|
||||||
|
context_processors_data_child = None
|
||||||
|
parent_request: Optional[HttpRequest] = None
|
||||||
|
child_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test_parent")
|
||||||
|
class TestParentComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test_child" / %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal parent_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
parent_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@register("test_child")
|
||||||
|
class TestChildComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data_child
|
||||||
|
nonlocal child_request
|
||||||
|
context_processors_data_child = self.context_processors_data
|
||||||
|
child_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test_parent" / %}
|
||||||
|
"""
|
||||||
|
request = HttpRequest()
|
||||||
|
request_context = RequestContext(request)
|
||||||
|
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(request_context)
|
||||||
|
|
||||||
|
self.assertIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(list(context_processors_data_child.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(parent_request, request)
|
||||||
|
self.assertEqual(child_request, request)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_request_context_in_template_slot(self):
|
||||||
|
context_processors_data = None
|
||||||
|
context_processors_data_child = None
|
||||||
|
parent_request: Optional[HttpRequest] = None
|
||||||
|
child_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test_parent")
|
||||||
|
class TestParentComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test_child" %}
|
||||||
|
{% slot "content" default / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal parent_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
parent_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@register("test_child")
|
||||||
|
class TestChildComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data_child
|
||||||
|
nonlocal child_request
|
||||||
|
context_processors_data_child = self.context_processors_data
|
||||||
|
child_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test_parent" %}
|
||||||
|
{% component "test_child" / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
request = HttpRequest()
|
||||||
|
request_context = RequestContext(request)
|
||||||
|
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(request_context)
|
||||||
|
|
||||||
|
self.assertIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(list(context_processors_data_child.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(parent_request, request)
|
||||||
|
self.assertEqual(child_request, request)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_request_context_in_python(self):
|
||||||
|
context_processors_data = None
|
||||||
|
inner_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal inner_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
inner_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
request = HttpRequest()
|
||||||
|
request_context = RequestContext(request)
|
||||||
|
rendered = TestComponent.render(request_context)
|
||||||
|
|
||||||
|
self.assertIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(inner_request, request)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_request_context_in_python_nested(self):
|
||||||
|
context_processors_data: Optional[Dict] = None
|
||||||
|
context_processors_data_child: Optional[Dict] = None
|
||||||
|
parent_request: Optional[HttpRequest] = None
|
||||||
|
child_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test_parent")
|
||||||
|
class TestParentComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test_child" / %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal parent_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
parent_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@register("test_child")
|
||||||
|
class TestChildComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data_child
|
||||||
|
nonlocal child_request
|
||||||
|
context_processors_data_child = self.context_processors_data
|
||||||
|
child_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
request = HttpRequest()
|
||||||
|
request_context = RequestContext(request)
|
||||||
|
|
||||||
|
rendered = TestParentComponent.render(request_context)
|
||||||
|
self.assertIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(list(context_processors_data_child.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(parent_request, request)
|
||||||
|
self.assertEqual(child_request, request)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_request_in_python(self):
|
||||||
|
context_processors_data: Optional[Dict] = None
|
||||||
|
inner_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal inner_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
inner_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
request = HttpRequest()
|
||||||
|
rendered = TestComponent.render(request=request)
|
||||||
|
|
||||||
|
self.assertIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(inner_request, request)
|
||||||
|
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_request_in_python_nested(self):
|
||||||
|
context_processors_data: Optional[Dict] = None
|
||||||
|
context_processors_data_child: Optional[Dict] = None
|
||||||
|
parent_request: Optional[HttpRequest] = None
|
||||||
|
child_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test_parent")
|
||||||
|
class TestParentComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test_child" / %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal parent_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
parent_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@register("test_child")
|
||||||
|
class TestChildComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data_child
|
||||||
|
nonlocal child_request
|
||||||
|
context_processors_data_child = self.context_processors_data
|
||||||
|
child_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
request = HttpRequest()
|
||||||
|
rendered = TestParentComponent.render(request=request)
|
||||||
|
|
||||||
|
self.assertIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(list(context_processors_data_child.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(parent_request, request)
|
||||||
|
self.assertEqual(child_request, request)
|
||||||
|
|
||||||
|
# No request, regular Context
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_no_context_processor_when_non_request_context_in_python(self):
|
||||||
|
context_processors_data: Optional[Dict] = None
|
||||||
|
inner_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal inner_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
inner_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
rendered = TestComponent.render(context=Context())
|
||||||
|
|
||||||
|
self.assertNotIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), []) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(inner_request, None)
|
||||||
|
|
||||||
|
# No request, no Context
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_no_context_processor_when_non_request_context_in_python_2(self):
|
||||||
|
context_processors_data: Optional[Dict] = None
|
||||||
|
inner_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal inner_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
inner_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
rendered = TestComponent.render()
|
||||||
|
|
||||||
|
self.assertNotIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), []) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(inner_request, None)
|
||||||
|
|
||||||
|
# Yes request, regular Context
|
||||||
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
def test_context_processor_when_regular_context_and_request_in_python(self):
|
||||||
|
context_processors_data: Optional[Dict] = None
|
||||||
|
inner_request: Optional[HttpRequest] = None
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
nonlocal context_processors_data
|
||||||
|
nonlocal inner_request
|
||||||
|
context_processors_data = self.context_processors_data
|
||||||
|
inner_request = self.request
|
||||||
|
return {}
|
||||||
|
|
||||||
|
request = HttpRequest()
|
||||||
|
rendered = TestComponent.render(Context(), request=request)
|
||||||
|
|
||||||
|
self.assertIn("csrfmiddlewaretoken", rendered)
|
||||||
|
self.assertEqual(list(context_processors_data.keys()), ["csrf_token"]) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(inner_request, request)
|
||||||
|
|
||||||
|
def test_raises_on_accessing_context_processors_data_outside_of_rendering(self):
|
||||||
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
RuntimeError,
|
||||||
|
"Tried to access Component's `context_processors_data` attribute while outside of rendering execution",
|
||||||
|
):
|
||||||
|
TestComponent().context_processors_data
|
||||||
|
|
||||||
|
def test_raises_on_accessing_request_outside_of_rendering(self):
|
||||||
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """{% csrf_token %}"""
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
RuntimeError,
|
||||||
|
"Tried to access Component's `request` attribute while outside of rendering execution",
|
||||||
|
):
|
||||||
|
TestComponent().request
|
||||||
|
|
||||||
|
|
||||||
class OuterContextPropertyTests(BaseTestCase):
|
class OuterContextPropertyTests(BaseTestCase):
|
||||||
|
@ -527,7 +883,7 @@ class OuterContextPropertyTests(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
rendered = template.render(Context({"variable": "outer_value"})).strip()
|
||||||
self.assertIn("outer_value", rendered, rendered)
|
self.assertIn("outer_value", rendered)
|
||||||
|
|
||||||
|
|
||||||
class ContextVarsIsFilledTests(BaseTestCase):
|
class ContextVarsIsFilledTests(BaseTestCase):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue