/", IdomAsyncWebsocketConsumer.as_asgi()
)
-"""A URL resolver for :class:`IdomAsyncWebSocketConsumer`
+"""A URL resolver for :class:`IdomAsyncWebsocketConsumer`
While this is relatively uncommon in most Django apps, because the URL of the
websocket must be defined by the setting ``IDOM_WEBSOCKET_URL``. There's no need
diff --git a/src/django_idom/templates/idom/component.html b/src/django_idom/templates/idom/component.html
index 84ec6963..65ad0c45 100644
--- a/src/django_idom/templates/idom/component.html
+++ b/src/django_idom/templates/idom/component.html
@@ -7,6 +7,7 @@
mountPoint,
"{{ idom_websocket_url }}",
"{{ idom_web_modules_url }}",
+ "{{ idom_ws_max_reconnect_delay }}",
"{{ idom_component_id }}",
"{{ idom_component_params }}"
);
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py
index ba44878c..cd83ebec 100644
--- a/src/django_idom/templatetags/idom.py
+++ b/src/django_idom/templatetags/idom.py
@@ -10,6 +10,7 @@
IDOM_REGISTERED_COMPONENTS,
IDOM_WEB_MODULES_URL,
IDOM_WEBSOCKET_URL,
+ IDOM_WS_MAX_RECONNECT_DELAY,
)
@@ -27,6 +28,7 @@ def idom_component(_component_id_, **kwargs):
"class": class_,
"idom_websocket_url": IDOM_WEBSOCKET_URL,
"idom_web_modules_url": IDOM_WEB_MODULES_URL,
+ "idom_ws_max_reconnect_delay": IDOM_WS_MAX_RECONNECT_DELAY,
"idom_mount_uuid": uuid4().hex,
"idom_component_id": _component_id_,
"idom_component_params": urlencode({"kwargs": json_kwargs}),
diff --git a/src/django_idom/websocket_consumer.py b/src/django_idom/websocket_consumer.py
index 31f1aa38..349a8366 100644
--- a/src/django_idom/websocket_consumer.py
+++ b/src/django_idom/websocket_consumer.py
@@ -2,9 +2,12 @@
import asyncio
import json
import logging
-from typing import Any
+from dataclasses import dataclass
+from typing import Any, Awaitable, Callable, Optional
from urllib.parse import parse_qsl
+from channels.auth import login
+from channels.db import database_sync_to_async as convert_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from idom.core.dispatcher import dispatch_single_view
from idom.core.layout import Layout, LayoutEvent
@@ -15,14 +18,30 @@
_logger = logging.getLogger(__name__)
-class IdomAsyncWebSocketConsumer(AsyncJsonWebsocketConsumer):
- """Communicates with the browser to perform actions on-demand."""
+@dataclass
+class WebsocketConnection:
+ scope: dict
+ close: Callable[[Optional[int]], Awaitable[None]]
+ disconnect: Callable[[int], Awaitable[None]]
+ view_id: str
+
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- super().__init__(*args, **kwargs)
+class IdomAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer):
+ """Communicates with the browser to perform actions on-demand."""
async def connect(self) -> None:
await super().connect()
+
+ user = self.scope.get("user")
+ if user and user.is_authenticated:
+ try:
+ await login(self.scope, user)
+ await convert_to_async(self.scope["session"].save)()
+ except Exception:
+ _logger.exception("IDOM websocket authentication has failed!")
+ elif user is None:
+ _logger.warning("IDOM websocket is missing AuthMiddlewareStack!")
+
self._idom_dispatcher_future = asyncio.ensure_future(self._run_dispatch_loop())
async def disconnect(self, code: int) -> None:
@@ -41,14 +60,17 @@ async def _run_dispatch_loop(self):
try:
component_constructor = IDOM_REGISTERED_COMPONENTS[view_id]
except KeyError:
- _logger.warning(f"Uknown IDOM view ID {view_id!r}")
+ _logger.warning(f"Unknown IDOM view ID {view_id!r}")
return
query_dict = dict(parse_qsl(self.scope["query_string"].decode()))
component_kwargs = json.loads(query_dict.get("kwargs", "{}"))
+ # Provide developer access to parts of this websocket
+ socket = WebsocketConnection(self.scope, self.close, self.disconnect, view_id)
+
try:
- component_instance = component_constructor(**component_kwargs)
+ component_instance = component_constructor(socket, **component_kwargs)
except Exception:
_logger.exception(
f"Failed to construct component {component_constructor} "
diff --git a/src/js/package-lock.json b/src/js/package-lock.json
index ba26a073..6869fe24 100644
--- a/src/js/package-lock.json
+++ b/src/js/package-lock.json
@@ -70,9 +70,9 @@
"integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
},
"idom-client-react": {
- "version": "0.33.2",
- "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.33.2.tgz",
- "integrity": "sha512-B0dIWTYtmSwWUkA7g9/8Stz0Otar/pSaARdxxaPVL8QGvjxaCEnP0M1jklep2SXlbO1U/mXRYIYWgGE2sSlTLg==",
+ "version": "0.33.3",
+ "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.33.3.tgz",
+ "integrity": "sha512-CTqnSzhAVz20IHDexECtCdLuvE2diUIi63kog45sNiJdu5ig+cUKjT4zuA1YHGchf4jJVgsQKPsd1BB3Bx6cew==",
"requires": {
"fast-json-patch": "^3.0.0-1",
"htm": "^3.0.3"
diff --git a/src/js/package.json b/src/js/package.json
index bea68d7a..ea04bf2d 100644
--- a/src/js/package.json
+++ b/src/js/package.json
@@ -18,7 +18,7 @@
"rollup-plugin-replace": "^2.2.0"
},
"dependencies": {
- "idom-client-react": "^0.33.0",
+ "idom-client-react": "^0.33.3",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
diff --git a/src/js/src/index.js b/src/js/src/index.js
index bddf2a55..8f17c0d4 100644
--- a/src/js/src/index.js
+++ b/src/js/src/index.js
@@ -14,18 +14,24 @@ export function mountViewToElement(
mountPoint,
idomWebsocketUrl,
idomWebModulesUrl,
+ maxReconnectTimeout,
viewId,
queryParams
) {
const fullWebsocketUrl =
WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/?" + queryParams;
- const fullWebModulesUrl = LOCATION.origin + "/" + idomWebModulesUrl
+ const fullWebModulesUrl = LOCATION.origin + "/" + idomWebModulesUrl;
const loadImportSource = (source, sourceType) => {
return import(
sourceType == "NAME" ? `${fullWebModulesUrl}${source}` : source
);
};
- mountLayoutWithWebSocket(mountPoint, fullWebsocketUrl, loadImportSource);
+ mountLayoutWithWebSocket(
+ mountPoint,
+ fullWebsocketUrl,
+ loadImportSource,
+ maxReconnectTimeout
+ );
}
diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py
index dcb8112c..2b711cc1 100644
--- a/tests/test_app/asgi.py
+++ b/tests/test_app/asgi.py
@@ -19,12 +19,16 @@
# Fetch ASGI application before importing dependencies that require ORM models.
http_asgi_app = get_asgi_application()
+from channels.auth import AuthMiddlewareStack # noqa: E402
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
+from channels.sessions import SessionMiddlewareStack # noqa: E402
application = ProtocolTypeRouter(
{
"http": http_asgi_app,
- "websocket": URLRouter([IDOM_WEBSOCKET_PATH]),
+ "websocket": SessionMiddlewareStack(
+ AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH]))
+ ),
}
)
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index f242b9f1..d3452efb 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -2,12 +2,12 @@
@idom.component
-def HelloWorld():
+def HelloWorld(websocket):
return idom.html.h1({"id": "hello-world"}, "Hello World!")
@idom.component
-def Button():
+def Button(websocket):
count, set_count = idom.hooks.use_state(0)
return idom.html.div(
idom.html.button(
@@ -22,7 +22,7 @@ def Button():
@idom.component
-def ParametrizedComponent(x, y):
+def ParametrizedComponent(websocket, x, y):
total = x + y
return idom.html.h1({"id": "parametrized-component", "data-value": total}, total)
@@ -32,5 +32,5 @@ def ParametrizedComponent(x, y):
@idom.component
-def SimpleBarChart():
+def SimpleBarChart(websocket):
return VictoryBar()
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
index ac7d6ab9..29eadffa 100644
--- a/tests/test_app/settings.py
+++ b/tests/test_app/settings.py
@@ -26,7 +26,7 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html
index 1ad73c4a..5b2311d2 100644
--- a/tests/test_app/templates/base.html
+++ b/tests/test_app/templates/base.html
@@ -1,23 +1,22 @@
{% load static %} {% load idom %}
-
-
-
-
-
- IDOM
-
-
- IDOM Test Page
- {% idom_component "test_app.components.HelloWorld" class="hello-world" %}
- {% idom_component "test_app.components.Button" class="button" %}
- {% idom_component "test_app.components.ParametrizedComponent" class="parametarized-component" x=123 y=456 %}
- {% idom_component "test_app.components.SimpleBarChart" class="simple-bar-chart" %}
-
+
+
+
+
+
+ IDOM
+
+
+
+ IDOM Test Page
+ {% idom_component "test_app.components.HelloWorld" class="hello-world" %}
+ {% idom_component "test_app.components.Button" class="button" %}
+ {% idom_component "test_app.components.ParametrizedComponent" class="parametarized-component" x=123 y=456 %}
+
+ {% idom_component "test_app.components.SimpleBarChart" class="simple-bar-chart" %}
+
+
diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py
index cd4f3da0..1ea0c99b 100644
--- a/tests/test_app/urls.py
+++ b/tests/test_app/urls.py
@@ -17,6 +17,7 @@
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
+from django.contrib import admin
from django.urls import path
from django_idom import IDOM_WEB_MODULES_PATH
@@ -24,4 +25,8 @@
from .views import base_template
-urlpatterns = [path("", base_template), IDOM_WEB_MODULES_PATH]
+urlpatterns = [
+ path("", base_template),
+ IDOM_WEB_MODULES_PATH,
+ path("admin/", admin.site.urls),
+]
diff --git a/tests/test_app/views.py b/tests/test_app/views.py
index 248235a7..11874908 100644
--- a/tests/test_app/views.py
+++ b/tests/test_app/views.py
@@ -1,7 +1,6 @@
-from django.http import HttpResponse
-from django.template import loader
+from django.shortcuts import render
def base_template(request):
context = {}
- return HttpResponse(loader.get_template("base.html").render(context, request))
+ return render(request, "base.html", context)