fix: populate RequestContext with context processors (#643)

This commit is contained in:
Juro Oravec 2024-09-04 21:31:44 +02:00 committed by GitHub
parent 96a4717631
commit 2d0f270df4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 113 additions and 17 deletions

View file

@ -1,6 +1,7 @@
import inspect
import types
from collections import deque
from contextlib import contextmanager
from dataclasses import dataclass
from typing import (
Any,
@ -8,6 +9,7 @@ from typing import (
ClassVar,
Deque,
Dict,
Generator,
Generic,
List,
Literal,
@ -520,22 +522,7 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
context_data = self.get_context_data(*args, **kwargs)
self._validate_outputs(context_data)
with context.update(context_data):
template = self.get_template(context)
_monkeypatch_template(template)
if context.template is None:
# Associate the newly-created Context with a Template, otherwise we get
# an error when we try to use `{% include %}` tag inside the template?
# See https://github.com/EmilStenstrom/django-components/issues/580
context.template = template
context.template_name = template.name
# Set `Template._dc_is_component_nested` based on whether we're currently INSIDE
# the `{% extends %}` tag.
# Part of fix for https://github.com/EmilStenstrom/django-components/issues/508
template._dc_is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
with _prepare_template(self, context, context_data) as template:
# Support passing slots explicitly to `render` method
if has_slots:
fill_content = self._fills_from_slots_data(
@ -843,3 +830,35 @@ def _monkeypatch_template(template: Template) -> None:
# See https://stackoverflow.com/a/42154067/9788634
template.render = types.MethodType(_template_render, template)
@contextmanager
def _maybe_bind_template(context: Context, template: Template) -> Generator[None, Any, None]:
if context.template is None:
with context.bind_template(template):
yield
else:
yield
@contextmanager
def _prepare_template(
component: Component,
context: Context,
context_data: Any,
) -> Generator[Template, Any, None]:
with context.update(context_data):
# Associate the newly-created Context with a Template, otherwise we get
# an error when we try to use `{% include %}` tag inside the template?
# See https://github.com/EmilStenstrom/django-components/issues/580
# And https://github.com/EmilStenstrom/django-components/issues/634
template = component.get_template(context)
_monkeypatch_template(template)
# Set `Template._dc_is_component_nested` based on whether we're currently INSIDE
# the `{% extends %}` tag.
# Part of fix for https://github.com/EmilStenstrom/django-components/issues/508
template._dc_is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
with _maybe_bind_template(context, template):
yield template

View file

@ -3,6 +3,7 @@ Tests focusing on the Component class.
For tests focusing on the `component` tag, see `test_templatetags_component.py`
"""
import re
import sys
from typing import Any, Dict, Tuple, Union, no_type_check
@ -14,13 +15,16 @@ else:
from unittest import skipIf
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
from django.template import Context, RequestContext, Template, TemplateSyntaxError
from django.template.base import TextNode
from django.test import Client
from django.urls import path
from django.utils.safestring import SafeString
from django_components import Component, SlotFunc, registry, types
from django_components import Component, ComponentView, SlotFunc, register, registry, types
from django_components.slots import SlotRef
from .django_test_setup import setup_test_config
@ -29,6 +33,21 @@ from .testutils import BaseTestCase, parametrize_context_behavior
setup_test_config({"autodiscover": False})
# Client for testing endpoints via requests
class CustomClient(Client):
def __init__(self, urlpatterns=None, *args, **kwargs):
import types
if urlpatterns:
urls_module = types.ModuleType("urls")
urls_module.urlpatterns = urlpatterns # type: ignore
settings.ROOT_URLCONF = urls_module
else:
settings.ROOT_URLCONF = __name__
settings.SECRET_KEY = "secret" # noqa
super().__init__(*args, **kwargs)
# Component typings
CompArgs = Tuple[int, str]
@ -772,6 +791,7 @@ class ComponentRenderTest(BaseTestCase):
)
# See https://github.com/EmilStenstrom/django-components/issues/580
# And https://github.com/EmilStenstrom/django-components/issues/634
# And https://github.com/EmilStenstrom/django-components/commit/fee26ec1d8b46b5ee065ca1ce6143889b0f96764
@parametrize_context_behavior(["django", "isolated"])
def test_render_with_include_and_request_context(self):
@ -793,6 +813,63 @@ class ComponentRenderTest(BaseTestCase):
""",
)
# See https://github.com/EmilStenstrom/django-components/issues/580
# And https://github.com/EmilStenstrom/django-components/issues/634
@parametrize_context_behavior(["django", "isolated"])
def test_request_context_is_populated_from_context_processors(self):
@register("thing")
class Thing(Component):
template: types.django_html = """
<kbd>Rendered {{ how }}</kbd>
<div>
CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}
</div>
"""
def get_context_data(self, *args, how: str, **kwargs):
return {"how": how}
class View(ComponentView):
def get(self, request):
how = "via GET request"
return self.component.render_to_response(
context=RequestContext(self.request),
kwargs=self.component.get_context_data(how=how),
)
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
response = client.get("/test_thing/")
self.assertEqual(response.status_code, 200)
# Full response:
# """
# <kbd>
# Rendered via GET request
# </kbd>
# <div>
# CSRF token:
# <div>
# test_csrf_token
# </div>
# </div>
# """
self.assertInHTML(
"""
<kbd>
Rendered via GET request
</kbd>
""",
response.content.decode(),
)
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)
@parametrize_context_behavior(["django", "isolated"])
def test_render_with_extends(self):
class SimpleComponent(Component):