Skip to content

Commit

Permalink
Adding request arg to render (#817)
Browse files Browse the repository at this point in the history
Co-authored-by: Laurence Hole <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 10, 2024
1 parent 3f2d92f commit dfd4187
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 6 deletions.
6 changes: 6 additions & 0 deletions docs/concepts/fundamentals/components_in_python.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 19 additions & 6 deletions src/django_components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
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
Expand Down Expand Up @@ -495,6 +495,7 @@ def render_to_response(
args: Optional[ArgsType] = None,
kwargs: Optional[KwargsType] = None,
type: RenderType = "document",
request: Optional[HttpRequest] = None,
*response_args: Any,
**response_kwargs: Any,
) -> HttpResponse:
Expand Down Expand Up @@ -523,6 +524,9 @@ def render_to_response(
- `"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`.
Expand Down Expand Up @@ -553,6 +557,7 @@ def render_to_response(
escape_slots_content=escape_slots_content,
type=type,
render_dependencies=True,
request=request,
)
return cls.response_class(content, *response_args, **response_kwargs)

Expand All @@ -566,6 +571,7 @@ def render(
escape_slots_content: bool = True,
type: RenderType = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
"""
Render the component into a string.
Expand All @@ -588,7 +594,9 @@ def render(
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(
Expand All @@ -611,7 +619,7 @@ def render(
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(
Expand All @@ -623,9 +631,12 @@ def _render(
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.
Expand Down Expand Up @@ -662,6 +673,7 @@ def _render_impl(
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.
Expand All @@ -672,12 +684,13 @@ def _render_impl(
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
Expand Down
65 changes: 65 additions & 0 deletions tests/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,71 @@ def get(self, request):
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):
Expand Down

0 comments on commit dfd4187

Please sign in to comment.