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:
Laurence Hole 2024-12-10 12:28:34 +00:00 committed by GitHub
parent 3f2d92f252
commit dfd4187192
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 90 additions and 6 deletions

View file

@ -74,6 +74,12 @@ Component.render(
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
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`
When rendering components with slots in `render` or `render_to_response`, you can pass either a string or a function.

View file

@ -28,7 +28,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media
from django.http import HttpRequest, HttpResponse
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_tags import BLOCK_CONTEXT_KEY
from django.utils.html import conditional_escape
@ -495,6 +495,7 @@ class Component(
args: Optional[ArgsType] = None,
kwargs: Optional[KwargsType] = None,
type: RenderType = "document",
request: Optional[HttpRequest] = None,
*response_args: Any,
**response_kwargs: Any,
) -> HttpResponse:
@ -523,6 +524,9 @@ class Component(
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
or to the end of the `<body>` tag. CSS dependencies are inserted into
`{% 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`.
@ -553,6 +557,7 @@ class Component(
escape_slots_content=escape_slots_content,
type=type,
render_dependencies=True,
request=request,
)
return cls.response_class(content, *response_args, **response_kwargs)
@ -566,6 +571,7 @@ class Component(
escape_slots_content: bool = True,
type: RenderType = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
"""
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
`{% 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.
- `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:
```py
MyComponent.render(
@ -611,7 +619,7 @@ class Component(
else:
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
def _render(
@ -623,9 +631,12 @@ class Component(
escape_slots_content: bool = True,
type: RenderType = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
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:
# Nicely format the error message to include the component path.
# E.g.
@ -662,6 +673,7 @@ class Component(
escape_slots_content: bool = True,
type: RenderType = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
# NOTE: We must run validation before we normalize the slots, because the normalization
# wraps them in functions.
@ -672,12 +684,13 @@ class Component(
kwargs = cast(KwargsType, kwargs or {})
slots_untyped = self._normalize_slot_fills(slots or {}, escape_slots_content)
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
# NOTE: This if/else is important to avoid nested Contexts,
# 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
# to access the provided context, slots, etc. Also required so users can

View file

@ -1070,6 +1070,71 @@ class ComponentRenderTest(BaseTestCase):
self.assertTrue(token)
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"])
def test_render_with_extends(self):
class SimpleComponent(Component):