mirror of
https://github.com/django-components/django-components.git
synced 2025-08-18 13:10:13 +00:00
Adding request
arg to render (#817)
Co-authored-by: Laurence Hole <laurence@safi.co> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
3f2d92f252
commit
dfd4187192
3 changed files with 90 additions and 6 deletions
|
@ -74,6 +74,12 @@ Component.render(
|
||||||
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
|
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
|
||||||
component's args and kwargs.
|
component's args and kwargs.
|
||||||
|
|
||||||
|
- _`request`_ - A Django request object. This is used to enable Django template `context_processors` to run,
|
||||||
|
allowing for template tags like `{% csrf_token %}` and variables like `{{ debug }}`.
|
||||||
|
- Similar behavior can be achieved with [provide / inject](#how-to-use-provide--inject).
|
||||||
|
- This is used internally to convert `context` to a RequestContext. It does nothing if `context` is already
|
||||||
|
a `Context` instance.
|
||||||
|
|
||||||
### `SlotFunc`
|
### `SlotFunc`
|
||||||
|
|
||||||
When rendering components with slots in `render` or `render_to_response`, you can pass either a string or a function.
|
When rendering components with slots in `render` or `render_to_response`, you can pass either a string or a function.
|
||||||
|
|
|
@ -28,7 +28,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.forms.widgets import Media
|
from django.forms.widgets import Media
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.template.base import NodeList, Template, TextNode
|
from django.template.base import NodeList, Template, TextNode
|
||||||
from django.template.context import Context
|
from django.template.context import Context, RequestContext
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.template.loader_tags import BLOCK_CONTEXT_KEY
|
from django.template.loader_tags import BLOCK_CONTEXT_KEY
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
|
@ -495,6 +495,7 @@ class Component(
|
||||||
args: Optional[ArgsType] = None,
|
args: Optional[ArgsType] = None,
|
||||||
kwargs: Optional[KwargsType] = None,
|
kwargs: Optional[KwargsType] = None,
|
||||||
type: RenderType = "document",
|
type: RenderType = "document",
|
||||||
|
request: Optional[HttpRequest] = None,
|
||||||
*response_args: Any,
|
*response_args: Any,
|
||||||
**response_kwargs: Any,
|
**response_kwargs: Any,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
|
@ -523,6 +524,9 @@ class Component(
|
||||||
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
|
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
|
||||||
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,
|
||||||
|
e.g. to enable template `context_processors`. Unused if context is already an instance
|
||||||
|
of `Context`
|
||||||
|
|
||||||
Any additional args and kwargs are passed to the `response_class`.
|
Any additional args and kwargs are passed to the `response_class`.
|
||||||
|
|
||||||
|
@ -553,6 +557,7 @@ class Component(
|
||||||
escape_slots_content=escape_slots_content,
|
escape_slots_content=escape_slots_content,
|
||||||
type=type,
|
type=type,
|
||||||
render_dependencies=True,
|
render_dependencies=True,
|
||||||
|
request=request,
|
||||||
)
|
)
|
||||||
return cls.response_class(content, *response_args, **response_kwargs)
|
return cls.response_class(content, *response_args, **response_kwargs)
|
||||||
|
|
||||||
|
@ -566,6 +571,7 @@ class Component(
|
||||||
escape_slots_content: bool = True,
|
escape_slots_content: bool = True,
|
||||||
type: RenderType = "document",
|
type: RenderType = "document",
|
||||||
render_dependencies: bool = True,
|
render_dependencies: bool = True,
|
||||||
|
request: Optional[HttpRequest] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Render the component into a string.
|
Render the component into a string.
|
||||||
|
@ -588,7 +594,9 @@ 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.
|
||||||
- `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,
|
||||||
|
e.g. to enable template `context_processors`. Unused if context is already an instance of
|
||||||
|
`Context`
|
||||||
Example:
|
Example:
|
||||||
```py
|
```py
|
||||||
MyComponent.render(
|
MyComponent.render(
|
||||||
|
@ -611,7 +619,7 @@ class Component(
|
||||||
else:
|
else:
|
||||||
comp = cls()
|
comp = cls()
|
||||||
|
|
||||||
return comp._render(context, args, kwargs, slots, escape_slots_content, type, render_dependencies)
|
return comp._render(context, args, kwargs, slots, escape_slots_content, type, render_dependencies, request)
|
||||||
|
|
||||||
# This is the internal entrypoint for the render function
|
# This is the internal entrypoint for the render function
|
||||||
def _render(
|
def _render(
|
||||||
|
@ -623,9 +631,12 @@ class Component(
|
||||||
escape_slots_content: bool = True,
|
escape_slots_content: bool = True,
|
||||||
type: RenderType = "document",
|
type: RenderType = "document",
|
||||||
render_dependencies: bool = True,
|
render_dependencies: bool = True,
|
||||||
|
request: Optional[HttpRequest] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
try:
|
try:
|
||||||
return self._render_impl(context, args, kwargs, slots, escape_slots_content, type, render_dependencies)
|
return self._render_impl(
|
||||||
|
context, args, kwargs, slots, escape_slots_content, type, render_dependencies, request
|
||||||
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
# Nicely format the error message to include the component path.
|
# Nicely format the error message to include the component path.
|
||||||
# E.g.
|
# E.g.
|
||||||
|
@ -662,6 +673,7 @@ class Component(
|
||||||
escape_slots_content: bool = True,
|
escape_slots_content: bool = True,
|
||||||
type: RenderType = "document",
|
type: RenderType = "document",
|
||||||
render_dependencies: bool = True,
|
render_dependencies: bool = True,
|
||||||
|
request: Optional[HttpRequest] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
# NOTE: We must run validation before we normalize the slots, because the normalization
|
# NOTE: We must run validation before we normalize the slots, because the normalization
|
||||||
# wraps them in functions.
|
# wraps them in functions.
|
||||||
|
@ -672,12 +684,13 @@ class Component(
|
||||||
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)
|
||||||
context = context or 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
|
||||||
# NOTE: This if/else is important to avoid nested Contexts,
|
# NOTE: This if/else is important to avoid nested Contexts,
|
||||||
# See https://github.com/EmilStenstrom/django-components/issues/414
|
# See https://github.com/EmilStenstrom/django-components/issues/414
|
||||||
context = context if isinstance(context, Context) else Context(context)
|
if not isinstance(context, Context):
|
||||||
|
context = RequestContext(request, context) if request else Context(context)
|
||||||
|
|
||||||
# By adding the current input to the stack, we temporarily allow users
|
# By adding the current input to the stack, we temporarily allow users
|
||||||
# to access the provided context, slots, etc. Also required so users can
|
# to access the provided context, slots, etc. Also required so users can
|
||||||
|
|
|
@ -1070,6 +1070,71 @@ class ComponentRenderTest(BaseTestCase):
|
||||||
self.assertTrue(token)
|
self.assertTrue(token)
|
||||||
self.assertEqual(len(token), 64)
|
self.assertEqual(len(token), 64)
|
||||||
|
|
||||||
|
def test_request_context_created_when_no_context(self):
|
||||||
|
@register("thing")
|
||||||
|
class Thing(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return self.render_to_response(request=request)
|
||||||
|
|
||||||
|
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
||||||
|
response = client.get("/test_thing/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
token_re = re.compile(rb"CSRF token:\s+(?P<token>[0-9a-zA-Z]{64})")
|
||||||
|
token = token_re.findall(response.content)[0]
|
||||||
|
|
||||||
|
self.assertTrue(token)
|
||||||
|
self.assertEqual(len(token), 64)
|
||||||
|
|
||||||
|
def test_request_context_created_when_already_a_context_dict(self):
|
||||||
|
@register("thing")
|
||||||
|
class Thing(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<p>CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}</p>
|
||||||
|
<p>Existing context: {{ existing_context|default:"<em>No existing context</em>" }}</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return self.render_to_response(request=request, context={"existing_context": "foo"})
|
||||||
|
|
||||||
|
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
||||||
|
response = client.get("/test_thing/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
token_re = re.compile(rb"CSRF token:\s+(?P<token>[0-9a-zA-Z]{64})")
|
||||||
|
token = token_re.findall(response.content)[0]
|
||||||
|
|
||||||
|
self.assertTrue(token)
|
||||||
|
self.assertEqual(len(token), 64)
|
||||||
|
self.assertInHTML("Existing context: foo", response.content.decode())
|
||||||
|
|
||||||
|
def request_context_ignores_context_when_already_a_context(self):
|
||||||
|
@register("thing")
|
||||||
|
class Thing(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<p>CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}</p>
|
||||||
|
<p>Existing context: {{ existing_context|default:"<em>No existing context</em>" }}</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return self.render_to_response(request=request, context=Context({"existing_context": "foo"}))
|
||||||
|
|
||||||
|
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
||||||
|
response = client.get("/test_thing/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
token_re = re.compile(rb"CSRF token:\s+(?P<token>[0-9a-zA-Z]{64})")
|
||||||
|
|
||||||
|
self.assertFalse(token_re.findall(response.content))
|
||||||
|
self.assertInHTML("Existing context: foo", response.content.decode())
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_render_with_extends(self):
|
def test_render_with_extends(self):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue