feat: Typing for component inputs and access inputs during render (#585)

This commit is contained in:
Juro Oravec 2024-08-22 23:42:34 +02:00 committed by GitHub
parent 4dd3e3d5b3
commit efd05d6150
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 470 additions and 59 deletions

177
README.md
View file

@ -49,6 +49,13 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
## Release notes
🚨📢 **Version 0.92**
- BREAKING CHANGE: `Component` class is no longer a subclass of `View`. To configure the `View` class, set the `Component.View` nested class. HTTP methods like `get` or `post` can still be defined directly on `Component` class, and `Component.as_view()` internally calls `Component.View.as_view()`. (See [Modifying the View class](#modifying-the-view-class))
- The inputs (args, kwargs, slots, context, ...) that you pass to `Component.render()` can be accessed from within `get_context_data`, `get_template_string` and `get_template_name` via `self.input`. (See [Accessing data passed to the component](#accessing-data-passed-to-the-component))
- Typing: `Component` class supports generics that specify types for `Component.render` (See [Adding type hints with Generics](#adding-type-hints-with-generics))
**Version 0.90**
- All tags (`component`, `slot`, `fill`, ...) now support "self-closing" or "inline" form, where you can omit the closing tag:
```django
@ -588,6 +595,52 @@ MyComponent.render_to_response(
)
```
### Adding type hints with Generics
The `Component` class optionally accepts type parameters
that allow you to specify the types of args, kwargs, slots, and
data.
```py
from typing import NotRequired, Tuple, TypedDict
# Tuple
Args = Tuple[int, str]
# Mapping
class Kwargs(TypedDict):
variable: str
another: int
maybe_var: NotRequired[int]
# Mapping
class Data(TypedDict):
variable: str
# Mapping
class Slots(TypedDict):
my_slot: NotRequired[str]
class Button(Component[Args, Kwargs, Data, Slots]):
def get_context_data(self, variable, another):
return {
"variable": variable,
}
```
When you then call `Component.render` or `Component.render_to_response`, you will get type hints:
```py
Button.render(
# Error: First arg must be `int`, got `float`
args=(1.25, "abc"),
# Error: Key "another" is missing
kwargs={
"variable": "text",
},
)
```
### Response class of `render_to_response`
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is `django.http.HttpResponse`.
@ -614,15 +667,22 @@ assert isinstance(response, MyResponse)
_New in version 0.34_
Components can now be used as views. To do this, `Component` subclasses Django's `View` class. This means that you can use all of the [methods](https://docs.djangoproject.com/en/5.0/ref/class-based-views/base/#view) of `View` in your component. For example, you can override `get` and `post` to handle GET and POST requests, respectively.
_Note: Since 0.92, Component no longer subclasses View. To configure the View class, set the nested `Component.View` class_
In addition, `Component` now has a [`render_to_response`](#inputs-of-render-and-render_to_response) method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object.
Components can now be used as views:
- Components define the `Component.as_view()` class method that can be used the same as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view).
- By default, you can define GET, POST or other HTTP handlers directly on the Component, same as you do with [View](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#view). For example, you can override `get` and `post` to handle GET and POST requests, respectively.
- In addition, `Component` now has a [`render_to_response`](#inputs-of-render-and-render_to_response) method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object.
### Component as view example
Here's an example of a calendar component defined as a view:
```python
# In a file called [project root]/components/calendar.py
from django_components import Component, register
from django_components import Component, ComponentView, register
@register("calendar")
class Calendar(Component):
@ -638,6 +698,7 @@ class Calendar(Component):
</div>
"""
# Handle GET requests
def get(self, request, *args, **kwargs):
context = {
"date": request.GET.get("date", "2020-06-06"),
@ -645,7 +706,11 @@ class Calendar(Component):
slots = {
"header": "Calendar header",
}
return self.render_to_response(context=context, slots=slots)
# Return HttpResponse with the rendered content
return self.render_to_response(
context=context,
slots=slots,
)
```
Then, to use this component as a view, you should create a `urls.py` file in your components directory, and add a path to the component's view:
@ -660,6 +725,9 @@ urlpatterns = [
]
```
`Component.as_view()` is a shorthand for calling [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view) and passing the component
instance as one of the arguments.
Remember to add `__init__.py` to your components directory, so that Django can find the `urls.py` file.
Finally, include the component's urls in your project's `urls.py` file:
@ -673,10 +741,77 @@ urlpatterns = [
]
```
Note: slots content are automatically escaped by default to prevent XSS attacks. To disable escaping, set `escape_slots_content=False` in the `render_to_response` method. If you do so, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
Note: Slots content are automatically escaped by default to prevent XSS attacks. To disable escaping, set `escape_slots_content=False` in the `render_to_response` method. If you do so, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
### Modifying the View class
The View class that handles the requests is defined on `Component.View`.
When you define a GET or POST handlers on the `Component` class, like so:
```py
class MyComponent(Component):
def get(self, request, *args, **kwargs):
return self.render_to_response(
context={
"date": request.GET.get("date", "2020-06-06"),
},
)
def post(self, request, *args, **kwargs) -> HttpResponse:
variable = request.POST.get("variable")
return self.render_to_response(
kwargs={"variable": variable}
)
```
Then the request is still handled by `Component.View.get()` or `Component.View.post()`
methods. However, by default, `Component.View.get()` points to `Component.get()`, and so on.
```py
class ComponentView(View):
component: Component = None
...
def get(self, request, *args, **kwargs):
return self.component.get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.component.post(request, *args, **kwargs)
...
```
If you want to define your own `View` class, you need to:
1. Set the class as `Component.View`
2. Subclass from `ComponentView`, so the View instance has access to the component class.
In the example below, we added extra logic into `View.setup()`.
Note that the POST handler is still defined at the top. This is because `View` subclasses `ComponentView`, which defines the `post()` method that calls `Component.post()`.
If you were to overwrite the `View.post()` method, then `Component.post()` would be ignored.
```py
from django_components import Component, ComponentView
class MyComponent(Component):
def post(self, request, *args, **kwargs) -> HttpResponse:
variable = request.POST.get("variable")
return self.component.render_to_response(
kwargs={"variable": variable}
)
class View(ComponentView):
def setup(self, request, *args, **kwargs):
super(request, *args, **kwargs)
do_something_extra(request, *args, **kwargs)
```
## Registering components
In previous examples you could repeatedly see us using `@register()` to "register"
@ -1389,6 +1524,38 @@ Sweet! Now all the relevant HTML is inside the template, and we can move it to a
> {"attrs": {"my_key:two": 2}}
> ```
### Accessing data passed to the component
When you call `Component.render` or `Component.render_to_response`, the inputs to these methods can be accessed from within the instance under `self.input`.
This means that you can use `self.input` inside:
- `get_context_data`
- `get_template_name`
- `get_template_string`
`self.input` is defined only for the duration of `Component.render`, and returns `None` when called outside of this.
`self.input` has the same fields as the input to `Component.render`:
```py
class TestComponent(Component):
def get_context_data(self, var1, var2, variable, another, **attrs):
assert self.input.args == (123, "str")
assert self.input.kwargs == {"variable": "test", "another": 1}
assert self.input.slots == {"my_slot": "MY_SLOT"}
assert isinstance(self.input.context, Context)
return {
"variable": variable,
}
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
```
## Rendering HTML attributes
_New in version 0.74_:

View file

@ -7,7 +7,10 @@ from django_components.autodiscover import (
autodiscover as autodiscover,
import_libraries as import_libraries,
)
from django_components.component import Component as Component
from django_components.component import (
Component as Component,
ComponentView as ComponentView,
)
from django_components.component_registry import (
AlreadyRegistered as AlreadyRegistered,
ComponentRegistry as ComponentRegistry,

View file

@ -1,10 +1,28 @@
import inspect
import types
from typing import Any, ClassVar, Dict, List, Mapping, Optional, Tuple, Type, Union
from collections import deque
from dataclasses import dataclass
from typing import (
Any,
Callable,
ClassVar,
Deque,
Dict,
Generic,
List,
Mapping,
Optional,
Protocol,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media
from django.http import HttpResponse
from django.http import HttpRequest, HttpResponse
from django.template.base import FilterExpression, Node, NodeList, Template, TextNode
from django.template.context import Context
from django.template.exceptions import TemplateSyntaxError
@ -53,6 +71,25 @@ from django_components.component_registry import registry as registry # NOQA
RENDERED_COMMENT_TEMPLATE = "<!-- _RENDERED {name} -->"
# Define TypeVars for args and kwargs
ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True)
KwargsType = TypeVar("KwargsType", bound=Mapping[str, Any], contravariant=True)
DataType = TypeVar("DataType", bound=Mapping[str, Any], covariant=True)
SlotsType = TypeVar("SlotsType", bound=Mapping[SlotName, SlotContent])
@dataclass(frozen=True)
class RenderInput(Generic[ArgsType, KwargsType, SlotsType]):
context: Context
args: ArgsType
kwargs: KwargsType
slots: SlotsType
escape_slots_content: bool
class ViewFn(Protocol):
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
class ComponentMeta(MediaMeta):
def __new__(mcs, name: str, bases: Tuple[Type, ...], attrs: Dict[str, Any]) -> Type:
@ -64,7 +101,42 @@ class ComponentMeta(MediaMeta):
return super().__new__(mcs, name, bases, attrs)
class Component(View, metaclass=ComponentMeta):
# NOTE: We use metaclass to automatically define the HTTP methods as defined
# in `View.http_method_names`.
class ComponentViewMeta(type):
def __new__(cls, name: str, bases: Any, dct: Dict) -> Any:
# Default implementation shared by all HTTP methods
def create_handler(method: str) -> Callable:
def handler(self, request: HttpRequest, *args: Any, **kwargs: Any): # type: ignore[no-untyped-def]
component: "Component" = self.component
return getattr(component, method)(request, *args, **kwargs)
return handler
# Add methods to the class
for method_name in View.http_method_names:
if method_name not in dct:
dct[method_name] = create_handler(method_name)
return super().__new__(cls, name, bases, dct)
class ComponentView(View, metaclass=ComponentViewMeta):
"""
Subclass of `django.views.View` where the `Component` instance is available
via `self.component`.
"""
# NOTE: This attribute must be declared on the class for `View.as_view` to allow
# us to pass `component` kwarg.
component = cast("Component", None)
def __init__(self, component: "Component", **kwargs: Any) -> None:
super().__init__(**kwargs)
self.component = component
class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=ComponentMeta):
# Either template_name or template must be set on subclass OR subclass must implement get_template() with
# non-null return.
_class_hash: ClassVar[int]
@ -90,6 +162,8 @@ class Component(View, metaclass=ComponentMeta):
Media = ComponentMediaInput
"""Defines JS and CSS media files associated with this component."""
View = ComponentView
def __init__(
self,
registered_name: Optional[str] = None,
@ -97,12 +171,6 @@ class Component(View, metaclass=ComponentMeta):
outer_context: Optional[Context] = None,
fill_content: Optional[Dict[str, FillContent]] = None,
):
self.registered_name: Optional[str] = registered_name
self.outer_context: Context = outer_context or Context()
self.fill_content = fill_content or {}
self.component_id = component_id or gen_id()
self._context: Optional[Context] = None
# When user first instantiates the component class before calling
# `render` or `render_to_response`, then we want to allow the render
# function to make use of the instantiated object.
@ -117,6 +185,12 @@ class Component(View, metaclass=ComponentMeta):
self.render_to_response = types.MethodType(self.__class__.render_to_response.__func__, self) # type: ignore
self.render = types.MethodType(self.__class__.render.__func__, self) # type: ignore
self.registered_name: Optional[str] = registered_name
self.outer_context: Context = outer_context or Context()
self.fill_content = fill_content or {}
self.component_id = component_id or gen_id()
self._render_stack: Deque[RenderInput[ArgsType, KwargsType, SlotsType]] = deque()
def __init_subclass__(cls, **kwargs: Any) -> None:
cls._class_hash = hash(inspect.getfile(cls) + cls.__name__)
@ -124,8 +198,18 @@ class Component(View, metaclass=ComponentMeta):
def name(self) -> str:
return self.registered_name or self.__class__.__name__
def get_context_data(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
return {}
@property
def input(self) -> Optional[RenderInput[ArgsType, KwargsType, SlotsType]]:
"""
Input holds the data (like arg, kwargs, slots) that were passsed to
the current execution of the `render` method.
"""
# NOTE: Input is managed as a stack, so if `render` is called within another `render`,
# the propertes below will return only the inner-most state.
return self._render_stack[-1] if len(self._render_stack) else None
def get_context_data(self, *args: Any, **kwargs: Any) -> DataType:
return cast(DataType, {})
def get_template_name(self, context: Context) -> Optional[str]:
return self.template_name
@ -220,21 +304,30 @@ class Component(View, metaclass=ComponentMeta):
As the `{{ data.hello }}` is taken from the "provider".
"""
if self._context is None:
if self.input is None:
raise RuntimeError(
f"Method 'inject()' of component '{self.name}' was called outside of 'get_context_data()'"
)
return get_injected_context_var(self.name, self._context, key, default)
return get_injected_context_var(self.name, self.input.context, key, default)
@classmethod
def as_view(cls, **initkwargs: Any) -> ViewFn:
"""
Shortcut for calling `Component.View.as_view` and passing component instance to it.
"""
# Allow the View class to access this component via `self.component`
component = cls()
return component.View.as_view(**initkwargs, component=component)
@classmethod
def render_to_response(
cls,
context: Union[Dict[str, Any], Context] = None,
slots: Optional[Mapping[SlotName, SlotContent]] = None,
context: Optional[Union[Dict[str, Any], Context]] = None,
slots: Optional[SlotsType] = None,
escape_slots_content: bool = True,
args: Optional[Union[List, Tuple]] = None,
kwargs: Optional[Dict[str, Any]] = None,
args: Optional[ArgsType] = None,
kwargs: Optional[KwargsType] = None,
*response_args: Any,
**response_kwargs: Any,
) -> HttpResponse:
@ -294,9 +387,9 @@ class Component(View, metaclass=ComponentMeta):
def render(
cls,
context: Optional[Union[Dict[str, Any], Context]] = None,
args: Optional[Union[List, Tuple]] = None,
kwargs: Optional[Dict[str, Any]] = None,
slots: Optional[Mapping[SlotName, SlotContent]] = None,
args: Optional[ArgsType] = None,
kwargs: Optional[KwargsType] = None,
slots: Optional[SlotsType] = None,
escape_slots_content: bool = True,
) -> str:
"""
@ -343,10 +436,10 @@ class Component(View, metaclass=ComponentMeta):
# This is the internal entrypoint for the render function
def _render(
self,
context: Union[Dict[str, Any], Context] = None,
args: Optional[Union[List, Tuple]] = None,
kwargs: Optional[Dict[str, Any]] = None,
slots: Optional[Mapping[SlotName, SlotContent]] = None,
context: Optional[Union[Dict[str, Any], Context]] = None,
args: Optional[ArgsType] = None,
kwargs: Optional[KwargsType] = None,
slots: Optional[SlotsType] = None,
escape_slots_content: bool = True,
) -> str:
try:
@ -356,16 +449,18 @@ class Component(View, metaclass=ComponentMeta):
def _render_impl(
self,
context: Union[Dict[str, Any], Context] = None,
args: Optional[Union[List, Tuple]] = None,
kwargs: Optional[Dict[str, Any]] = None,
slots: Optional[Mapping[SlotName, SlotContent]] = None,
context: Optional[Union[Dict[str, Any], Context]] = None,
args: Optional[ArgsType] = None,
kwargs: Optional[KwargsType] = None,
slots: Optional[SlotsType] = None,
escape_slots_content: bool = True,
) -> str:
has_slots = slots is not None
# Allow to provide no args/kwargs/slots/context
args = args or [] # type: ignore[assignment]
kwargs = kwargs or {} # type: ignore[assignment]
slots = slots or {} # type: ignore[assignment]
args = cast(ArgsType, args or ())
kwargs = cast(KwargsType, kwargs or {})
slots = cast(SlotsType, slots or {})
context = context or Context()
# Allow to provide a dict instead of Context
@ -374,11 +469,20 @@ class Component(View, metaclass=ComponentMeta):
context = context if isinstance(context, Context) else Context(context)
prepare_context(context, self.component_id)
# Temporarily populate _context so user can call `self.inject()` from
# within `get_context_data()`
self._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
# call `self.inject()` from within `get_context_data()`.
self._render_stack.append(
RenderInput(
context=context,
slots=slots,
args=args,
kwargs=kwargs,
escape_slots_content=escape_slots_content,
)
)
context_data = self.get_context_data(*args, **kwargs)
self._context = None
with context.update(context_data):
template = self.get_template(context)
@ -397,8 +501,11 @@ class Component(View, metaclass=ComponentMeta):
template._dc_is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
# Support passing slots explicitly to `render` method
if slots:
fill_content = self._fills_from_slots_data(slots, escape_slots_content)
if has_slots:
fill_content = self._fills_from_slots_data(
slots,
escape_slots_content,
)
else:
fill_content = self.fill_content
@ -407,7 +514,7 @@ class Component(View, metaclass=ComponentMeta):
if not context[_PARENT_COMP_CONTEXT_KEY]:
slot_context_data = self.outer_context.flatten()
slots, resolved_fills = resolve_slots(
_, resolved_fills = resolve_slots(
context,
template,
component_name=self.name,
@ -444,6 +551,10 @@ class Component(View, metaclass=ComponentMeta):
else:
output = rendered_component
# After rendering is done, remove the current state from the stack, which means
# properties like `self.context` will no longer return the current state.
self._render_stack.pop()
return output
def _fills_from_slots_data(

View file

@ -2,7 +2,7 @@ import difflib
import json
import re
from collections import deque
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Set, Tuple, Type, Union
from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Set, Tuple, Type, Union
from django.template import Context, Template
from django.template.base import FilterExpression, Node, NodeList, Parser, TextNode
@ -86,7 +86,7 @@ class SlotFill(NamedTuple):
escaped_name: str
is_filled: bool
content_func: SlotRenderFunc
context_data: Dict
context_data: Mapping
slot_default_var: Optional[SlotDefaultName]
slot_data_var: Optional[SlotDataName]
@ -372,7 +372,7 @@ def resolve_slots(
context: Context,
template: Template,
component_name: Optional[str],
context_data: Dict[str, Any],
context_data: Mapping[str, Any],
fill_content: Dict[SlotName, FillContent],
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]:
"""

View file

@ -3,7 +3,7 @@ Tests focusing on the Component class.
For tests focusing on the `component` tag, see `test_templatetags_component.py`
"""
from typing import Dict
from typing import Dict, Tuple, TypedDict, no_type_check
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
@ -186,6 +186,88 @@ class ComponentTest(BaseTestCase):
""",
)
def test_typed(self):
TestCompArgs = Tuple[int, str]
class TestCompKwargs(TypedDict):
variable: str
another: int
class TestCompData(TypedDict):
abc: int
class TestCompSlots(TypedDict):
my_slot: str
class TestComponent(Component[TestCompArgs, TestCompKwargs, TestCompData, TestCompSlots]):
def get_context_data(self, var1, var2, variable, another, **attrs):
return {
"variable": variable,
}
def get_template(self, context):
template_str: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
{% slot 'my_slot' / %}
"""
return Template(template_str)
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong> MY_SLOT
""",
)
def test_input(self):
tester = self
class TestComponent(Component):
@no_type_check
def get_context_data(self, var1, var2, variable, another, **attrs):
tester.assertEqual(self.input.args, (123, "str"))
tester.assertEqual(self.input.kwargs, {"variable": "test", "another": 1})
tester.assertIsInstance(self.input.context, Context)
tester.assertEqual(self.input.slots, {"my_slot": "MY_SLOT"})
return {
"variable": variable,
}
@no_type_check
def get_template(self, context):
tester.assertEqual(self.input.args, (123, "str"))
tester.assertEqual(self.input.kwargs, {"variable": "test", "another": 1})
tester.assertIsInstance(self.input.context, Context)
tester.assertEqual(self.input.slots, {"my_slot": "MY_SLOT"})
template_str: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
{% slot 'my_slot' / %}
"""
return Template(template_str)
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
self.assertHTMLEqual(
rendered,
"""
Variable: <strong>test</strong> MY_SLOT
""",
)
class ComponentRenderTest(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])

View file

@ -6,12 +6,12 @@ from django.template import Context, Template
from django.test import Client
from django.urls import path
from django_components import Component, register
from django_components import Component, ComponentView, register, types
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
setup_test_config()
setup_test_config({"autodiscover": False})
class CustomClient(Client):
@ -40,9 +40,6 @@ class TestComponentAsView(BaseTestCase):
</form>
"""
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response({"variable": "GET"})
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
return {"variable": variable}
@ -73,12 +70,37 @@ class TestComponentAsView(BaseTestCase):
</form>
"""
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response(kwargs={"variable": "GET"})
def get_context_data(self, variable):
return {"inner_var": variable}
class View(ComponentView):
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.component.render_to_response(kwargs={"variable": "GET"})
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.get("/test/")
self.assertEqual(response.status_code, 200)
self.assertIn(
b'<input type="text" name="variable" value="GET">',
response.content,
)
def test_get_request_shortcut(self):
class MockComponentRequest(Component):
template = """
<form method="post">
{% csrf_token %}
<input type="text" name="variable" value="{{ inner_var }}">
<input type="submit">
</form>
"""
def get_context_data(self, variable):
return {"inner_var": variable}
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response(kwargs={"variable": "GET"})
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.get("/test/")
self.assertEqual(response.status_code, 200)
@ -89,7 +111,7 @@ class TestComponentAsView(BaseTestCase):
def test_post_request(self):
class MockComponentRequest(Component):
template = """
template: types.django_html = """
<form method="post">
{% csrf_token %}
<input type="text" name="variable" value="{{ inner_var }}">
@ -97,13 +119,39 @@ class TestComponentAsView(BaseTestCase):
</form>
"""
def post(self, request, *args, **kwargs) -> HttpResponse:
variable = request.POST.get("variable")
return self.render_to_response(kwargs={"variable": variable})
def get_context_data(self, variable):
return {"inner_var": variable}
class View(ComponentView):
def post(self, request, *args, **kwargs) -> HttpResponse:
variable = request.POST.get("variable")
return self.component.render_to_response(kwargs={"variable": variable})
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.post("/test/", {"variable": "POST"})
self.assertEqual(response.status_code, 200)
self.assertIn(
b'<input type="text" name="variable" value="POST">',
response.content,
)
def test_post_request_shortcut(self):
class MockComponentRequest(Component):
template: types.django_html = """
<form method="post">
{% csrf_token %}
<input type="text" name="variable" value="{{ inner_var }}">
<input type="submit">
</form>
"""
def get_context_data(self, variable):
return {"inner_var": variable}
def post(self, request, *args, **kwargs) -> HttpResponse:
variable = request.POST.get("variable")
return self.render_to_response(kwargs={"variable": variable})
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.post("/test/", {"variable": "POST"})
self.assertEqual(response.status_code, 200)