diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a579201..cab02d0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,13 +24,28 @@ Using the following categories, list your changes in this order:
- Nothing (yet)
-## [2.0.1]- 2022-10-18
+## [2.1.0] - 2022-11-01
+
+## Changed
+
+- Minimum `channels` version is now `4.0.0`.
+
+### Fixed
+
+- Change type hint on `view_to_component` callable to have `request` argument be optional.
+- Change type hint on `view_to_component` to represent it as a decorator with paranthesis (ex `@view_to_component(compatibility=True)`)
+
+## Security
+
+- Add note to docs about potential information exposure via `view_to_component` when using `compatibility=True`.
+
+## [2.0.1] - 2022-10-18
### Fixed
-- Ability to use `key=...` parameter on all prefabricated components
+- Ability to use `key=...` parameter on all prefabricated components.
-## [2.0.0]- 2022-10-17
+## [2.0.0] - 2022-10-17
### Added
@@ -155,7 +170,8 @@ Using the following categories, list your changes in this order:
- Support for IDOM within the Django
-[unreleased]: https://github.com/idom-team/django-idom/compare/2.0.1...HEAD
+[unreleased]: https://github.com/idom-team/django-idom/compare/2.1.0...HEAD
+[2.1.0]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0
[2.0.1]: https://github.com/idom-team/django-idom/compare/2.0.0...2.0.1
[2.0.0]: https://github.com/idom-team/django-idom/compare/1.2.0...2.0.0
[1.2.0]: https://github.com/idom-team/django-idom/compare/1.1.0...1.2.0
diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md
index ece8d1a7..f1ff6cc1 100644
--- a/docs/src/contribute/running-tests.md
+++ b/docs/src/contribute/running-tests.md
@@ -1,15 +1,26 @@
-This repo uses [Nox](https://nox.thea.codes/en/stable/) to run scripts which can be found in `noxfile.py`. For a full test of available scripts run `nox -l`. To run the full test suite simple execute:
+This repo uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`.
+
+If you plan to run tests, you'll need to install the following dependencies first:
+
+- [Python 3.8+](https://www.python.org/downloads/)
+- [Git](https://git-scm.com/downloads)
+
+Once done, you should clone this repository:
+
+```bash
+git clone https://github.com/idom-team/django-idom.git
+cd django-idom
+pip install -r ./requirements/test-run.txt --upgrade
+```
+
+Then, by running the command below you can run the full test suite:
```
nox -s test
```
-If you do not want to run the tests in the background:
+Or, if you want to run the tests in the foreground:
```
nox -s test -- --headed
```
-
-!!! warning "Most tests will not run on Windows"
-
- Due to [bugs within Django Channels](https://github.com/django/channels/issues/1207), functional tests are not run on Windows. In order for Windows users to test Django-IDOM functionality, you will need to run tests via [Windows Subsystem for Linux](https://code.visualstudio.com/docs/remote/wsl).
diff --git a/docs/src/features/components.md b/docs/src/features/components.md
index c8a110f9..eca86f66 100644
--- a/docs/src/features/components.md
+++ b/docs/src/features/components.md
@@ -4,7 +4,7 @@
## View To Component
-Convert any Django view into a IDOM component by usng this decorator. Compatible with sync/async [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/).
+Convert any Django view into a IDOM component by usng this decorator. Compatible with [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/). Views can be sync or async.
=== "components.py"
@@ -41,11 +41,42 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
| --- | --- |
| `_ViewComponentConstructor` | A function that takes `request: HttpRequest | None, *args: Any, key: Key | None, **kwargs: Any` and returns an IDOM component. |
-??? warning "Existing limitations"
+??? Warning "Potential information exposure when using `compatibility = True`"
+
+ When using `compatibility` mode, IDOM automatically exposes a URL to your view.
+
+ It is your responsibility to ensure priveledged information is not leaked via this method.
+
+ This can be done via directly writing conditionals into your view, or by adding decorators such as [user_passes_test](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views prior to using `view_to_component`.
+
+ === "Function Based View"
+
+ ```python
+ ...
+
+ @view_to_component(compatibility=True)
+ @user_passes_test(lambda u: u.is_superuser)
+ def example_view(request):
+ ...
+ ```
+
+ === "Class Based View"
+
+ ```python
+ ...
+
+ @view_to_component(compatibility=True)
+ @method_decorator(user_passes_test(lambda u: u.is_superuser), name="dispatch")
+ class ExampleView(TemplateView):
+ ...
+ ```
+
+??? info "Existing limitations"
There are currently several limitations of using `view_to_component` that may be resolved in a future version of `django_idom`.
- Requires manual intervention to change request methods beyond `GET`.
+ - IDOM events cannot conveniently be attached to converted view HTML.
- Does not currently load any HTML contained with a `
` tag
- Has no option to automatically intercept local anchor link (ex. `#!html `) click events
@@ -77,9 +108,9 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
??? question "How do I transform views from external libraries?"
- === "components.py"
+ In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator.
- In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator.
+ === "components.py"
```python linenums="1"
from idom import component, html
@@ -87,77 +118,85 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
from django_idom.components import view_to_component
from some_library.views import example_view
- converted_view = view_to_component(example_view)
+ example_vtc = view_to_component(example_view)
@component
def my_component():
return html.div(
- converted_view(),
+ example_vtc(),
)
```
-??? question "How do I provide `args` and `kwargs` to a view?"
+??? question "How do I provide `request`, `args`, and `kwargs` to a view?"
- You can use the `args` and `kwargs` parameters to provide positional and keyworded arguments to a view.
+ **`Request`**
+
+ You can use the `request` parameter to provide the view a custom request object.
=== "components.py"
```python linenums="1"
from idom import component, html
- from django.http import HttpResponse
+ from django.http import HttpResponse, HttpRequest
from django_idom.components import view_to_component
+ example_request = HttpRequest()
+ example_request.method = "PUT"
+
@view_to_component
- def hello_world_view(request, arg1, arg2, key1=None, key2=None):
- return HttpResponse(f"Hello World! {arg1} {arg2} {key1} {key2}")
+ def hello_world_view(request):
+ return HttpResponse(f"Hello World! {request.method}")
@component
def my_component():
return html.div(
hello_world_view(
- None, # Your request object (optional)
- "value_1",
- "value_2",
- key1="abc",
- key2="123",
+ example_request,
),
)
```
-??? question "How do I provide a custom `request` object to a view?"
+ ---
- You can use the `request` parameter to provide the view a custom request object.
+ **`args` and `kwargs`**
+
+ You can use the `args` and `kwargs` parameters to provide positional and keyworded arguments to a view.
=== "components.py"
```python linenums="1"
from idom import component, html
- from django.http import HttpResponse, HttpRequest
+ from django.http import HttpResponse
from django_idom.components import view_to_component
- example_request = HttpRequest()
- example_request.method = "PUT"
-
@view_to_component
- def hello_world_view(request):
- return HttpResponse(f"Hello World! {request.method}")
+ def hello_world_view(request, arg1, arg2, key1=None, key2=None):
+ return HttpResponse(f"Hello World! {arg1} {arg2} {key1} {key2}")
@component
def my_component():
return html.div(
hello_world_view(
- example_request,
+ None, # Your request object (optional)
+ "value_1",
+ "value_2",
+ key1="abc",
+ key2="123",
),
)
```
-??? question "What is `compatibility` mode?"
+??? question "How do I use `strict_parseing`, `compatibility`, and `transforms`?"
- For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe.
+ **`strict_parsing`**
- Any view can be rendered within compatibility mode. However, the `transforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments do not apply to compatibility mode.
+ By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5.
- Please note that by default the iframe is unstyled, and thus won't look pretty until you add some CSS.
+ However, there are some circumstances where you may not have control over the original HTML, so you may be unable to fix it. Or you may be relying on non-standard HTML tags such as `#!html Hello World `.
+
+ In these scenarios, you may want to rely on best-fit parsing by setting the `strict_parsing` parameter to `False`.
+
+ Note that best-fit parsing is designed to be similar to how web browsers would handle non-standard or broken HTML.
=== "components.py"
@@ -166,9 +205,9 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
from django.http import HttpResponse
from django_idom.components import view_to_component
- @view_to_component(compatibility=True)
+ @view_to_component(strict_parsing=False)
def hello_world_view(request):
- return HttpResponse("Hello World!")
+ return HttpResponse(" Hello World ")
@component
def my_component():
@@ -177,13 +216,17 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
)
```
-??? question "What is `strict_parsing`?"
- By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5.
- However, there are some circumstances where you may not have control over the original HTML, so you may be unable to fix it. Or you may be relying on non-standard HTML tags such as `#!html Hello World `.
+ ---
- In these scenarios, you may want to rely on best-fit parsing by setting the `strict_parsing` parameter to `False`.
+ **`compatibility`**
+
+ For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe.
+
+ Any view can be rendered within compatibility mode. However, the `transforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments do not apply to compatibility mode.
+
+ Please note that by default the iframe is unstyled, and thus won't look pretty until you add some CSS.
=== "components.py"
@@ -192,9 +235,9 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
from django.http import HttpResponse
from django_idom.components import view_to_component
- @view_to_component(strict_parsing=False)
+ @view_to_component(compatibility=True)
def hello_world_view(request):
- return HttpResponse(" Hello World ")
+ return HttpResponse("Hello World!")
@component
def my_component():
@@ -203,9 +246,9 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible
)
```
- Note that best-fit parsing is very similar to how web browsers will handle broken HTML.
+ ---
-??? question "What is `transforms`?"
+ **`transforms`**
After your view has been turned into [VDOM](https://idom-docs.herokuapp.com/docs/reference/specifications.html#vdom) (python dictionaries), `view_to_component` will call your `transforms` functions on every VDOM node.
diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md
index e9e67e39..b35f43ae 100644
--- a/docs/src/features/hooks.md
+++ b/docs/src/features/hooks.md
@@ -324,12 +324,6 @@ You can expect this hook to provide strings such as `/idom/my_path`.
return html.div(my_location)
```
-??? info "This hook's behavior will be changed in a future update"
-
- This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support.
-
- Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information.
-
??? example "See Interface"
**Parameters**
@@ -342,6 +336,12 @@ You can expect this hook to provide strings such as `/idom/my_path`.
| --- | --- |
| `Location` | A object containing the current URL's `pathname` and `search` query. |
+??? info "This hook's behavior will be changed in a future update"
+
+ This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support.
+
+ Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information.
+
## Use Origin
This is a shortcut that returns the Websocket's `origin`.
diff --git a/docs/src/getting-started/learn-more.md b/docs/src/getting-started/learn-more.md
index e9aa7419..c2d98e1b 100644
--- a/docs/src/getting-started/learn-more.md
+++ b/docs/src/getting-started/learn-more.md
@@ -8,4 +8,4 @@ Additionally, the vast majority of tutorials/guides you find for React can be ap
| Learn More |
| --- |
-| [Django-IDOM Advanced Usage](../features/components.md){ .md-button } [IDOM Hooks, Events, and More](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html){ .md-button } [Ask Questions on GitHub Discussions](https://github.com/idom-team/idom/discussions){ .md-button .md-button--primary } |
+| [Django-IDOM Advanced Usage](../features/components.md){ .md-button } [IDOM Core Documentation](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html){ .md-button } [Ask Questions on GitHub Discussions](https://github.com/idom-team/idom/discussions){ .md-button .md-button--primary } |
diff --git a/docs/src/installation/index.md b/docs/src/installation/index.md
index 61782278..a59816fc 100644
--- a/docs/src/installation/index.md
+++ b/docs/src/installation/index.md
@@ -29,7 +29,7 @@ In your settings you'll need to add `django_idom` to [`INSTALLED_APPS`](https://
Django-IDOM requires ASGI in order to use Websockets.
- If you haven't enabled ASGI on your **Django project** yet, you'll need to add `channels` to `INSTALLED_APPS` and set your `ASGI_APPLICATION` variable.
+ If you haven't enabled ASGI on your **Django project** yet, you'll need to install `channels[daphne]`, add `daphne` to `INSTALLED_APPS`, then set your `ASGI_APPLICATION` variable.
Read the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info.
@@ -37,7 +37,7 @@ In your settings you'll need to add `django_idom` to [`INSTALLED_APPS`](https://
```python linenums="1"
INSTALLED_APPS = [
- "channels",
+ "daphne",
...
]
ASGI_APPLICATION = "example_project.asgi.application"
diff --git a/noxfile.py b/noxfile.py
index c0c63855..1e55dabf 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -63,7 +63,7 @@ def test_suite(session: Session) -> None:
def test_types(session: Session) -> None:
install_requirements_file(session, "check-types")
install_requirements_file(session, "pkg-deps")
- session.run("mypy", "--show-error-codes", "src/django_idom")
+ session.run("mypy", "--show-error-codes", "src/django_idom", "tests/test_app")
@nox.session
diff --git a/pyproject.toml b/pyproject.toml
index 0103531f..6bf3a9fc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,3 +16,4 @@ ignore_missing_imports = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
+check_untyped_defs = true
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
index 8fb71a85..d2329eb6 100644
--- a/requirements/pkg-deps.txt
+++ b/requirements/pkg-deps.txt
@@ -1,4 +1,4 @@
-channels >=3.0.0
+channels >=4.0.0
idom >=0.40.2, <0.41.0
aiofile >=3.0
typing_extensions
diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py
index 04bb0519..406f04db 100644
--- a/src/django_idom/__init__.py
+++ b/src/django_idom/__init__.py
@@ -3,7 +3,7 @@
from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH
-__version__ = "2.0.1"
+__version__ = "2.1.0"
__all__ = [
"IDOM_WEBSOCKET_PATH",
"IdomWebsocket",
diff --git a/src/django_idom/components.py b/src/django_idom/components.py
index 8c9ff3fa..163eb47b 100644
--- a/src/django_idom/components.py
+++ b/src/django_idom/components.py
@@ -2,10 +2,8 @@
import json
import os
-from inspect import iscoroutinefunction
-from typing import Any, Callable, Dict, Protocol, Sequence
+from typing import Any, Callable, Protocol, Sequence, Union, cast, overload
-from channels.db import database_sync_to_async
from django.contrib.staticfiles.finders import find
from django.http import HttpRequest
from django.urls import reverse
@@ -15,12 +13,12 @@
from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES
from django_idom.types import ViewComponentIframe
-from django_idom.utils import _generate_obj_name
+from django_idom.utils import _generate_obj_name, render_view
class _ViewComponentConstructor(Protocol):
def __call__(
- self, request: HttpRequest | None, *args: Any, **kwargs: Any
+ self, request: HttpRequest | None = None, *args: Any, **kwargs: Any
) -> ComponentType:
...
@@ -33,91 +31,92 @@ def _view_to_component(
strict_parsing: bool,
request: HttpRequest | None,
args: Sequence | None,
- kwargs: Dict | None,
+ kwargs: dict | None,
):
- converted_view, set_converted_view = hooks.use_state(None)
- args = args or ()
- kwargs = kwargs or {}
- request_obj = request
- if not request_obj:
- request_obj = HttpRequest()
- request_obj.method = "GET"
- if compatibility:
- dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore[union-attr]
- dotted_path = dotted_path.replace("<", "").replace(">", "")
- IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe(
- view, args, kwargs
- )
+ converted_view, set_converted_view = hooks.use_state(
+ cast(Union[VdomDict, None], None)
+ )
+ _args: Sequence = args or ()
+ _kwargs: dict = kwargs or {}
+ if request:
+ _request: HttpRequest = request
else:
- dotted_path = None
+ _request = HttpRequest()
+ _request.method = "GET"
# Render the view render within a hook
@hooks.use_effect(
dependencies=[
- json.dumps(vars(request_obj), default=lambda x: _generate_obj_name(x)),
- json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)),
+ json.dumps(vars(_request), default=lambda x: _generate_obj_name(x)),
+ json.dumps([_args, _kwargs], default=lambda x: _generate_obj_name(x)),
]
)
async def async_render():
"""Render the view in an async hook to avoid blocking the main thread."""
- # Render Check 1: Compatibility mode
+ # Compatibility mode doesn't require a traditional render
if compatibility:
- set_converted_view(
- html.iframe(
- {
- "src": reverse("idom:view_to_component", args=[dotted_path]),
- "loading": "lazy",
- }
- )
- )
return
- # Render Check 2: Async function view
- elif iscoroutinefunction(view):
- view_html = await view(request_obj, *args, **kwargs)
-
- # Render Check 3: Async class view
- elif getattr(view, "view_is_async", False):
- view_or_template_view = await view.as_view()(request_obj, *args, **kwargs)
- if getattr(view_or_template_view, "render", None): # TemplateView
- view_html = await view_or_template_view.render()
- else: # View
- view_html = view_or_template_view
-
- # Render Check 4: Sync class view
- elif getattr(view, "as_view", None):
- async_cbv = database_sync_to_async(view.as_view())
- view_or_template_view = await async_cbv(request_obj, *args, **kwargs)
- if getattr(view_or_template_view, "render", None): # TemplateView
- view_html = await database_sync_to_async(view_or_template_view.render)()
- else: # View
- view_html = view_or_template_view
-
- # Render Check 5: Sync function view
- else:
- view_html = await database_sync_to_async(view)(request_obj, *args, **kwargs)
-
- # Signal that the view has been rendered
+ # Render the view
+ response = await render_view(view, _request, _args, _kwargs)
set_converted_view(
utils.html_to_vdom(
- view_html.content.decode("utf-8").strip(),
+ response.content.decode("utf-8").strip(),
*transforms,
strict=strict_parsing,
)
)
+ # Render in compatibility mode, if needed
+ if compatibility:
+ dotted_path = f"{view.__module__}.{view.__name__}".replace("<", "").replace(">", "") # type: ignore
+ IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe(
+ view, _args, _kwargs
+ )
+ return html.iframe(
+ {
+ "src": reverse("idom:view_to_component", args=[dotted_path]),
+ "loading": "lazy",
+ }
+ )
+
# Return the view if it's been rendered via the `async_render` hook
return converted_view
+# Type hints for:
+# 1. example = view_to_component(my_view, ...)
+# 2. @view_to_component
+@overload
+def view_to_component(
+ view: Callable | View,
+ compatibility: bool = False,
+ transforms: Sequence[Callable[[VdomDict], Any]] = (),
+ strict_parsing: bool = True,
+) -> _ViewComponentConstructor:
+ ...
+
+
+# Type hints for:
+# 1. @view_to_component(...)
+@overload
+def view_to_component(
+ view: None = ...,
+ compatibility: bool = False,
+ transforms: Sequence[Callable[[VdomDict], Any]] = (),
+ strict_parsing: bool = True,
+) -> Callable[[Callable], _ViewComponentConstructor]:
+ ...
+
+
# TODO: Might want to intercept href clicks and form submit events.
# Form events will probably be accomplished through the upcoming DjangoForm.
def view_to_component(
- view: Callable | View = None, # type: ignore[assignment]
+ view: Callable | View | None = None,
compatibility: bool = False,
transforms: Sequence[Callable[[VdomDict], Any]] = (),
strict_parsing: bool = True,
-) -> _ViewComponentConstructor:
+) -> _ViewComponentConstructor | Callable[[Callable], _ViewComponentConstructor]:
"""Converts a Django view to an IDOM component.
Keyword Args:
diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py
index 5800d220..6d01c990 100644
--- a/src/django_idom/decorators.py
+++ b/src/django_idom/decorators.py
@@ -11,7 +11,7 @@
def auth_required(
component: Callable | None = None,
auth_attribute: str = "is_active",
- fallback: ComponentType | VdomDict | None = None,
+ fallback: ComponentType | Callable | VdomDict | None = None,
) -> Callable:
"""If the user passes authentication criteria, the decorated component will be rendered.
Otherwise, the fallback component will be rendered.
diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py
index 033e9ffd..262634f9 100644
--- a/src/django_idom/http/views.py
+++ b/src/django_idom/http/views.py
@@ -1,13 +1,12 @@
import os
-from inspect import iscoroutinefunction
from aiofile import async_open
-from channels.db import database_sync_to_async
from django.core.exceptions import SuspiciousOperation
-from django.http import HttpRequest, HttpResponse
+from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
from idom.config import IDOM_WED_MODULES_DIR
from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES
+from django_idom.utils import render_view
async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
@@ -44,27 +43,10 @@ async def view_to_component_iframe(
# Get the view from IDOM_REGISTERED_IFRAMES
iframe = IDOM_VIEW_COMPONENT_IFRAMES.get(view_path)
if not iframe:
- raise ValueError(f"No view registered for component {view_path}.")
+ return HttpResponseNotFound()
- # Render Check 1: Async function view
- if iscoroutinefunction(iframe.view):
- response = await iframe.view(request, *iframe.args, **iframe.kwargs) # type: ignore[operator]
-
- # Render Check 2: Async class view
- elif getattr(iframe.view, "view_is_async", False):
- response = await iframe.view.as_view()(request, *iframe.args, **iframe.kwargs) # type: ignore[misc, union-attr]
-
- # Render Check 3: Sync class view
- elif getattr(iframe.view, "as_view", None):
- response = await database_sync_to_async(iframe.view.as_view())( # type: ignore[union-attr]
- request, *iframe.args, **iframe.kwargs
- )
-
- # Render Check 4: Sync function view
- else:
- response = await database_sync_to_async(iframe.view)(
- request, *iframe.args, **iframe.kwargs
- )
+ # Render the view
+ response = await render_view(iframe.view, request, iframe.args, iframe.kwargs)
# Ensure page can be rendered as an iframe
response["X-Frame-Options"] = "SAMEORIGIN"
diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py
index 6b34145d..8394589e 100644
--- a/src/django_idom/utils.py
+++ b/src/django_idom/utils.py
@@ -6,10 +6,14 @@
import re
from fnmatch import fnmatch
from importlib import import_module
-from typing import Any, Callable
+from inspect import iscoroutinefunction
+from typing import Any, Callable, Sequence
+from channels.db import database_sync_to_async
+from django.http import HttpRequest, HttpResponse
from django.template import engines
from django.utils.encoding import smart_str
+from django.views import View
from django_idom.config import IDOM_REGISTERED_COMPONENTS
@@ -30,6 +34,44 @@
)
+async def render_view(
+ view: Callable | View,
+ request: HttpRequest,
+ args: Sequence,
+ kwargs: dict,
+) -> HttpResponse:
+ """Ingests a Django view (class or function) and returns an HTTP response object."""
+ # Render Check 1: Async function view
+ if iscoroutinefunction(view) and callable(view):
+ response = await view(request, *args, **kwargs)
+
+ # Render Check 2: Async class view
+ elif getattr(view, "view_is_async", False):
+ # django-stubs does not support async views yet, so we have to ignore types here
+ view_or_template_view = await view.as_view()(request, *args, **kwargs) # type: ignore
+ if getattr(view_or_template_view, "render", None): # TemplateView
+ response = await view_or_template_view.render()
+ else: # View
+ response = view_or_template_view
+
+ # Render Check 3: Sync class view
+ elif getattr(view, "as_view", None):
+ # MyPy does not know how to properly interpret this as a `View` type
+ # And `isinstance(view, View)` does not work due to some weird Django internal shenanigans
+ async_cbv = database_sync_to_async(view.as_view()) # type: ignore
+ view_or_template_view = await async_cbv(request, *args, **kwargs)
+ if getattr(view_or_template_view, "render", None): # TemplateView
+ response = await database_sync_to_async(view_or_template_view.render)()
+ else: # View
+ response = view_or_template_view
+
+ # Render Check 4: Sync function view
+ else:
+ response = await database_sync_to_async(view)(request, *args, **kwargs)
+
+ return response
+
+
def _register_component(dotted_path: str) -> None:
if dotted_path in IDOM_REGISTERED_COMPONENTS:
return
@@ -70,7 +112,7 @@ def _get_loaders(self):
for e in engines.all():
if hasattr(e, "engine"):
template_source_loaders.extend(
- e.engine.get_template_loaders(e.engine.loaders)
+ e.engine.get_template_loaders(e.engine.loaders) # type: ignore
)
loaders = []
for loader in template_source_loaders:
diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py
index cdc3eb9f..c4c78971 100644
--- a/src/django_idom/websocket/consumer.py
+++ b/src/django_idom/websocket/consumer.py
@@ -71,7 +71,7 @@ async def _run_dispatch_loop(self):
)
return
- self._idom_recv_queue = recv_queue = asyncio.Queue()
+ self._idom_recv_queue = recv_queue = asyncio.Queue() # type: ignore
try:
await serve_json_patch(
Layout(WebsocketContext(component_instance, value=socket)),
diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py
index aa897829..da45013d 100644
--- a/tests/test_app/admin.py
+++ b/tests/test_app/admin.py
@@ -1,6 +1,5 @@
from django.contrib import admin
-
-from .models import TodoItem
+from test_app.models import TodoItem
@admin.register(TodoItem)
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index f0a405f0..01e0abd3 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -200,7 +200,7 @@ def todo_list():
elif add_item.error:
mutation_status = html.h2(f"Error when adding - {add_item.error}")
else:
- mutation_status = ""
+ mutation_status = "" # type: ignore
def on_submit(event):
if event["key"] == "Enter":
@@ -279,7 +279,7 @@ def _render_items(items, toggle_item):
@component
def view_to_component_sync_func_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_sync_func_compatibility(key="test"),
html.hr(),
)
@@ -288,7 +288,7 @@ def view_to_component_sync_func_compatibility():
@component
def view_to_component_async_func_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_async_func_compatibility(),
html.hr(),
)
@@ -297,7 +297,7 @@ def view_to_component_async_func_compatibility():
@component
def view_to_component_sync_class_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_sync_class_compatibility(),
html.hr(),
)
@@ -306,7 +306,7 @@ def view_to_component_sync_class_compatibility():
@component
def view_to_component_async_class_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_async_class_compatibility(),
html.hr(),
)
@@ -315,7 +315,7 @@ def view_to_component_async_class_compatibility():
@component
def view_to_component_template_view_class_compatibility():
return html.div(
- {"id": inspect.currentframe().f_code.co_name},
+ {"id": inspect.currentframe().f_code.co_name}, # type: ignore
_view_to_component_template_view_class_compatibility(),
html.hr(),
)
@@ -328,11 +328,11 @@ def view_to_component_request():
def on_click(_):
post_request = HttpRequest()
post_request.method = "POST"
- set_request(post_request)
+ set_request(post_request) # type: ignore
return html._(
html.button(
- {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click},
+ {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore
"Click me",
),
_view_to_component_request(request=request),
@@ -348,7 +348,7 @@ def on_click(_):
return html._(
html.button(
- {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click},
+ {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore
"Click me",
),
_view_to_component_args(None, success),
@@ -364,7 +364,7 @@ def on_click(_):
return html._(
html.button(
- {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click},
+ {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore
"Click me",
),
_view_to_component_kwargs(success=success),
@@ -376,7 +376,7 @@ def view_to_component_decorator(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore
)
@@ -385,5 +385,5 @@ def view_to_component_decorator_args(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore
)
diff --git a/tests/test_app/migrations/0001_initial.py b/tests/test_app/migrations/0001_initial.py
index e05fedd6..69498ba5 100644
--- a/tests/test_app/migrations/0001_initial.py
+++ b/tests/test_app/migrations/0001_initial.py
@@ -7,16 +7,23 @@ class Migration(migrations.Migration):
initial = True
- dependencies = [
- ]
+ dependencies = [] # type: ignore
operations = [
migrations.CreateModel(
- name='TodoItem',
+ name="TodoItem",
fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('done', models.BooleanField()),
- ('text', models.CharField(max_length=1000)),
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("done", models.BooleanField()),
+ ("text", models.CharField(max_length=1000)),
],
),
]
diff --git a/tests/test_app/models.py b/tests/test_app/models.py
index 93d3efd2..7b6fc0f8 100644
--- a/tests/test_app/models.py
+++ b/tests/test_app/models.py
@@ -2,5 +2,5 @@
class TodoItem(models.Model):
- done = models.BooleanField()
- text = models.CharField(max_length=1000)
+ done = models.BooleanField() # type: ignore
+ text = models.CharField(max_length=1000) # type: ignore
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
index 8f5ff109..6cb4dc82 100644
--- a/tests/test_app/settings.py
+++ b/tests/test_app/settings.py
@@ -30,6 +30,7 @@
# Application definition
INSTALLED_APPS = [
+ "daphne", # Overrides `runserver` command with an ASGI server
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py
index 3038eb46..0c996ac4 100644
--- a/tests/test_app/urls.py
+++ b/tests/test_app/urls.py
@@ -27,7 +27,7 @@ class AccessUser:
has_module_perms = has_perm = __getattr__ = lambda s, *a, **kw: True
-admin.site.has_permission = lambda r: setattr(r, "user", AccessUser()) or True
+admin.site.has_permission = lambda r: setattr(r, "user", AccessUser()) or True # type: ignore
urlpatterns = [
path("", base_template),
diff --git a/tests/test_app/views.py b/tests/test_app/views.py
index cec726f5..2d16ae97 100644
--- a/tests/test_app/views.py
+++ b/tests/test_app/views.py
@@ -6,15 +6,14 @@
def base_template(request):
- context = {}
- return render(request, "base.html", context)
+ return render(request, "base.html", {})
def view_to_component_sync_func(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
@@ -22,7 +21,7 @@ async def view_to_component_async_func(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
@@ -55,7 +54,7 @@ def view_to_component_sync_func_compatibility(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
@@ -63,7 +62,7 @@ async def view_to_component_async_func_compatibility(request):
return await database_sync_to_async(render)(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
@@ -97,7 +96,7 @@ def view_to_component_script(request):
request,
"view_to_component_script.html",
{
- "test_name": inspect.currentframe().f_code.co_name,
+ "test_name": inspect.currentframe().f_code.co_name, # type:ignore
"status": "false",
},
)
@@ -108,14 +107,14 @@ def view_to_component_request(request):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name},
+ {"test_name": inspect.currentframe().f_code.co_name}, # type:ignore
)
return render(
request,
"view_to_component.html",
{
- "test_name": inspect.currentframe().f_code.co_name,
+ "test_name": inspect.currentframe().f_code.co_name, # type:ignore
"status": "false",
"success": "false",
},
@@ -126,7 +125,10 @@ def view_to_component_args(request, success):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name, "status": success},
+ {
+ "test_name": inspect.currentframe().f_code.co_name, # type:ignore
+ "status": success,
+ },
)
@@ -134,5 +136,8 @@ def view_to_component_kwargs(request, success="false"):
return render(
request,
"view_to_component.html",
- {"test_name": inspect.currentframe().f_code.co_name, "status": success},
+ {
+ "test_name": inspect.currentframe().f_code.co_name, # type:ignore
+ "status": success,
+ },
)