From 48ef41da7f0e19ce3654914be21b7ae251fa7ec9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 9 Jan 2023 15:42:37 -0800 Subject: [PATCH 01/56] fix use_location bug --- src/django_idom/hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 4455f779..b40735c9 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -45,7 +45,9 @@ def use_location() -> Location: # TODO: Use the browser's current page, rather than the WS route scope = use_scope() search = scope["query_string"].decode() - return Location(scope["path"], f"?{search}" if search else "") + return Location( + scope["path"], f"?{search}" if (search and (search != "undefined")) else "" + ) def use_origin() -> str | None: From 89a59d6a08b97c56fa865ead8bd0b6fe4170b9ae Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 9 Jan 2023 16:55:07 -0800 Subject: [PATCH 02/56] add missing migration --- .../0003_alter_relationalparent_one_to_one.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/test_app/migrations/0003_alter_relationalparent_one_to_one.py diff --git a/tests/test_app/migrations/0003_alter_relationalparent_one_to_one.py b/tests/test_app/migrations/0003_alter_relationalparent_one_to_one.py new file mode 100644 index 00000000..1d3ab63d --- /dev/null +++ b/tests/test_app/migrations/0003_alter_relationalparent_one_to_one.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.3 on 2023-01-10 00:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("test_app", "0002_relationalchild_relationalparent_foriegnchild"), + ] + + operations = [ + migrations.AlterField( + model_name="relationalparent", + name="one_to_one", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="one_to_one", + to="test_app.relationalchild", + ), + ), + ] From 2247a183e210841d00bd7679c3556069b6aee3d0 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 9 Jan 2023 16:55:20 -0800 Subject: [PATCH 03/56] enable django mypy plugin --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6bf3a9fc..181cbf89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,3 +17,8 @@ warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true check_untyped_defs = true +plugins = ["mypy_django_plugin.main"] + +[tool.django-stubs] +django_settings_module = 'test_app.settings' + From f2b5a3a496df836eb3ac1b577216bc94f797ecd6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 9 Jan 2023 18:23:31 -0800 Subject: [PATCH 04/56] Functional DB-backed component params --- CHANGELOG.md | 4 +- requirements/pkg-deps.txt | 1 + src/django_idom/config.py | 6 +-- src/django_idom/migrations/0001_initial.py | 26 ++++++++++ src/django_idom/migrations/__init__.py | 0 src/django_idom/models.py | 7 +++ src/django_idom/templates/idom/component.html | 7 ++- src/django_idom/templatetags/idom.py | 33 ++++++++----- src/django_idom/types.py | 11 ++++- src/django_idom/utils.py | 10 +++- src/django_idom/websocket/consumer.py | 36 +++++++++----- src/django_idom/websocket/paths.py | 2 +- src/js/src/index.js | 49 +++++++++---------- tests/test_app/components.py | 2 +- 14 files changed, 131 insertions(+), 63 deletions(-) create mode 100644 src/django_idom/migrations/0001_initial.py create mode 100644 src/django_idom/migrations/__init__.py create mode 100644 src/django_idom/models.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7c1daa..460bfce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,9 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet) +### Security + +- Fixed a potential method of component argument spoofing ## [2.2.1] - 2022-01-09 diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index d2329eb6..873b689f 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,4 +1,5 @@ channels >=4.0.0 idom >=0.40.2, <0.41.0 aiofile >=3.0 +dill >=0.3.5 typing_extensions diff --git a/src/django_idom/config.py b/src/django_idom/config.py index 0e1d4cd7..7cd8a6db 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -17,10 +17,10 @@ "IDOM_WEBSOCKET_URL", "idom/", ) -IDOM_WS_MAX_RECONNECT_TIMEOUT = getattr( +IDOM_MAX_RECONNECT_TIMEOUT = getattr( settings, - "IDOM_WS_MAX_RECONNECT_TIMEOUT", - 604800, + "IDOM_MAX_RECONNECT_TIMEOUT", + 259200, # Default to 3 days ) IDOM_CACHE: BaseCache = ( caches["idom"] diff --git a/src/django_idom/migrations/0001_initial.py b/src/django_idom/migrations/0001_initial.py new file mode 100644 index 00000000..3ad19d56 --- /dev/null +++ b/src/django_idom/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 4.1.3 on 2023-01-10 00:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ComponentParams", + fields=[ + ( + "uuid", + models.UUIDField( + editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ("data", models.BinaryField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/src/django_idom/migrations/__init__.py b/src/django_idom/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/django_idom/models.py b/src/django_idom/models.py new file mode 100644 index 00000000..4e509255 --- /dev/null +++ b/src/django_idom/models.py @@ -0,0 +1,7 @@ +from django.db import models + + +class ComponentParams(models.Model): + uuid = models.UUIDField(primary_key=True, editable=False, unique=True) + data = models.BinaryField(editable=False) + created_at = models.DateTimeField(auto_now_add=True, editable=False) diff --git a/src/django_idom/templates/idom/component.html b/src/django_idom/templates/idom/component.html index fc8ba6f8..91f83222 100644 --- a/src/django_idom/templates/idom/component.html +++ b/src/django_idom/templates/idom/component.html @@ -2,13 +2,12 @@
diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index b6bb6868..d09aa74a 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -1,13 +1,13 @@ -import json -from typing import Any -from urllib.parse import urlencode from uuid import uuid4 +import dill as pickle from django import template from django.urls import reverse -from django_idom.config import IDOM_WEBSOCKET_URL, IDOM_WS_MAX_RECONNECT_TIMEOUT -from django_idom.utils import _register_component +from django_idom import models +from django_idom.config import IDOM_MAX_RECONNECT_TIMEOUT, IDOM_WEBSOCKET_URL +from django_idom.types import ComponentParamData +from django_idom.utils import _register_component, func_has_params IDOM_WEB_MODULES_URL = reverse("idom:web_modules", args=["x"])[:-1][1:] @@ -15,11 +15,12 @@ @register.inclusion_tag("idom/component.html") -def component(dotted_path: str, **kwargs: Any): +def component(dotted_path: str, *args, **kwargs): """This tag is used to embed an existing IDOM component into your HTML template. Args: dotted_path: The dotted path to the component to render. + *args: The positional arguments to pass to the component. Keyword Args: **kwargs: The keyword arguments to pass to the component. @@ -34,17 +35,23 @@ def component(dotted_path: str, **kwargs: Any): """ - _register_component(dotted_path) - + component = _register_component(dotted_path) + uuid = uuid4().hex class_ = kwargs.pop("class", "") - json_kwargs = json.dumps(kwargs, separators=(",", ":")) + + # Store the component's args/kwargs in the database if needed + # This will be fetched by the websocket consumer later + if func_has_params(component): + params = ComponentParamData(args, kwargs) + model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params)) + model.full_clean() + model.save() return { "class": class_, "idom_websocket_url": IDOM_WEBSOCKET_URL, "idom_web_modules_url": IDOM_WEB_MODULES_URL, - "idom_ws_max_reconnect_timeout": IDOM_WS_MAX_RECONNECT_TIMEOUT, - "idom_mount_uuid": uuid4().hex, - "idom_component_id": dotted_path, - "idom_component_params": urlencode({"kwargs": json_kwargs}), + "idom_ws_max_reconnect_timeout": IDOM_MAX_RECONNECT_TIMEOUT, + "idom_mount_uuid": uuid, + "idom_component_path": f"{dotted_path}/{uuid}", } diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 7705c98b..ea1d92b3 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -35,7 +35,7 @@ class IdomWebsocket: scope: dict close: Callable[[Optional[int]], Awaitable[None]] disconnect: Callable[[int], Awaitable[None]] - view_id: str + dotted_path: str @dataclass @@ -89,3 +89,12 @@ class QueryOptions: postprocessor_kwargs: dict[str, Any] = field(default_factory=lambda: {}) """Keyworded arguments directly passed into the `postprocessor` for configuration.""" + + +@dataclass +class ComponentParamData: + """Container used for serializing component parameters. + This dataclass is pickled & stored in the database, then unpickled when needed.""" + + args: Sequence + kwargs: dict diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 9c76a9f9..75f5ef02 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -1,6 +1,7 @@ from __future__ import annotations import contextlib +import inspect import logging import os import re @@ -74,14 +75,15 @@ async def render_view( return response -def _register_component(dotted_path: str) -> None: +def _register_component(dotted_path: str) -> Callable: from django_idom.config import IDOM_REGISTERED_COMPONENTS if dotted_path in IDOM_REGISTERED_COMPONENTS: - return + return IDOM_REGISTERED_COMPONENTS[dotted_path] IDOM_REGISTERED_COMPONENTS[dotted_path] = _import_dotted_path(dotted_path) _logger.debug("IDOM has registered component %s", dotted_path) + return IDOM_REGISTERED_COMPONENTS[dotted_path] def _import_dotted_path(dotted_path: str) -> Callable: @@ -257,3 +259,7 @@ def django_query_postprocessor( ) return data + + +def func_has_params(func: Callable): + return str(inspect.signature(func)) != "()" diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index c4c78971..377839e7 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -1,10 +1,9 @@ """Anything used to construct a websocket endpoint""" import asyncio -import json import logging from typing import Any -from urllib.parse import parse_qsl +import dill as pickle from channels.auth import login from channels.db import database_sync_to_async as convert_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer @@ -13,7 +12,8 @@ from django_idom.config import IDOM_REGISTERED_COMPONENTS from django_idom.hooks import WebsocketContext -from django_idom.types import IdomWebsocket +from django_idom.types import ComponentParamData, IdomWebsocket +from django_idom.utils import func_has_params _logger = logging.getLogger(__name__) @@ -48,22 +48,36 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None: await self._idom_recv_queue.put(LayoutEvent(**content)) async def _run_dispatch_loop(self): - view_id = self.scope["url_route"]["kwargs"]["view_id"] + from django_idom import models + + dotted_path = self.scope["url_route"]["kwargs"]["dotted_path"] + uuid = self.scope["url_route"]["kwargs"]["uuid"] try: - component_constructor = IDOM_REGISTERED_COMPONENTS[view_id] + component_constructor = IDOM_REGISTERED_COMPONENTS[dotted_path] except KeyError: - _logger.warning(f"Unknown IDOM view ID {view_id!r}") + _logger.warning( + f"Attempt to access invalid IDOM component: {dotted_path!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 = IdomWebsocket(self.scope, self.close, self.disconnect, view_id) + socket = IdomWebsocket(self.scope, self.close, self.disconnect, dotted_path) try: - component_instance = component_constructor(**component_kwargs) + # Fetch the component's args/kwargs from the database, if needed + component_args = [] + component_kwargs = {} + if func_has_params(component_constructor): + params_query = await models.ComponentParams.objects.aget(uuid=uuid) + component_params: ComponentParamData = pickle.loads(params_query.data) + component_args = component_params.args + component_kwargs = component_params.kwargs + + # Generate the initial component instance + component_instance = component_constructor( + *component_args, **component_kwargs + ) except Exception: _logger.exception( f"Failed to construct component {component_constructor} " diff --git a/src/django_idom/websocket/paths.py b/src/django_idom/websocket/paths.py index f337c83e..fab68aee 100644 --- a/src/django_idom/websocket/paths.py +++ b/src/django_idom/websocket/paths.py @@ -6,7 +6,7 @@ IDOM_WEBSOCKET_PATH = path( - f"{IDOM_WEBSOCKET_URL}/", IdomAsyncWebsocketConsumer.as_asgi() + f"{IDOM_WEBSOCKET_URL}//", IdomAsyncWebsocketConsumer.as_asgi() ) """A URL path for :class:`IdomAsyncWebsocketConsumer`. diff --git a/src/js/src/index.js b/src/js/src/index.js index 8f17c0d4..c702d519 100644 --- a/src/js/src/index.js +++ b/src/js/src/index.js @@ -1,37 +1,34 @@ import { mountLayoutWithWebSocket } from "idom-client-react"; // Set up a websocket at the base endpoint -let LOCATION = window.location; -let WS_PROTOCOL = ""; +const LOCATION = window.location; +const WS_PROTOCOL = ""; if (LOCATION.protocol == "https:") { - WS_PROTOCOL = "wss://"; + WS_PROTOCOL = "wss://"; } else { - WS_PROTOCOL = "ws://"; + WS_PROTOCOL = "ws://"; } -let WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/"; +const WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/"; export function mountViewToElement( - mountPoint, - idomWebsocketUrl, - idomWebModulesUrl, - maxReconnectTimeout, - viewId, - queryParams + mountElement, + idomWebsocketUrl, + idomWebModulesUrl, + maxReconnectTimeout, + componentPath ) { - const fullWebsocketUrl = - WS_ENDPOINT_URL + idomWebsocketUrl + viewId + "/?" + queryParams; + const WS_URL = WS_ENDPOINT_URL + idomWebsocketUrl + componentPath; + const WEB_MODULE_URL = LOCATION.origin + "/" + idomWebModulesUrl; + const loadImportSource = (source, sourceType) => { + return import( + sourceType == "NAME" ? `${WEB_MODULE_URL}${source}` : source + ); + }; - const fullWebModulesUrl = LOCATION.origin + "/" + idomWebModulesUrl; - const loadImportSource = (source, sourceType) => { - return import( - sourceType == "NAME" ? `${fullWebModulesUrl}${source}` : source - ); - }; - - mountLayoutWithWebSocket( - mountPoint, - fullWebsocketUrl, - loadImportSource, - maxReconnectTimeout - ); + mountLayoutWithWebSocket( + mountElement, + WS_URL, + loadImportSource, + maxReconnectTimeout + ); } diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 29c3da30..94e8b45d 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -65,7 +65,7 @@ def simple_button(): @component def use_websocket(): ws = django_idom.hooks.use_websocket() - success = bool(ws.scope and ws.close and ws.disconnect and ws.view_id) + success = bool(ws.scope and ws.close and ws.disconnect and ws.dotted_path) return html.div( {"id": "use-websocket", "data-success": success}, f"use_websocket: {ws}", From 0b11cf7707a9d4e2f1192be56cc2db8080af7d5f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 9 Jan 2023 21:34:34 -0800 Subject: [PATCH 05/56] remove broken django stubs --- pyproject.toml | 5 ----- src/django_idom/models.py | 6 +++--- src/django_idom/templatetags/idom.py | 4 ++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 181cbf89..6bf3a9fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,8 +17,3 @@ warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true check_untyped_defs = true -plugins = ["mypy_django_plugin.main"] - -[tool.django-stubs] -django_settings_module = 'test_app.settings' - diff --git a/src/django_idom/models.py b/src/django_idom/models.py index 4e509255..65ffa1a6 100644 --- a/src/django_idom/models.py +++ b/src/django_idom/models.py @@ -2,6 +2,6 @@ class ComponentParams(models.Model): - uuid = models.UUIDField(primary_key=True, editable=False, unique=True) - data = models.BinaryField(editable=False) - created_at = models.DateTimeField(auto_now_add=True, editable=False) + uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore + data = models.BinaryField(editable=False) # type: ignore + created_at = models.DateTimeField(auto_now_add=True, editable=False) # type: ignore diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index d09aa74a..b5a06f0d 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -20,10 +20,10 @@ def component(dotted_path: str, *args, **kwargs): Args: dotted_path: The dotted path to the component to render. - *args: The positional arguments to pass to the component. + *args: The positional arguments to provide to the component. Keyword Args: - **kwargs: The keyword arguments to pass to the component. + **kwargs: The keyword arguments to provide to the component. Example :: From 587cfa3233280a6e4180bd22a7819ce87e5b215a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 9 Jan 2023 22:21:01 -0800 Subject: [PATCH 06/56] fix type hints --- src/django_idom/types.py | 2 +- src/django_idom/utils.py | 3 ++- src/django_idom/websocket/consumer.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index ea1d92b3..f087de01 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -96,5 +96,5 @@ class ComponentParamData: """Container used for serializing component parameters. This dataclass is pickled & stored in the database, then unpickled when needed.""" - args: Sequence + args: tuple kwargs: dict diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 75f5ef02..f2954939 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -261,5 +261,6 @@ def django_query_postprocessor( return data -def func_has_params(func: Callable): +def func_has_params(func: Callable) -> bool: + """Checks if a function has any args or kwarg parameters.""" return str(inspect.signature(func)) != "()" diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index 377839e7..14b1e95d 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -66,8 +66,8 @@ async def _run_dispatch_loop(self): try: # Fetch the component's args/kwargs from the database, if needed - component_args = [] - component_kwargs = {} + component_args: tuple[Any, ...] = tuple() + component_kwargs: dict = {} if func_has_params(component_constructor): params_query = await models.ComponentParams.objects.aget(uuid=uuid) component_params: ComponentParamData = pickle.loads(params_query.data) From e93253c9a4b4b2c564d0e994c7a8e7ccc538c678 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 9 Jan 2023 22:22:05 -0800 Subject: [PATCH 07/56] skip migrations --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6bf3a9fc..d30f839e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,9 @@ line_length = 88 lines_after_imports = 2 [tool.mypy] +exclude = [ + 'migrations/.*', +] ignore_missing_imports = true warn_unused_configs = true warn_redundant_casts = true From 8a84f1decf6a46d3c0bb5b4bfece865da376f183 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:20:44 -0800 Subject: [PATCH 08/56] bump idom version --- requirements/pkg-deps.txt | 2 +- src/js/package-lock.json | 1100 ++++++++++++++++++------------------- src/js/package.json | 42 +- 3 files changed, 572 insertions(+), 572 deletions(-) diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index 873b689f..441e7a04 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,5 +1,5 @@ channels >=4.0.0 -idom >=0.40.2, <0.41.0 +idom >=0.43.0, <0.44.0 aiofile >=3.0 dill >=0.3.5 typing_extensions diff --git a/src/js/package-lock.json b/src/js/package-lock.json index a11a055a..fc51d27a 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -1,552 +1,552 @@ { - "name": "js", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "idom-client-react": "^0.40.2", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "devDependencies": { - "prettier": "^2.2.1", - "rollup": "^2.56.3", - "rollup-plugin-commonjs": "^10.1.0", - "rollup-plugin-node-resolve": "^5.2.0", - "rollup-plugin-replace": "^2.2.0" - } - }, - "node_modules/@types/estree": { - "version": "0.0.48", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", - "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", - "dev": true - }, - "node_modules/@types/node": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", - "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", - "dev": true - }, - "node_modules/@types/resolve": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", - "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - }, - "node_modules/fast-json-patch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.0.tgz", - "integrity": "sha512-IhpytlsVTRndz0hU5t0/MGzS/etxLlfrpG5V5M9mVbuj9TrJLWaMfsox9REM5rkuGX0T+5qjpe8XA1o0gZ42nA==" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/htm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", - "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" - }, - "node_modules/idom-client-react": { - "version": "0.40.2", - "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.40.2.tgz", - "integrity": "sha512-7oTdN23DU5oeBfCjGjVovMF8vQMQiD1+89EkNgYmxJL/zQtz7HpY11fxARTIZXnB8XPvICuGEZwcPYsXkZGBFQ==", - "dependencies": { - "fast-json-patch": "^3.0.0-1", - "htm": "^3.0.3" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", - "dev": true - }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "dev": true, - "dependencies": { - "sourcemap-codec": "^1.4.4" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/prettier": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", - "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "dependencies": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rollup": { - "version": "2.56.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.3.tgz", - "integrity": "sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-commonjs": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", - "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1", - "is-reference": "^1.1.2", - "magic-string": "^0.25.2", - "resolve": "^1.11.0", - "rollup-pluginutils": "^2.8.1" - }, - "peerDependencies": { - "rollup": ">=1.12.0" - } - }, - "node_modules/rollup-plugin-node-resolve": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", - "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.", - "dev": true, - "dependencies": { - "@types/resolve": "0.0.8", - "builtin-modules": "^3.1.0", - "is-module": "^1.0.0", - "resolve": "^1.11.1", - "rollup-pluginutils": "^2.8.1" - }, - "peerDependencies": { - "rollup": ">=1.11.0" - } - }, - "node_modules/rollup-plugin-replace": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", - "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", - "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.", - "dev": true, - "dependencies": { - "magic-string": "^0.25.2", - "rollup-pluginutils": "^2.6.0" - } - }, - "node_modules/rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "dependencies": { - "estree-walker": "^0.6.1" - } - }, - "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - } - }, - "dependencies": { - "@types/estree": { - "version": "0.0.48", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", - "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", - "dev": true - }, - "@types/node": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", - "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", - "dev": true - }, - "@types/resolve": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", - "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "builtin-modules": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", - "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true - }, - "estree-walker": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", - "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", - "dev": true - }, - "fast-json-patch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.0.tgz", - "integrity": "sha512-IhpytlsVTRndz0hU5t0/MGzS/etxLlfrpG5V5M9mVbuj9TrJLWaMfsox9REM5rkuGX0T+5qjpe8XA1o0gZ42nA==" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "htm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", - "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" - }, - "idom-client-react": { - "version": "0.40.2", - "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.40.2.tgz", - "integrity": "sha512-7oTdN23DU5oeBfCjGjVovMF8vQMQiD1+89EkNgYmxJL/zQtz7HpY11fxARTIZXnB8XPvICuGEZwcPYsXkZGBFQ==", - "requires": { - "fast-json-patch": "^3.0.0-1", - "htm": "^3.0.3" - } - }, - "is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", - "dev": true - }, - "is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "requires": { - "@types/estree": "*" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "dev": true, - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "prettier": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", - "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", - "dev": true - }, - "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - } - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "rollup": { - "version": "2.56.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.3.tgz", - "integrity": "sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "rollup-plugin-commonjs": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", - "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", - "dev": true, - "requires": { - "estree-walker": "^0.6.1", - "is-reference": "^1.1.2", - "magic-string": "^0.25.2", - "resolve": "^1.11.0", - "rollup-pluginutils": "^2.8.1" - } - }, - "rollup-plugin-node-resolve": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", - "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", - "dev": true, - "requires": { - "@types/resolve": "0.0.8", - "builtin-modules": "^3.1.0", - "is-module": "^1.0.0", - "resolve": "^1.11.1", - "rollup-pluginutils": "^2.8.1" - } - }, - "rollup-plugin-replace": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", - "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", - "dev": true, - "requires": { - "magic-string": "^0.25.2", - "rollup-pluginutils": "^2.6.0" - } - }, - "rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", - "dev": true, - "requires": { - "estree-walker": "^0.6.1" - } - }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true - } - } + "name": "js", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "idom-client-react": "^0.43.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "prettier": "^2.2.1", + "rollup": "^2.56.3", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0" + } + }, + "node_modules/@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "node_modules/@types/node": { + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/htm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", + "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" + }, + "node_modules/idom-client-react": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.43.0.tgz", + "integrity": "sha512-SjVR7wmqsNO5ymKKOsLsQUA+cN12+KuG56xLkCDVyEnlDxUytIOzpzQ3qBsAeMbRJkT/BFURim7UeKnAgWgoLw==", + "dependencies": { + "fast-json-patch": "^3.1.1", + "htm": "^3.0.3" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/prettier": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.56.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.3.tgz", + "integrity": "sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.12.0" + } + }, + "node_modules/rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.", + "dev": true, + "dependencies": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.11.0" + } + }, + "node_modules/rollup-plugin-replace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", + "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", + "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.", + "dev": true, + "dependencies": { + "magic-string": "^0.25.2", + "rollup-pluginutils": "^2.6.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + } + }, + "dependencies": { + "@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "@types/node": { + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", + "dev": true + }, + "@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "htm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", + "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" + }, + "idom-client-react": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.43.0.tgz", + "integrity": "sha512-SjVR7wmqsNO5ymKKOsLsQUA+cN12+KuG56xLkCDVyEnlDxUytIOzpzQ3qBsAeMbRJkT/BFURim7UeKnAgWgoLw==", + "requires": { + "fast-json-patch": "^3.1.1", + "htm": "^3.0.3" + } + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "prettier": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", + "dev": true + }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.56.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.56.3.tgz", + "integrity": "sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "dev": true, + "requires": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-replace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", + "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", + "dev": true, + "requires": { + "magic-string": "^0.25.2", + "rollup-pluginutils": "^2.6.0" + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + } + }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + } + } } diff --git a/src/js/package.json b/src/js/package.json index 4d4112a2..10b43551 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,23 +1,23 @@ { - "description": "test app for idom_django websocket server", - "main": "src/index.js", - "files": [ - "src/**/*.js" - ], - "scripts": { - "build": "rollup --config", - "format": "prettier --ignore-path .gitignore --write ." - }, - "devDependencies": { - "prettier": "^2.2.1", - "rollup": "^2.56.3", - "rollup-plugin-commonjs": "^10.1.0", - "rollup-plugin-node-resolve": "^5.2.0", - "rollup-plugin-replace": "^2.2.0" - }, - "dependencies": { - "idom-client-react": "^0.40.2", - "react": "^17.0.2", - "react-dom": "^17.0.2" - } + "description": "test app for idom_django websocket server", + "main": "src/index.js", + "files": [ + "src/**/*.js" + ], + "scripts": { + "build": "rollup --config", + "format": "prettier --ignore-path .gitignore --write ." + }, + "devDependencies": { + "prettier": "^2.2.1", + "rollup": "^2.56.3", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0" + }, + "dependencies": { + "idom-client-react": "^0.43.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + } } From 3760dded11317d03bf5856747993ebceab829913 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 13:30:12 -0800 Subject: [PATCH 09/56] Use idom 0.43.0 hooks --- src/django_idom/__init__.py | 4 +-- src/django_idom/decorators.py | 7 ++--- src/django_idom/hooks.py | 39 +++---------------------- src/django_idom/types.py | 7 ++--- src/django_idom/websocket/consumer.py | 32 +++++++++++++------- tests/test_app/components.py | 23 ++++++++++----- tests/test_app/templates/base.html | 2 +- tests/test_app/tests/test_components.py | 4 +-- 8 files changed, 52 insertions(+), 66 deletions(-) diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index c1991661..aa912c6f 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,12 +1,12 @@ from django_idom import components, decorators, hooks, types, utils -from django_idom.types import IdomWebsocket +from django_idom.types import WebsocketConnection from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH __version__ = "2.2.1" __all__ = [ "IDOM_WEBSOCKET_PATH", - "IdomWebsocket", + "WebsocketConnection", "hooks", "components", "decorators", diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py index 6d01c990..7e6918da 100644 --- a/src/django_idom/decorators.py +++ b/src/django_idom/decorators.py @@ -3,10 +3,9 @@ from functools import wraps from typing import Callable +from idom.backend.hooks import use_scope from idom.core.types import ComponentType, VdomDict -from django_idom.hooks import use_websocket - def auth_required( component: Callable | None = None, @@ -27,9 +26,9 @@ def auth_required( def decorator(component): @wraps(component) def _wrapped_func(*args, **kwargs): - websocket = use_websocket() + scope = use_scope() - if getattr(websocket.scope["user"], auth_attribute): + if getattr(scope["user"], auth_attribute): return component(*args, **kwargs) return fallback(*args, **kwargs) if callable(fallback) else fallback diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index b40735c9..e7eac90e 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -15,17 +15,10 @@ from channels.db import database_sync_to_async as _database_sync_to_async from idom import use_callback, use_ref -from idom.backend.types import Location -from idom.core.hooks import Context, create_context, use_context, use_effect, use_state - -from django_idom.types import ( - IdomWebsocket, - Mutation, - Query, - QueryOptions, - _Params, - _Result, -) +from idom.backend.hooks import use_scope +from idom.core.hooks import use_effect, use_state + +from django_idom.types import Mutation, Query, QueryOptions, _Params, _Result from django_idom.utils import _generate_obj_name @@ -34,22 +27,11 @@ Callable[..., Callable[..., Awaitable[Any]]], _database_sync_to_async, ) -WebsocketContext: Context[IdomWebsocket | None] = create_context(None) _REFETCH_CALLBACKS: DefaultDict[ Callable[..., Any], set[Callable[[], None]] ] = DefaultDict(set) -def use_location() -> Location: - """Get the current route as a `Location` object""" - # TODO: Use the browser's current page, rather than the WS route - scope = use_scope() - search = scope["query_string"].decode() - return Location( - scope["path"], f"?{search}" if (search and (search != "undefined")) else "" - ) - - def use_origin() -> str | None: """Get the current origin as a string. If the browser did not send an origin header, this will be None.""" @@ -67,19 +49,6 @@ def use_origin() -> str | None: return None -def use_scope() -> dict[str, Any]: - """Get the current ASGI scope dictionary""" - return use_websocket().scope - - -def use_websocket() -> IdomWebsocket: - """Get the current IdomWebsocket object""" - websocket = use_context(WebsocketContext) - if websocket is None: - raise RuntimeError("No websocket. Are you running with a Django server?") - return websocket - - @overload def use_query( options: QueryOptions, diff --git a/src/django_idom/types.py b/src/django_idom/types.py index f087de01..5f98003a 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -21,7 +21,7 @@ from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR -__all__ = ["_Result", "_Params", "_Data", "IdomWebsocket", "Query", "Mutation"] +__all__ = ["_Result", "_Params", "_Data", "WebsocketConnection", "Query", "Mutation"] _Result = TypeVar("_Result", bound=Union[Model, QuerySet[Any]]) _Params = ParamSpec("_Params") @@ -29,10 +29,9 @@ @dataclass -class IdomWebsocket: - """Websocket returned by the `use_websocket` hook.""" +class WebsocketConnection: + """Carrier type for `idom.backends.hooks.use_connection().carrier`""" - scope: dict close: Callable[[Optional[int]], Awaitable[None]] disconnect: Callable[[int], Awaitable[None]] dotted_path: str diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index 14b1e95d..dbfcf9a7 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -7,12 +7,13 @@ 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.backend.hooks import ConnectionContext +from idom.backend.types import Connection, Location from idom.core.layout import Layout, LayoutEvent from idom.core.serve import serve_json_patch from django_idom.config import IDOM_REGISTERED_COMPONENTS -from django_idom.hooks import WebsocketContext -from django_idom.types import ComponentParamData, IdomWebsocket +from django_idom.types import ComponentParamData, WebsocketConnection from django_idom.utils import func_has_params @@ -50,10 +51,24 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None: async def _run_dispatch_loop(self): from django_idom import models - dotted_path = self.scope["url_route"]["kwargs"]["dotted_path"] - uuid = self.scope["url_route"]["kwargs"]["uuid"] + scope = self.scope + dotted_path = scope["url_route"]["kwargs"]["dotted_path"] + uuid = scope["url_route"]["kwargs"]["uuid"] + search = scope["query_string"].decode() + self._idom_recv_queue = recv_queue = asyncio.Queue() # type: ignore + connection = Connection( # Set up the `idom.backend.hooks` using context values + scope=scope, + location=Location( + pathname=scope["path"], + search=f"?{search}" if (search and (search != "undefined")) else "", + ), + carrier=WebsocketConnection(self.close, self.disconnect, dotted_path), + ) + component_args: tuple[Any, ...] = tuple() + component_kwargs: dict = {} try: + # Verify the component has already been registered component_constructor = IDOM_REGISTERED_COMPONENTS[dotted_path] except KeyError: _logger.warning( @@ -61,13 +76,8 @@ async def _run_dispatch_loop(self): ) return - # Provide developer access to parts of this websocket - socket = IdomWebsocket(self.scope, self.close, self.disconnect, dotted_path) - try: # Fetch the component's args/kwargs from the database, if needed - component_args: tuple[Any, ...] = tuple() - component_kwargs: dict = {} if func_has_params(component_constructor): params_query = await models.ComponentParams.objects.aget(uuid=uuid) component_params: ComponentParamData = pickle.loads(params_query.data) @@ -85,10 +95,10 @@ async def _run_dispatch_loop(self): ) return - self._idom_recv_queue = recv_queue = asyncio.Queue() # type: ignore try: + # Begin serving the IDOM component await serve_json_patch( - Layout(WebsocketContext(component_instance, value=socket)), + Layout(ConnectionContext(component_instance, value=connection)), self.send_json, recv_queue.get, ) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 94e8b45d..760d7f2d 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -4,6 +4,9 @@ from django.http import HttpRequest from django.shortcuts import render from idom import component, hooks, html, web +from idom.backend.hooks import use_connection as use_connection_hook +from idom.backend.hooks import use_location as use_location_hook +from idom.backend.hooks import use_scope as use_scope_hook from test_app.models import ForiegnChild, RelationalChild, RelationalParent, TodoItem import django_idom @@ -63,19 +66,25 @@ def simple_button(): @component -def use_websocket(): - ws = django_idom.hooks.use_websocket() - success = bool(ws.scope and ws.close and ws.disconnect and ws.dotted_path) +def use_connection(): + ws = use_connection_hook() + success = bool( + ws.scope + and ws.location + and ws.carrier.close + and ws.carrier.disconnect + and ws.carrier.dotted_path + ) return html.div( - {"id": "use-websocket", "data-success": success}, - f"use_websocket: {ws}", + {"id": "use-connection", "data-success": success}, + f"use_connection: {ws}", html.hr(), ) @component def use_scope(): - scope = django_idom.hooks.use_scope() + scope = use_scope_hook() success = len(scope) >= 10 and scope["type"] == "websocket" return html.div( {"id": "use-scope", "data-success": success}, @@ -86,7 +95,7 @@ def use_scope(): @component def use_location(): - location = django_idom.hooks.use_location() + location = use_location_hook() success = bool(location) return html.div( {"id": "use-location", "data-success": success}, diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index ca196297..7450682d 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -24,7 +24,7 @@

IDOM Test Page

{% component "test_app.components.button" class="button" %}
{% component "test_app.components.parameterized_component" class="parametarized-component" x=123 y=456 %}
{% component "test_app.components.simple_button" %}
-
{% component "test_app.components.use_websocket" %}
+
{% component "test_app.components.use_connection" %}
{% component "test_app.components.use_scope" %}
{% component "test_app.components.use_location" %}
{% component "test_app.components.use_origin" %}
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 5c435f22..cd566256 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -50,8 +50,8 @@ def test_parametrized_component(self): def test_component_from_web_module(self): self.page.wait_for_selector("#simple-button") - def test_use_websocket(self): - self.page.locator("#use-websocket[data-success=true]").wait_for() + def test_use_connection(self): + self.page.locator("#use-connection[data-success=true]").wait_for() def test_use_scope(self): self.page.locator("#use-scope[data-success=true]").wait_for() From e686894db9557ffbf8f30c0573b766f669f51c38 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 13:33:15 -0800 Subject: [PATCH 10/56] don't render component if args/kwargs are wrong --- src/django_idom/templatetags/idom.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index b5a06f0d..ecf551e1 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -46,6 +46,14 @@ def component(dotted_path: str, *args, **kwargs): model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params)) model.full_clean() model.save() + else: + kwargs.pop("key", "") + if args or kwargs: + raise ValueError( + f"Component {dotted_path!r} does not accept any arguments, but " + f"{f'{args} and ' if args else ''!r}" + f"{kwargs!r} were provided." + ) return { "class": class_, From 4b7676c6ff42fbb5cda1559d7a1b15441e47587a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 13:34:56 -0800 Subject: [PATCH 11/56] docs and changelog --- CHANGELOG.md | 12 ++++++++++++ README.md | 2 +- docs/src/features/decorators.md | 4 ---- docs/src/features/hooks.md | 8 ++++---- docs/src/features/settings.md | 2 +- docs/src/features/templatetag.md | 11 +++-------- docs/src/getting-started/reference-component.md | 2 -- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 460bfce7..e90743a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,18 @@ Using the following categories, list your changes in this order: ## [Unreleased] +### Changed + +- The `component` template tag now supports both positional and keyword arguments. +- `use_location`, `use_scope`, and `use_websocket` previously contained within `django_idom.hooks` have been migrated to `idom.backend.hooks`. +- Bumped the minimum IDOM version to 0.43.0 + +### Removed + +- `django_idom.hooks.use_location` has been removed. The equivalent replacement is found at `idom.backends.hooks.use_location`. +- `django_idom.hooks.use_scope` has been removed. The equivalent replacement is found at `idom.backends.hooks.use_scope`. +- `django_idom.hooks.use_websocket` has been removed. The equivalent replacement is found at `idom.backends.hooks.use_connection`. + ### Security - Fixed a potential method of component argument spoofing diff --git a/README.md b/README.md index f45f1242..11e19477 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ def hello_world(recipient: str): In your **Django app**'s HTML template, you can now embed your IDOM component using the `component` template tag. Within this tag, you will need to type in your dotted path to the component function as the first argument. -Additionally, you can pass in keyword arguments into your component function. For example, after reading the code below, pay attention to how the function definition for `hello_world` (_in the previous example_) accepts a `recipient` argument. +Additionally, you can pass in `args` and `kwargs` into your component function. For example, after reading the code below, pay attention to how the function definition for `hello_world` (_in the previous example_) accepts a `recipient` argument. diff --git a/docs/src/features/decorators.md b/docs/src/features/decorators.md index e643ec77..c4b60d9f 100644 --- a/docs/src/features/decorators.md +++ b/docs/src/features/decorators.md @@ -16,7 +16,6 @@ This decorator can be used with or without parentheses. ```python from django_idom.decorators import auth_required - from django_idom.hooks import use_websocket from idom import component, html @component @@ -70,7 +69,6 @@ This decorator can be used with or without parentheses. ```python from django_idom.decorators import auth_required - from django_idom.hooks import use_websocket from idom import component, html @component @@ -87,7 +85,6 @@ This decorator can be used with or without parentheses. ```python from django_idom.decorators import auth_required - from django_idom.hooks import use_websocket from idom import component, html @@ -120,7 +117,6 @@ This decorator can be used with or without parentheses. ```python from django_idom.decorators import auth_required - from django_idom.hooks import use_websocket from idom import component, html @component diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index 07040d0b..9525523d 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -399,19 +399,19 @@ The function you provide into this hook will have no return value. {% include-markdown "../../includes/orm.md" start="" end="" %} -## Use Websocket +## Use Connection -You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_websocket`. +You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_connection`. === "components.py" ```python from idom import component, html - from django_idom.hooks import use_websocket + from django_idom.hooks import use_connection @component def my_component(): - my_websocket = use_websocket() + my_websocket = use_connection() return html.div(my_websocket) ``` diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md index 4eb83fe1..af53d3de 100644 --- a/docs/src/features/settings.md +++ b/docs/src/features/settings.md @@ -16,7 +16,7 @@ # Maximum seconds between two reconnection attempts that would cause the client give up. # 0 will disable reconnection. - IDOM_WS_MAX_RECONNECT_TIMEOUT = 604800 + IDOM_MAX_RECONNECT_TIMEOUT = 259200 # The URL for IDOM to serve websockets IDOM_WEBSOCKET_URL = "idom/" diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md index 6aa58023..bdaf8074 100644 --- a/docs/src/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -44,7 +44,7 @@ For this template tag, there are two reserved keyword arguments: `class` and `key` - `class` allows you to apply a HTML class to the top-level component div. This is useful for styling purposes. - - `key` allows you to force the component to use a [specific key value](https://idom-docs.herokuapp.com/docs/guides/understanding-idom/why-idom-needs-keys.html?highlight=key). You typically won't need to set this. + - `key` allows you to force the component to use a [specific key value](https://idom-docs.herokuapp.com/docs/guides/understanding-idom/why-idom-needs-keys.html?highlight=key). Using `key` within a template tag is effectively useless. === "my-template.html" @@ -63,7 +63,8 @@ | Name | Type | Description | Default | | --- | --- | --- | --- | | `dotted_path` | `str` | The dotted path to the component to render. | N/A | - | `**kwargs` | `Any` | The keyword arguments to pass to the component. | N/A | + | `*args` | `Any` | The positional arguments to provide to the component. | N/A | + | `**kwargs` | `Any` | The keyword arguments to provide to the component. | N/A | **Returns** @@ -96,13 +97,7 @@ Additionally, the components in the example above will not be able to interact with each other, except through database queries. - -??? question "Can I use positional arguments instead of keyword arguments?" - - You can only pass in **keyword arguments** within the template tag. Due to technical limitations, **positional arguments** are not supported at this time. - - ??? question "What is a "template tag"?" diff --git a/docs/src/getting-started/reference-component.md b/docs/src/getting-started/reference-component.md index f7c03ecb..d341af11 100644 --- a/docs/src/getting-started/reference-component.md +++ b/docs/src/getting-started/reference-component.md @@ -16,8 +16,6 @@ {% include-markdown "../features/templatetag.md" start="" end="" %} -{% include-markdown "../features/templatetag.md" start="" end="" %} - {% include-markdown "../features/templatetag.md" start="" end="" %} ??? question "Where is my templates folder?" From 0f5770266abb3d1a2598c7bdfb1cde81e1c84be3 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 14:59:26 -0800 Subject: [PATCH 12/56] Add object in templatetag tests --- src/django_idom/templatetags/idom.py | 2 +- src/js/src/index.js | 2 +- tests/test_app/components.py | 27 +++++++++++++++++++++++-- tests/test_app/templates/base.html | 1 + tests/test_app/tests/test_components.py | 3 +++ tests/test_app/types.py | 6 ++++++ tests/test_app/views.py | 4 +++- 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 tests/test_app/types.py diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index ecf551e1..d14270f3 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -61,5 +61,5 @@ def component(dotted_path: str, *args, **kwargs): "idom_web_modules_url": IDOM_WEB_MODULES_URL, "idom_ws_max_reconnect_timeout": IDOM_MAX_RECONNECT_TIMEOUT, "idom_mount_uuid": uuid, - "idom_component_path": f"{dotted_path}/{uuid}", + "idom_component_path": f"{dotted_path}/{uuid}/", } diff --git a/src/js/src/index.js b/src/js/src/index.js index c702d519..478bf8ca 100644 --- a/src/js/src/index.js +++ b/src/js/src/index.js @@ -2,7 +2,7 @@ import { mountLayoutWithWebSocket } from "idom-client-react"; // Set up a websocket at the base endpoint const LOCATION = window.location; -const WS_PROTOCOL = ""; +let WS_PROTOCOL = ""; if (LOCATION.protocol == "https:") { WS_PROTOCOL = "wss://"; } else { diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 760d7f2d..11939d8e 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -14,11 +14,12 @@ from django_idom.hooks import use_mutation, use_query from . import views +from .types import TestObject @component def hello_world(): - return html._(html.h1({"id": "hello-world"}, "Hello World!"), html.hr()) + return html._(html.div({"id": "hello-world"}, "Hello World!"), html.hr()) @component @@ -26,6 +27,7 @@ def button(): count, set_count = hooks.use_state(0) return html._( html.div( + "button:", html.button( {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)}, "Click me!", @@ -43,7 +45,27 @@ def button(): def parameterized_component(x, y): total = x + y return html._( - html.h1({"id": "parametrized-component", "data-value": total}, total), + html.div( + {"id": "parametrized-component", "data-value": total}, + f"parameterized_component: {total}", + ), + html.hr(), + ) + + +@component +def object_in_templatetag(my_object: TestObject): + success = bool(my_object and my_object.value) + co_name = inspect.currentframe().f_code.co_name # type: ignore + return html._( + html.div( + { + "id": co_name, + "data-success": success, + }, + f"{co_name}: ", + str(my_object), + ), html.hr(), ) @@ -60,6 +82,7 @@ def parameterized_component(x, y): @component def simple_button(): return html._( + "simple_button:", SimpleButton({"id": "simple-button"}), html.hr(), ) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 7450682d..8b7344b2 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -23,6 +23,7 @@

IDOM Test Page

{% component "test_app.components.hello_world" class="hello-world" %}
{% component "test_app.components.button" class="button" %}
{% component "test_app.components.parameterized_component" class="parametarized-component" x=123 y=456 %}
+
{% component "test_app.components.object_in_templatetag" my_object %}
{% component "test_app.components.simple_button" %}
{% component "test_app.components.use_connection" %}
{% component "test_app.components.use_scope" %}
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index cd566256..00299684 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -47,6 +47,9 @@ def test_counter(self): def test_parametrized_component(self): self.page.locator("#parametrized-component[data-value='579']").wait_for() + def test_object_in_templatetag(self): + self.page.locator("#object_in_templatetag[data-success=true]").wait_for() + def test_component_from_web_module(self): self.page.wait_for_selector("#simple-button") diff --git a/tests/test_app/types.py b/tests/test_app/types.py new file mode 100644 index 00000000..438c69d0 --- /dev/null +++ b/tests/test_app/types.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + + +@dataclass +class TestObject: + value: int = 0 diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 2d16ae97..b172132a 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -4,9 +4,11 @@ from django.shortcuts import render from django.views.generic import TemplateView, View +from .types import TestObject + def base_template(request): - return render(request, "base.html", {}) + return render(request, "base.html", {"my_object": TestObject(1)}) def view_to_component_sync_func(request): From 943234976a4e3d05244956f2a767fcc1a5ab8d8e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:26:48 -0800 Subject: [PATCH 13/56] fix styling errors --- pyproject.toml | 1 + src/django_idom/websocket/consumer.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d30f839e..f90ffb77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ ensure_newline_before_comments = "True" include_trailing_comma = "True" line_length = 88 lines_after_imports = 2 +skip = ['src/django_idom/migrations/', 'tests/test_app/migrations/', '.nox/', '.venv/', 'build/'] [tool.mypy] exclude = [ diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index dbfcf9a7..f8e88198 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -1,7 +1,7 @@ """Anything used to construct a websocket endpoint""" import asyncio import logging -from typing import Any +from typing import Any, Tuple import dill as pickle from channels.auth import login @@ -64,7 +64,7 @@ async def _run_dispatch_loop(self): ), carrier=WebsocketConnection(self.close, self.disconnect, dotted_path), ) - component_args: tuple[Any, ...] = tuple() + component_args: Tuple[Any, ...] = tuple() component_kwargs: dict = {} try: From a87a7b5a59cdeea6479c125879f546c44826eee0 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:48:35 -0800 Subject: [PATCH 14/56] type hints for user --- src/django_idom/websocket/consumer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index f8e88198..f5cbd411 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -24,9 +24,11 @@ class IdomAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer): """Communicates with the browser to perform actions on-demand.""" async def connect(self) -> None: + from django.contrib.auth.models import AbstractBaseUser + await super().connect() - user = self.scope.get("user") + user: AbstractBaseUser = self.scope.get("user") if user and user.is_authenticated: try: await login(self.scope, user) From 4f4197513878cc8c8672d07579a8e71e6266628a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:58:44 -0800 Subject: [PATCH 15/56] validate function signature --- src/django_idom/templatetags/idom.py | 24 +++++++++++------------- src/django_idom/utils.py | 17 ++++++++++++++--- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index d14270f3..384ea98a 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -38,22 +38,20 @@ def component(dotted_path: str, *args, **kwargs): component = _register_component(dotted_path) uuid = uuid4().hex class_ = kwargs.pop("class", "") + kwargs.pop("key", "") # `key` is effectively useless for the root node # Store the component's args/kwargs in the database if needed # This will be fetched by the websocket consumer later - if func_has_params(component): - params = ComponentParamData(args, kwargs) - model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params)) - model.full_clean() - model.save() - else: - kwargs.pop("key", "") - if args or kwargs: - raise ValueError( - f"Component {dotted_path!r} does not accept any arguments, but " - f"{f'{args} and ' if args else ''!r}" - f"{kwargs!r} were provided." - ) + try: + if func_has_params(component, *args, **kwargs): + params = ComponentParamData(args, kwargs) + model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params)) + model.full_clean() + model.save() + except TypeError as e: + raise TypeError( + f"The provided parameters are incompatible with component '{dotted_path}'." + ) from e return { "class": class_, diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index f2954939..ddf49ec9 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -261,6 +261,17 @@ def django_query_postprocessor( return data -def func_has_params(func: Callable) -> bool: - """Checks if a function has any args or kwarg parameters.""" - return str(inspect.signature(func)) != "()" +def func_has_params(func: Callable, *args, **kwargs) -> bool: + """Checks if a function has any args or kwarg parameters. + + Can optionally validate whether a set of args/kwargs would work on the given function. + """ + signature = inspect.signature(func) + + # Check if the function has any args/kwargs + if not args and not kwargs: + return str(signature) != "()" + + # Check if the function has the given args/kwargs + signature.bind(*args, **kwargs) + return True From ec4646956e775b9e324f9f36c72ab1fd7d381e08 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:39:57 -0800 Subject: [PATCH 16/56] use lowercase tuple --- src/django_idom/websocket/consumer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index f5cbd411..8933db7a 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -1,7 +1,9 @@ """Anything used to construct a websocket endpoint""" +from __future__ import annotations + import asyncio import logging -from typing import Any, Tuple +from typing import Any import dill as pickle from channels.auth import login @@ -66,7 +68,7 @@ async def _run_dispatch_loop(self): ), carrier=WebsocketConnection(self.close, self.disconnect, dotted_path), ) - component_args: Tuple[Any, ...] = tuple() + component_args: tuple[Any, ...] = tuple() component_kwargs: dict = {} try: From 5e754885a26f1022b1662de6582b3849d74e7261 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 18:39:35 -0800 Subject: [PATCH 17/56] functioning eviction strategy --- CHANGELOG.md | 2 + docs/src/features/settings.md | 4 +- src/django_idom/apps.py | 5 ++- src/django_idom/config.py | 4 +- src/django_idom/http/views.py | 4 +- ...reated_at_componentparams_last_accessed.py | 18 ++++++++ src/django_idom/models.py | 2 +- src/django_idom/templates/idom/component.html | 2 +- src/django_idom/templatetags/idom.py | 4 +- src/django_idom/utils.py | 43 +++++++++++++++++++ src/django_idom/websocket/consumer.py | 18 +++++++- tests/test_app/admin.py | 7 +++ tests/test_app/settings.py | 8 ++++ 13 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 src/django_idom/migrations/0002_rename_created_at_componentparams_last_accessed.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e90743a9..06f9f445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ Using the following categories, list your changes in this order: - The `component` template tag now supports both positional and keyword arguments. - `use_location`, `use_scope`, and `use_websocket` previously contained within `django_idom.hooks` have been migrated to `idom.backend.hooks`. - Bumped the minimum IDOM version to 0.43.0 +- `IDOM_WS_MAX_RECONNECT_TIMEOUT` has been renamed to `IDOM_RECONNECT_MAX` +- It is now mandatory to run `manage.py migrate` after installing IDOM ### Removed diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md index af53d3de..a3060f15 100644 --- a/docs/src/features/settings.md +++ b/docs/src/features/settings.md @@ -14,9 +14,9 @@ "idom": {"BACKEND": ...}, } - # Maximum seconds between two reconnection attempts that would cause the client give up. + # Maximum seconds between a reconnection attempt before giving up. # 0 will disable reconnection. - IDOM_MAX_RECONNECT_TIMEOUT = 259200 + IDOM_RECONNECT_MAX = 259200 # The URL for IDOM to serve websockets IDOM_WEBSOCKET_URL = "idom/" diff --git a/src/django_idom/apps.py b/src/django_idom/apps.py index 6a963664..b7a60dc7 100644 --- a/src/django_idom/apps.py +++ b/src/django_idom/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -from django_idom.utils import ComponentPreloader +from django_idom.utils import ComponentPreloader, db_cleanup class DjangoIdomConfig(AppConfig): @@ -9,3 +9,6 @@ class DjangoIdomConfig(AppConfig): def ready(self): # Populate the IDOM component registry when Django is ready ComponentPreloader().register_all() + + # Delete expired database entries + db_cleanup(immediate=True) diff --git a/src/django_idom/config.py b/src/django_idom/config.py index 7cd8a6db..1d7b3075 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -17,9 +17,9 @@ "IDOM_WEBSOCKET_URL", "idom/", ) -IDOM_MAX_RECONNECT_TIMEOUT = getattr( +IDOM_RECONNECT_MAX = getattr( settings, - "IDOM_MAX_RECONNECT_TIMEOUT", + "IDOM_RECONNECT_MAX", 259200, # Default to 3 days ) IDOM_CACHE: BaseCache = ( diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 57233bab..d365fe64 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -6,7 +6,7 @@ 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 +from django_idom.utils import _create_cache_key, render_view async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: @@ -23,7 +23,7 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: # Fetch the file from cache, if available last_modified_time = os.stat(path).st_mtime - cache_key = f"django_idom:web_module:{str(path).lstrip(str(web_modules_dir))}" + cache_key = _create_cache_key("web_module", str(path).lstrip(str(web_modules_dir))) response = await IDOM_CACHE.aget(cache_key, version=int(last_modified_time)) if response is None: async with async_open(path, "r") as fp: diff --git a/src/django_idom/migrations/0002_rename_created_at_componentparams_last_accessed.py b/src/django_idom/migrations/0002_rename_created_at_componentparams_last_accessed.py new file mode 100644 index 00000000..80207bce --- /dev/null +++ b/src/django_idom/migrations/0002_rename_created_at_componentparams_last_accessed.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.5 on 2023-01-11 01:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("django_idom", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="componentparams", + old_name="created_at", + new_name="last_accessed", + ), + ] diff --git a/src/django_idom/models.py b/src/django_idom/models.py index 65ffa1a6..1e67f368 100644 --- a/src/django_idom/models.py +++ b/src/django_idom/models.py @@ -4,4 +4,4 @@ class ComponentParams(models.Model): uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore data = models.BinaryField(editable=False) # type: ignore - created_at = models.DateTimeField(auto_now_add=True, editable=False) # type: ignore + last_accessed = models.DateTimeField(auto_now_add=True) # type: ignore diff --git a/src/django_idom/templates/idom/component.html b/src/django_idom/templates/idom/component.html index 91f83222..7fd82c22 100644 --- a/src/django_idom/templates/idom/component.html +++ b/src/django_idom/templates/idom/component.html @@ -7,7 +7,7 @@ mountElement, "{{ idom_websocket_url }}", "{{ idom_web_modules_url }}", - "{{ idom_ws_max_reconnect_timeout }}", + "{{ idom_reconnect_max }}", "{{ idom_component_path }}", ); diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index 384ea98a..4309177b 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -5,7 +5,7 @@ from django.urls import reverse from django_idom import models -from django_idom.config import IDOM_MAX_RECONNECT_TIMEOUT, IDOM_WEBSOCKET_URL +from django_idom.config import IDOM_RECONNECT_MAX, IDOM_WEBSOCKET_URL from django_idom.types import ComponentParamData from django_idom.utils import _register_component, func_has_params @@ -57,7 +57,7 @@ def component(dotted_path: str, *args, **kwargs): "class": class_, "idom_websocket_url": IDOM_WEBSOCKET_URL, "idom_web_modules_url": IDOM_WEB_MODULES_URL, - "idom_ws_max_reconnect_timeout": IDOM_MAX_RECONNECT_TIMEOUT, + "idom_reconnect_max": IDOM_RECONNECT_MAX, "idom_mount_uuid": uuid, "idom_component_path": f"{dotted_path}/{uuid}/", } diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index ddf49ec9..670d79e0 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -5,6 +5,7 @@ import logging import os import re +from datetime import datetime, timedelta from fnmatch import fnmatch from importlib import import_module from inspect import iscoroutinefunction @@ -17,6 +18,7 @@ from django.db.models.query import QuerySet from django.http import HttpRequest, HttpResponse from django.template import engines +from django.utils import timezone from django.utils.encoding import smart_str from django.views import View @@ -275,3 +277,44 @@ def func_has_params(func: Callable, *args, **kwargs) -> bool: # Check if the function has the given args/kwargs signature.bind(*args, **kwargs) return True + + +def _create_cache_key(*args): + """Creates a cache key string that starts with `django_idom` contains + all *args separated by `:`.""" + + if not args: + raise ValueError("At least one argument is required to create a cache key.") + + return f"django_idom:{':'.join(str(arg) for arg in args)}" + + +def db_cleanup(immediate: bool = False): + """Deletes expired component parameters from the database. + This function may be expanded in the future to include additional cleanup tasks.""" + from .config import IDOM_CACHE, IDOM_RECONNECT_MAX + from .models import ComponentParams + + date_format: str = "%Y-%m-%d %H:%M:%S.%f+%Z" + cache_key: str = _create_cache_key("last_cleaned") + now_str: str = datetime.strftime(timezone.now(), date_format) + last_cleaned_str: str = IDOM_CACHE.get(cache_key) + + # Calculate the expiration time using Django timezones + last_cleaned: datetime = timezone.make_aware( + datetime.strptime(last_cleaned_str or now_str, date_format) + ) + expiration: datetime = last_cleaned + timedelta(seconds=IDOM_RECONNECT_MAX) + + # Component params exist in the DB, but we don't know when they were last cleaned + if not last_cleaned_str and ComponentParams.objects.all(): + _logger.warning( + "IDOM has detected component sessions in the database, " + "but no timestamp was found in cache. This may indicate that " + "the cache has been cleared." + ) + + # Delete expired component parameters + if immediate or not last_cleaned_str or timezone.now() >= expiration: + ComponentParams.objects.filter(last_accessed__gte=expiration).delete() + IDOM_CACHE.set(cache_key, now_str) diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index 8933db7a..8d6d4fbd 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -9,6 +9,7 @@ from channels.auth import login from channels.db import database_sync_to_async as convert_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer +from django.utils import timezone from idom.backend.hooks import ConnectionContext from idom.backend.types import Connection, Location from idom.core.layout import Layout, LayoutEvent @@ -16,7 +17,7 @@ from django_idom.config import IDOM_REGISTERED_COMPONENTS from django_idom.types import ComponentParamData, WebsocketConnection -from django_idom.utils import func_has_params +from django_idom.utils import db_cleanup, func_has_params _logger = logging.getLogger(__name__) @@ -83,7 +84,20 @@ async def _run_dispatch_loop(self): try: # Fetch the component's args/kwargs from the database, if needed if func_has_params(component_constructor): - params_query = await models.ComponentParams.objects.aget(uuid=uuid) + try: + # Always clean up expired entries first + await convert_to_async(db_cleanup)() + + # Get the queries from a DB + params_query = await models.ComponentParams.objects.aget(uuid=uuid) + params_query.last_accessed = timezone.now() + await convert_to_async(params_query.save)() + except models.ComponentParams.DoesNotExist: + _logger.warning( + f"Browser has attempted to access '{dotted_path}', " + f"but the component has already expired beyond IDOM_RECONNECT_MAX." + ) + return component_params: ComponentParamData = pickle.loads(params_query.data) component_args = component_params.args component_kwargs = component_params.kwargs diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py index 5e35a515..e0701a96 100644 --- a/tests/test_app/admin.py +++ b/tests/test_app/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin from test_app.models import ForiegnChild, RelationalChild, RelationalParent, TodoItem +from django_idom.models import ComponentParams + @admin.register(TodoItem) class TodoItemAdmin(admin.ModelAdmin): @@ -20,3 +22,8 @@ class RelationalParentAdmin(admin.ModelAdmin): @admin.register(ForiegnChild) class ForiegnChildAdmin(admin.ModelAdmin): pass + + +@admin.register(ComponentParams) +class ComponentParamsAdmin(admin.ModelAdmin): + list_display = ("uuid", "last_accessed") diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index 6cb4dc82..bbc4be6a 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -80,6 +80,14 @@ }, } +# Cache +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": os.path.join(BASE_DIR, "cache"), + } +} + # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ From 7344a9793045e51c814919a8c47ad67b269d3d1e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 18:55:02 -0800 Subject: [PATCH 18/56] don't clean DB on startup if running django tests --- src/django_idom/apps.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/django_idom/apps.py b/src/django_idom/apps.py index b7a60dc7..6d2cda8a 100644 --- a/src/django_idom/apps.py +++ b/src/django_idom/apps.py @@ -1,3 +1,5 @@ +import sys + from django.apps import AppConfig from django_idom.utils import ComponentPreloader, db_cleanup @@ -11,4 +13,5 @@ def ready(self): ComponentPreloader().register_all() # Delete expired database entries - db_cleanup(immediate=True) + if "test" not in sys.argv: + db_cleanup(immediate=True) From d2d457365aa33b22183ff1e5fccb0711f95fb3ce Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 23:05:36 -0800 Subject: [PATCH 19/56] switch sys check to contextlib suppress --- src/django_idom/apps.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/django_idom/apps.py b/src/django_idom/apps.py index 6d2cda8a..7de4c4fe 100644 --- a/src/django_idom/apps.py +++ b/src/django_idom/apps.py @@ -1,6 +1,7 @@ -import sys +import contextlib from django.apps import AppConfig +from django.db.utils import OperationalError from django_idom.utils import ComponentPreloader, db_cleanup @@ -13,5 +14,8 @@ def ready(self): ComponentPreloader().register_all() # Delete expired database entries - if "test" not in sys.argv: + # Suppress exceptions to avoid issues with `manage.py` commands such as + # `test`, `migrate`, `makemigrations`, or any custom user created commands + # where the database may not be preconfigured. + with contextlib.suppress(OperationalError): db_cleanup(immediate=True) From ace4a34b90d0a3b995dc797b646d0bb823028161 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 10 Jan 2023 23:31:25 -0800 Subject: [PATCH 20/56] view_to_component uses del_html_head_body_transform --- src/django_idom/components.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 163eb47b..a17c0037 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -62,6 +62,7 @@ async def async_render(): set_converted_view( utils.html_to_vdom( response.content.decode("utf-8").strip(), + utils.del_html_head_body_transform, *transforms, strict=strict_parsing, ) From 8cf28848bd53f637bd1589a5a2d3ad6018f3c929 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 00:08:41 -0800 Subject: [PATCH 21/56] update docs --- CHANGELOG.md | 14 +++-- docs/src/features/hooks.md | 66 ++++++++++++------------ docs/src/getting-started/installation.md | 10 +++- src/django_idom/types.py | 2 +- 4 files changed, 53 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06f9f445..fff2e4be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,16 +25,22 @@ Using the following categories, list your changes in this order: ### Changed - The `component` template tag now supports both positional and keyword arguments. +- The `component` template tag now supports non-serializable arguments. - `use_location`, `use_scope`, and `use_websocket` previously contained within `django_idom.hooks` have been migrated to `idom.backend.hooks`. - Bumped the minimum IDOM version to 0.43.0 -- `IDOM_WS_MAX_RECONNECT_TIMEOUT` has been renamed to `IDOM_RECONNECT_MAX` +- `IDOM_WS_MAX_RECONNECT_TIMEOUT` setting has been renamed to `IDOM_RECONNECT_MAX` +- `django_idom.types.IdomWebsocket` has been renamed to `WebsocketConnection` - It is now mandatory to run `manage.py migrate` after installing IDOM ### Removed -- `django_idom.hooks.use_location` has been removed. The equivalent replacement is found at `idom.backends.hooks.use_location`. -- `django_idom.hooks.use_scope` has been removed. The equivalent replacement is found at `idom.backends.hooks.use_scope`. -- `django_idom.hooks.use_websocket` has been removed. The equivalent replacement is found at `idom.backends.hooks.use_connection`. +- `django_idom.hooks.use_location` has been removed. The equivalent replacement is found at `idom.backend.hooks.use_location`. +- `django_idom.hooks.use_scope` has been removed. The equivalent replacement is found at `idom.backend.hooks.use_scope`. +- `django_idom.hooks.use_websocket` has been removed. The equivalent replacement is found at `idom.backend.hooks.use_connection`. + +### Fixed + +- `view_to_component` will now retain nodes (such as CSS and JavaScript) that were defined in a page's HTML `` ### Security diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index 9525523d..c8bb6c06 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -399,20 +399,22 @@ The function you provide into this hook will have no return value. {% include-markdown "../../includes/orm.md" start="" end="" %} -## Use Connection +## Use Origin -You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_connection`. +This is a shortcut that returns the Websocket's `origin`. + +You can expect this hook to provide strings such as `http://example.com`. === "components.py" ```python from idom import component, html - from django_idom.hooks import use_connection + from django_idom.hooks import use_origin @component def my_component(): - my_websocket = use_connection() - return html.div(my_websocket) + my_origin = use_origin() + return html.div(my_origin) ``` ??? example "See Interface" @@ -425,22 +427,22 @@ You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en | Type | Description | | --- | --- | - | `IdomWebsocket` | The component's websocket. | + | `str | None` | A string containing the browser's current origin, obtained from websocket headers (if available). | -## Use Scope +## Use Connection -This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). +You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_connection`. === "components.py" ```python from idom import component, html - from django_idom.hooks import use_scope + from idom.backend.hooks import use_connection @component def my_component(): - my_scope = use_scope() - return html.div(my_scope) + my_websocket = use_connection() + return html.div(my_websocket) ``` ??? example "See Interface" @@ -453,24 +455,22 @@ This is a shortcut that returns the Websocket's [`scope`](https://channels.readt | Type | Description | | --- | --- | - | `dict[str, Any]` | The websocket's `scope`. | - -## Use Location + | `WebsocketConnection` | The component's websocket. | -This is a shortcut that returns the Websocket's `path`. +## Use Scope -You can expect this hook to provide strings such as `/idom/my_path`. +This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). === "components.py" ```python from idom import component, html - from django_idom.hooks import use_location + from idom.backend.hooks import use_scope @component def my_component(): - my_location = use_location() - return html.div(my_location) + my_scope = use_scope() + return html.div(my_scope) ``` ??? example "See Interface" @@ -483,30 +483,24 @@ You can expect this hook to provide strings such as `/idom/my_path`. | Type | Description | | --- | --- | - | `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. + | `dict[str, Any]` | The websocket's `scope`. | -## Use Origin +## Use Location -This is a shortcut that returns the Websocket's `origin`. +This is a shortcut that returns the Websocket's `path`. -You can expect this hook to provide strings such as `http://example.com`. +You can expect this hook to provide strings such as `/idom/my_path`. === "components.py" ```python from idom import component, html - from django_idom.hooks import use_origin + from idom.backend.hooks import use_location @component def my_component(): - my_origin = use_origin() - return html.div(my_origin) + my_location = use_location() + return html.div(my_location) ``` ??? example "See Interface" @@ -519,4 +513,10 @@ You can expect this hook to provide strings such as `http://example.com`. | Type | Description | | --- | --- | - | `str | None` | A string containing the browser's current origin, obtained from websocket headers (if available). | + | `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. diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 1224df32..140e0c3f 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -2,7 +2,7 @@ Django-IDOM can be installed from PyPI to an existing **Django project** with minimal configuration. -## Step 0: Set up a Django Project +## Step 0: Create a Django Project These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which involves creating and installing at least one **Django app**. If not, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_. @@ -96,3 +96,11 @@ Register IDOM's Websocket using `IDOM_WEBSOCKET_PATH`. ??? question "Where is my `asgi.py`?" If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html). + +## Step 5: Run Migrations + +Run Django's database migrations to initialize Django-IDOM's database table. + +```bash +python manage.py migrate +``` diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 5f98003a..c2d16d6b 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -30,7 +30,7 @@ @dataclass class WebsocketConnection: - """Carrier type for `idom.backends.hooks.use_connection().carrier`""" + """Carrier type for `idom.backend.hooks.use_connection().carrier`""" close: Callable[[Optional[int]], Awaitable[None]] disconnect: Callable[[int], Awaitable[None]] From d985e149b354e4fb6e2d359a9b84f27ccbc8544b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 00:13:08 -0800 Subject: [PATCH 22/56] add serializable to dictionary --- docs/src/dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 7892a120..64d6903a 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -25,3 +25,4 @@ idom asgi postfixed postprocessing +serializable From 3011e7541991181db10ddb09d8f36e5f87f91cc7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:21:35 -0800 Subject: [PATCH 23/56] fix #35 --- CHANGELOG.md | 1 + src/js/rollup.config.js | 46 +++++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fff2e4be..52ca3238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Using the following categories, list your changes in this order: ### Fixed - `view_to_component` will now retain nodes (such as CSS and JavaScript) that were defined in a page's HTML `` +- The React client is now set to `production` rather than `development` ### Security diff --git a/src/js/rollup.config.js b/src/js/rollup.config.js index 646f8b69..2443f388 100644 --- a/src/js/rollup.config.js +++ b/src/js/rollup.config.js @@ -2,32 +2,28 @@ import resolve from "rollup-plugin-node-resolve"; import commonjs from "rollup-plugin-commonjs"; import replace from "rollup-plugin-replace"; -const { PRODUCTION } = process.env; - export default { - input: "src/index.js", - output: { - file: "../django_idom/static/django_idom/client.js", - format: "esm", - }, - plugins: [ - resolve(), - commonjs(), - replace({ - "process.env.NODE_ENV": JSON.stringify( - PRODUCTION ? "production" : "development" - ), - }), - ], - onwarn: function (warning) { - // Skip certain warnings + input: "src/index.js", + output: { + file: "../django_idom/static/django_idom/client.js", + format: "esm", + }, + plugins: [ + resolve(), + commonjs(), + replace({ + "process.env.NODE_ENV": JSON.stringify("production"), + }), + ], + onwarn: function (warning) { + // Skip certain warnings - // should intercept ... but doesn't in some rollup versions - if (warning.code === "THIS_IS_UNDEFINED") { - return; - } + // should intercept ... but doesn't in some rollup versions + if (warning.code === "THIS_IS_UNDEFINED") { + return; + } - // console.warn everything else - console.warn(warning.message); - }, + // console.warn everything else + console.warn(warning.message); + }, }; From e9a13647087f6ba496b336413d060c3626aea3b9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:27:35 -0800 Subject: [PATCH 24/56] fix #29 --- CHANGELOG.md | 4 ++++ src/django_idom/config.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ca3238..ec10c697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ Using the following categories, list your changes in this order: ## [Unreleased] +### Added + +- The built-in `idom` client will now automatically configure itself to debug mode depending on `settings.py:DEBUG` + ### Changed - The `component` template tag now supports both positional and keyword arguments. diff --git a/src/django_idom/config.py b/src/django_idom/config.py index 1d7b3075..7931f12d 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -4,12 +4,14 @@ from django.conf import settings from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches +from idom.config import IDOM_DEBUG_MODE from idom.core.types import ComponentConstructor from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR from django_idom.types import Postprocessor, ViewComponentIframe +IDOM_DEBUG_MODE.set_current(getattr(settings, "DEBUG")) IDOM_REGISTERED_COMPONENTS: Dict[str, ComponentConstructor] = {} IDOM_VIEW_COMPONENT_IFRAMES: Dict[str, ViewComponentIframe] = {} IDOM_WEBSOCKET_URL = getattr( From e0fcbd9351a56d046fc09692dd8a0819749eb431 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 16:10:23 -0800 Subject: [PATCH 25/56] Make some utils public if they're harmless --- src/django_idom/components.py | 6 +++--- src/django_idom/hooks.py | 6 +++--- src/django_idom/http/views.py | 4 ++-- src/django_idom/utils.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index a17c0037..f1583c76 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -13,7 +13,7 @@ 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, render_view +from django_idom.utils import generate_obj_name, render_view class _ViewComponentConstructor(Protocol): @@ -47,8 +47,8 @@ def _view_to_component( # Render the view render within a hook @hooks.use_effect( dependencies=[ - json.dumps(vars(_request), 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(): diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index e7eac90e..a923c063 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -19,7 +19,7 @@ from idom.core.hooks import use_effect, use_state from django_idom.types import Mutation, Query, QueryOptions, _Params, _Result -from django_idom.utils import _generate_obj_name +from django_idom.utils import generate_obj_name _logger = logging.getLogger(__name__) @@ -136,7 +136,7 @@ def execute_query() -> None: set_loading(False) set_error(e) _logger.exception( - f"Failed to execute query: {_generate_obj_name(query) or query}" + f"Failed to execute query: {generate_obj_name(query) or query}" ) return finally: @@ -179,7 +179,7 @@ def execute_mutation() -> None: set_loading(False) set_error(e) _logger.exception( - f"Failed to execute mutation: {_generate_obj_name(mutate) or mutate}" + f"Failed to execute mutation: {generate_obj_name(mutate) or mutate}" ) else: set_loading(False) diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index d365fe64..4cf42b0a 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -6,7 +6,7 @@ from idom.config import IDOM_WED_MODULES_DIR from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES -from django_idom.utils import _create_cache_key, render_view +from django_idom.utils import create_cache_key, render_view async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: @@ -23,7 +23,7 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: # Fetch the file from cache, if available last_modified_time = os.stat(path).st_mtime - cache_key = _create_cache_key("web_module", str(path).lstrip(str(web_modules_dir))) + cache_key = create_cache_key("web_module", str(path).lstrip(str(web_modules_dir))) response = await IDOM_CACHE.aget(cache_key, version=int(last_modified_time)) if response is None: async with async_open(path, "r") as fp: diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 670d79e0..6779b133 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -198,7 +198,7 @@ def _register_components(self, components: set[str]) -> None: ) -def _generate_obj_name(object: Any) -> str | None: +def generate_obj_name(object: Any) -> str | None: """Makes a best effort to create a name for an object. Useful for JSON serialization of Python objects.""" if hasattr(object, "__module__"): @@ -279,7 +279,7 @@ def func_has_params(func: Callable, *args, **kwargs) -> bool: return True -def _create_cache_key(*args): +def create_cache_key(*args): """Creates a cache key string that starts with `django_idom` contains all *args separated by `:`.""" From 2adb676e69c4372cb5269eac45203154b5821218 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 16:10:49 -0800 Subject: [PATCH 26/56] Make DATE_FORMAT a global --- src/django_idom/utils.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 6779b133..da96e24e 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -37,6 +37,7 @@ + _component_kwargs + r"\s*%}" ) +DATE_FORMAT = "%Y-%m-%d %H:%M:%S.%f+%Z" async def render_view( @@ -292,19 +293,13 @@ def create_cache_key(*args): def db_cleanup(immediate: bool = False): """Deletes expired component parameters from the database. This function may be expanded in the future to include additional cleanup tasks.""" - from .config import IDOM_CACHE, IDOM_RECONNECT_MAX + from .config import IDOM_CACHE from .models import ComponentParams - date_format: str = "%Y-%m-%d %H:%M:%S.%f+%Z" - cache_key: str = _create_cache_key("last_cleaned") - now_str: str = datetime.strftime(timezone.now(), date_format) + cache_key: str = create_cache_key("last_cleaned") + now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT) last_cleaned_str: str = IDOM_CACHE.get(cache_key) - - # Calculate the expiration time using Django timezones - last_cleaned: datetime = timezone.make_aware( - datetime.strptime(last_cleaned_str or now_str, date_format) - ) - expiration: datetime = last_cleaned + timedelta(seconds=IDOM_RECONNECT_MAX) + expiration: datetime = connection_expiration_date(last_cleaned_str or now_str) # Component params exist in the DB, but we don't know when they were last cleaned if not last_cleaned_str and ComponentParams.objects.all(): @@ -318,3 +313,14 @@ def db_cleanup(immediate: bool = False): if immediate or not last_cleaned_str or timezone.now() >= expiration: ComponentParams.objects.filter(last_accessed__gte=expiration).delete() IDOM_CACHE.set(cache_key, now_str) + + +def connection_expiration_date(datetime_str) -> datetime: + """Calculates the expiration time for a connection.""" + from .config import IDOM_RECONNECT_MAX + + # Calculate the expiration time using Django timezones + last_cleaned: datetime = timezone.make_aware( + datetime.strptime(datetime_str, DATE_FORMAT) + ) + return last_cleaned + timedelta(seconds=IDOM_RECONNECT_MAX) From 319dda2f691dc400702e058584a60a3036d70144 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 16:39:55 -0800 Subject: [PATCH 27/56] validate params_query expiration within fetch queries --- src/django_idom/websocket/consumer.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index 8d6d4fbd..25afdbc4 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -3,6 +3,7 @@ import asyncio import logging +from datetime import timedelta from typing import Any import dill as pickle @@ -55,6 +56,7 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None: async def _run_dispatch_loop(self): from django_idom import models + from django_idom.config import IDOM_RECONNECT_MAX scope = self.scope dotted_path = scope["url_route"]["kwargs"]["dotted_path"] @@ -69,11 +71,12 @@ async def _run_dispatch_loop(self): ), carrier=WebsocketConnection(self.close, self.disconnect, dotted_path), ) + now = timezone.now() component_args: tuple[Any, ...] = tuple() component_kwargs: dict = {} + # Verify the component has already been registered try: - # Verify the component has already been registered component_constructor = IDOM_REGISTERED_COMPONENTS[dotted_path] except KeyError: _logger.warning( @@ -81,15 +84,18 @@ async def _run_dispatch_loop(self): ) return + # Fetch the component's args/kwargs from the database, if needed try: - # Fetch the component's args/kwargs from the database, if needed if func_has_params(component_constructor): try: # Always clean up expired entries first await convert_to_async(db_cleanup)() # Get the queries from a DB - params_query = await models.ComponentParams.objects.aget(uuid=uuid) + params_query = await models.ComponentParams.objects.aget( + uuid=uuid, + last_accessed__gt=now - timedelta(seconds=IDOM_RECONNECT_MAX), + ) params_query.last_accessed = timezone.now() await convert_to_async(params_query.save)() except models.ComponentParams.DoesNotExist: @@ -113,8 +119,8 @@ async def _run_dispatch_loop(self): ) return + # Begin serving the IDOM component try: - # Begin serving the IDOM component await serve_json_patch( Layout(ConnectionContext(component_instance, value=connection)), self.send_json, From 00e3c85395fe2b2e5727bd0b692652eb0fb394dd Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 16:47:52 -0800 Subject: [PATCH 28/56] refactor db_cleanup --- src/django_idom/utils.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index da96e24e..af74de17 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -293,16 +293,20 @@ def create_cache_key(*args): def db_cleanup(immediate: bool = False): """Deletes expired component parameters from the database. This function may be expanded in the future to include additional cleanup tasks.""" - from .config import IDOM_CACHE + from .config import IDOM_CACHE, IDOM_RECONNECT_MAX from .models import ComponentParams cache_key: str = create_cache_key("last_cleaned") now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT) - last_cleaned_str: str = IDOM_CACHE.get(cache_key) - expiration: datetime = connection_expiration_date(last_cleaned_str or now_str) + cleaned_at_str: str = IDOM_CACHE.get(cache_key) + cleaned_at: datetime = timezone.make_aware( + datetime.strptime(cleaned_at_str or now_str, DATE_FORMAT) + ) + clean_needed_by = cleaned_at + timedelta(seconds=IDOM_RECONNECT_MAX) + expires_by: datetime = timezone.now() - timedelta(seconds=IDOM_RECONNECT_MAX) # Component params exist in the DB, but we don't know when they were last cleaned - if not last_cleaned_str and ComponentParams.objects.all(): + if not cleaned_at_str and ComponentParams.objects.all(): _logger.warning( "IDOM has detected component sessions in the database, " "but no timestamp was found in cache. This may indicate that " @@ -310,17 +314,6 @@ def db_cleanup(immediate: bool = False): ) # Delete expired component parameters - if immediate or not last_cleaned_str or timezone.now() >= expiration: - ComponentParams.objects.filter(last_accessed__gte=expiration).delete() + if immediate or not cleaned_at_str or timezone.now() >= clean_needed_by: + ComponentParams.objects.filter(last_accessed__lte=expires_by).delete() IDOM_CACHE.set(cache_key, now_str) - - -def connection_expiration_date(datetime_str) -> datetime: - """Calculates the expiration time for a connection.""" - from .config import IDOM_RECONNECT_MAX - - # Calculate the expiration time using Django timezones - last_cleaned: datetime = timezone.make_aware( - datetime.strptime(datetime_str, DATE_FORMAT) - ) - return last_cleaned + timedelta(seconds=IDOM_RECONNECT_MAX) From 43f4a32c3fdd80861242eef4359e96bd57db6a1e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 17:14:49 -0800 Subject: [PATCH 29/56] more deprivatization --- src/django_idom/apps.py | 2 +- src/django_idom/defaults.py | 4 ++-- src/django_idom/utils.py | 26 +++++++++++++------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/django_idom/apps.py b/src/django_idom/apps.py index 7de4c4fe..8c202f33 100644 --- a/src/django_idom/apps.py +++ b/src/django_idom/apps.py @@ -11,7 +11,7 @@ class DjangoIdomConfig(AppConfig): def ready(self): # Populate the IDOM component registry when Django is ready - ComponentPreloader().register_all() + ComponentPreloader().run() # Delete expired database entries # Suppress exceptions to avoid issues with `manage.py` commands such as diff --git a/src/django_idom/defaults.py b/src/django_idom/defaults.py index bc0530bb..9e2f7582 100644 --- a/src/django_idom/defaults.py +++ b/src/django_idom/defaults.py @@ -4,10 +4,10 @@ from django.conf import settings -from django_idom.utils import _import_dotted_path +from django_idom.utils import import_dotted_path -_DEFAULT_QUERY_POSTPROCESSOR: Callable[..., Any] | None = _import_dotted_path( +_DEFAULT_QUERY_POSTPROCESSOR: Callable[..., Any] | None = import_dotted_path( getattr( settings, "IDOM_DEFAULT_QUERY_POSTPROCESSOR", diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index af74de17..0dceee57 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -84,12 +84,12 @@ def _register_component(dotted_path: str) -> Callable: if dotted_path in IDOM_REGISTERED_COMPONENTS: return IDOM_REGISTERED_COMPONENTS[dotted_path] - IDOM_REGISTERED_COMPONENTS[dotted_path] = _import_dotted_path(dotted_path) + IDOM_REGISTERED_COMPONENTS[dotted_path] = import_dotted_path(dotted_path) _logger.debug("IDOM has registered component %s", dotted_path) return IDOM_REGISTERED_COMPONENTS[dotted_path] -def _import_dotted_path(dotted_path: str) -> Callable: +def import_dotted_path(dotted_path: str) -> Callable: """Imports a dotted path and returns the callable.""" module_name, component_name = dotted_path.rsplit(".", 1) @@ -104,18 +104,18 @@ def _import_dotted_path(dotted_path: str) -> Callable: class ComponentPreloader: - def register_all(self): + def run(self): """Registers all IDOM components found within Django templates.""" # Get all template folder paths - paths = self._get_paths() + paths = self.get_paths() # Get all HTML template files - templates = self._get_templates(paths) + templates = self.get_templates(paths) # Get all components - components = self._get_components(templates) + components = self.get_components(templates) # Register all components - self._register_components(components) + self.register_components(components) - def _get_loaders(self): + def get_loaders(self): """Obtains currently configured template loaders.""" template_source_loaders = [] for e in engines.all(): @@ -131,10 +131,10 @@ def _get_loaders(self): loaders.append(loader) return loaders - def _get_paths(self) -> set[str]: + def get_paths(self) -> set[str]: """Obtains a set of all template directories.""" paths: set[str] = set() - for loader in self._get_loaders(): + for loader in self.get_loaders(): with contextlib.suppress(ImportError, AttributeError, TypeError): module = import_module(loader.__module__) get_template_sources = getattr(module, "get_template_sources", None) @@ -143,7 +143,7 @@ def _get_paths(self) -> set[str]: paths.update(smart_str(origin) for origin in get_template_sources("")) return paths - def _get_templates(self, paths: set[str]) -> set[str]: + def get_templates(self, paths: set[str]) -> set[str]: """Obtains a set of all HTML template paths.""" extensions = [".html"] templates: set[str] = set() @@ -158,7 +158,7 @@ def _get_templates(self, paths: set[str]) -> set[str]: return templates - def _get_components(self, templates: set[str]) -> set[str]: + def get_components(self, templates: set[str]) -> set[str]: """Obtains a set of all IDOM components by parsing HTML templates.""" components: set[str] = set() for template in templates: @@ -182,7 +182,7 @@ def _get_components(self, templates: set[str]) -> set[str]: ) return components - def _register_components(self, components: set[str]) -> None: + def register_components(self, components: set[str]) -> None: """Registers all IDOM components in an iterable.""" for component in components: try: From f79f93634469c6d672f34e71382e09d1484d179e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 17:21:38 -0800 Subject: [PATCH 30/56] document django_query_postprocessor --- docs/src/features/utils.md | 60 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + src/django_idom/components.py | 2 +- src/django_idom/utils.py | 13 +++++++- 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 docs/src/features/utils.md diff --git a/docs/src/features/utils.md b/docs/src/features/utils.md new file mode 100644 index 00000000..bb96e943 --- /dev/null +++ b/docs/src/features/utils.md @@ -0,0 +1,60 @@ +???+ summary + + Utility functions that you can use when needed. + +## Django Query Postprocessor + +This is the default postprocessor for the `use_query` hook. + +To prevent Django's `SynchronousOnlyException`, this postprocessor will recursively fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily. + +=== "components.py" + + ```python + from example_project.my_app.models import TodoItem + from idom import component + from django_idom.hooks import use_query + from django_idom.types import QueryOptions + from django_idom.utils import django_query_postprocessor + + def get_items(): + return TodoItem.objects.all() + + @component + def todo_list(): + # These `QueryOptions` are functionally equivalent to Django-IDOM's default values + item_query = use_query( + QueryOptions( + postprocessor=django_query_postprocessor, + postprocessor_kwargs={"many_to_many": True, "many_to_one": True}, + ), + get_items, + ) + + ... + ``` + +=== "models.py" + + ```python + from django.db import models + + class TodoItem(models.Model): + text = models.CharField(max_length=255) + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `data` | `QuerySet | Model` | The `Model` or `QuerySet` to recursively fetch fields from. | N/A | + | `many_to_many` | `bool` | Whether or not to recursively fetch `ManyToManyField` relationships. | `True` | + | `many_to_one` | `bool` | Whether or not to recursively fetch `ForeignKey` relationships. | `True` | + + **Returns** + + | Type | Description | + | --- | --- | + | `QuerySet | Model` | The `Model` or `QuerySet` with all fields fetched. | diff --git a/mkdocs.yml b/mkdocs.yml index f51bc051..2a60cb5c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,7 @@ nav: - Components: features/components.md - Hooks: features/hooks.md - Decorators: features/decorators.md + - Utilities: features/utils.md - Template Tag: features/templatetag.md - Settings: features/settings.md - Contribute: diff --git a/src/django_idom/components.py b/src/django_idom/components.py index f1583c76..2733b614 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -131,7 +131,7 @@ def view_to_component( perfectly adhere to HTML5. Returns: - Callable: A function that takes `request: HttpRequest | None, *args: Any, key: Key | None, **kwargs: Any` + A function that takes `request: HttpRequest | None, *args: Any, key: Key | None, **kwargs: Any` and returns an IDOM component. """ diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 0dceee57..cdd8c425 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -215,7 +215,18 @@ def django_query_postprocessor( ) -> QuerySet | Model: """Recursively fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily. - Some behaviors can be modified through `query_options` attributes.""" + Behaviors can be modified through `QueryOptions` within your `use_query` hook. + + Args: + data: The `Model` or `QuerySet` to recursively fetch fields from. + + Keyword Args: + many_to_many: Whether or not to recursively fetch `ManyToManyField` relationships. + many_to_one: Whether or not to recursively fetch `ForeignKey` relationships. + + Returns: + The `Model` or `QuerySet` with all fields fetched. + """ # `QuerySet`, which is an iterable of `Model`/`QuerySet` instances # https://github.com/typeddjango/django-stubs/issues/704 From 83730cd52829a309ff5573a980e487f16f255a25 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 17:25:06 -0800 Subject: [PATCH 31/56] comment for _register_component --- src/django_idom/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index cdd8c425..595a1f82 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -79,6 +79,8 @@ async def render_view( def _register_component(dotted_path: str) -> Callable: + """Adds a component to the mapping of registered components. + This should only be called on startup to maintain synchronization during mulitprocessing.""" from django_idom.config import IDOM_REGISTERED_COMPONENTS if dotted_path in IDOM_REGISTERED_COMPONENTS: From 5dd8d1675a3e78c06dc21c459366a455685a1bd7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 11 Jan 2023 17:30:06 -0800 Subject: [PATCH 32/56] add postprocessor to dictionary --- docs/src/dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 64d6903a..1bbd2e9b 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -26,3 +26,4 @@ asgi postfixed postprocessing serializable +postprocessor From 9f6b757c56717107ed9ec0d8f57dd1f4da8847f8 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 13 Jan 2023 16:04:47 -0800 Subject: [PATCH 33/56] docs revision --- CHANGELOG.md | 19 ++++++++++--------- docs/src/features/components.md | 3 +-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec10c697..ed2617b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,32 +24,33 @@ Using the following categories, list your changes in this order: ### Added -- The built-in `idom` client will now automatically configure itself to debug mode depending on `settings.py:DEBUG` +- The built-in `idom` client will now automatically configure itself to debug mode depending on `settings.py:DEBUG`. ### Changed - The `component` template tag now supports both positional and keyword arguments. - The `component` template tag now supports non-serializable arguments. -- `use_location`, `use_scope`, and `use_websocket` previously contained within `django_idom.hooks` have been migrated to `idom.backend.hooks`. +- `use_location` and `use_scope` previously contained within `django_idom.hooks` have been migrated to `idom.backend.hooks`. +- `use_websocket` functionality has been moved into `idom.backend.hooks.use_connection`. - Bumped the minimum IDOM version to 0.43.0 -- `IDOM_WS_MAX_RECONNECT_TIMEOUT` setting has been renamed to `IDOM_RECONNECT_MAX` -- `django_idom.types.IdomWebsocket` has been renamed to `WebsocketConnection` -- It is now mandatory to run `manage.py migrate` after installing IDOM +- `IDOM_WS_MAX_RECONNECT_TIMEOUT` setting has been renamed to `IDOM_RECONNECT_MAX`. +- `django_idom.types.IdomWebsocket` has been renamed to `WebsocketConnection`. +- It is now mandatory to run `manage.py migrate` after installing IDOM. ### Removed - `django_idom.hooks.use_location` has been removed. The equivalent replacement is found at `idom.backend.hooks.use_location`. - `django_idom.hooks.use_scope` has been removed. The equivalent replacement is found at `idom.backend.hooks.use_scope`. -- `django_idom.hooks.use_websocket` has been removed. The equivalent replacement is found at `idom.backend.hooks.use_connection`. +- `django_idom.hooks.use_websocket` has been removed. The functionally similar replacement is found at `idom.backend.hooks.use_connection`. ### Fixed -- `view_to_component` will now retain nodes (such as CSS and JavaScript) that were defined in a page's HTML `` -- The React client is now set to `production` rather than `development` +- `view_to_component` will now retain any HTML that was defined in a `` tag. +- The React client is now set to `production` rather than `development`. ### Security -- Fixed a potential method of component argument spoofing +- Fixed a potential method of component argument spoofing. ## [2.2.1] - 2022-01-09 diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 9f444d8d..849635ab 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -77,8 +77,7 @@ Convert any Django view into a IDOM component by using this decorator. Compatibl - 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 (such as `#!html `) click events + - Has no option to automatically intercept local anchor link (such as `#!html `) click events. _Please note these limitations do not exist when using `compatibility` mode._ From 5d56d238bb34eba50dab6b377ebd7d72e7c13de2 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 13 Jan 2023 16:51:00 -0800 Subject: [PATCH 34/56] misc docs updates --- docs/src/dictionary.txt | 1 + docs/src/features/decorators.md | 4 +--- docs/src/features/settings.md | 9 ++++---- docs/src/features/templatetag.md | 22 +++++-------------- docs/src/features/utils.md | 2 +- .../getting-started/reference-component.md | 2 -- 6 files changed, 13 insertions(+), 27 deletions(-) diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 1bbd2e9b..c21b4dfd 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -27,3 +27,4 @@ postfixed postprocessing serializable postprocessor +preprocessor diff --git a/docs/src/features/decorators.md b/docs/src/features/decorators.md index c4b60d9f..19ff8726 100644 --- a/docs/src/features/decorators.md +++ b/docs/src/features/decorators.md @@ -4,14 +4,12 @@ ## Auth Required -You can limit access to a component to users with a specific `auth_attribute` by using this decorator. +You can limit access to a component to users with a specific `auth_attribute` by using this decorator (with or without parentheses). By default, this decorator checks if the user is logged in, and his/her account has not been deactivated. This decorator is commonly used to selectively render a component only if a user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). -This decorator can be used with or without parentheses. - === "components.py" ```python diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md index a3060f15..826da81d 100644 --- a/docs/src/features/settings.md +++ b/docs/src/features/settings.md @@ -9,16 +9,17 @@ ```python - # If "idom" cache is not configured, then we will use "default" instead + # If "idom" cache is not configured, then "default" will be used + # IDOM expects a multiprocessing-safe and thread-safe cache backend. CACHES = { "idom": {"BACKEND": ...}, } - # Maximum seconds between a reconnection attempt before giving up. - # 0 will disable reconnection. + # Maximum seconds between reconnection attempts before giving up. + # Use `0` to prevent component reconnection. IDOM_RECONNECT_MAX = 259200 - # The URL for IDOM to serve websockets + # The URL for IDOM to serve the component rendering websocket IDOM_WEBSOCKET_URL = "idom/" # Dotted path to the default postprocessor function, or `None` diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md index bdaf8074..42f2bccd 100644 --- a/docs/src/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -4,6 +4,8 @@ ## Component +The `component` template tag can be used to insert any number of IDOM components onto your page. + === "my-template.html" {% include-markdown "../../../README.md" start="" end="" %} @@ -12,7 +14,7 @@ ??? warning "Do not use context variables for the IDOM component name" - Our pre-processor relies on the template tag containing a string. + Our preprocessor relies on the template tag containing a string. **Do not** use Django template/context variables for the component path. Failure to follow this warning will result in unexpected behavior. @@ -92,22 +94,8 @@ ``` - But keep in mind, in scenarios where you are trying to create a Single Page Application (SPA) within Django, you will only have one central component within your `#!html ` tag. + Please note that components separated like this will not be able to interact with each other, except through database queries. - Additionally, the components in the example above will not be able to interact with each other, except through database queries. + Additionally, in scenarios where you are trying to create a Single Page Application (SPA) within Django, you will only have one component within your `#!html ` tag. - - - -??? question "What is a "template tag"?" - - You can think of template tags as Django's way of allowing you to run Python code within your HTML. Django-IDOM uses a `#!jinja {% component ... %}` template tag to perform it's magic. - - Keep in mind, in order to use the `#!jinja {% component ... %}` tag, you will need to first call `#!jinja {% load idom %}` to gain access to it. - - === "my-template.html" - - {% include-markdown "../../../README.md" start="" end="" %} - - diff --git a/docs/src/features/utils.md b/docs/src/features/utils.md index bb96e943..9a0aca8b 100644 --- a/docs/src/features/utils.md +++ b/docs/src/features/utils.md @@ -6,7 +6,7 @@ This is the default postprocessor for the `use_query` hook. -To prevent Django's `SynchronousOnlyException`, this postprocessor will recursively fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily. +This postprocessor is designed to prevent Django's `SynchronousOnlyException` by recursively fetching all fields within a `Model` or `QuerySet` to prevent [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy). === "components.py" diff --git a/docs/src/getting-started/reference-component.md b/docs/src/getting-started/reference-component.md index d341af11..1576f3ee 100644 --- a/docs/src/getting-started/reference-component.md +++ b/docs/src/getting-started/reference-component.md @@ -16,8 +16,6 @@ {% include-markdown "../features/templatetag.md" start="" end="" %} -{% include-markdown "../features/templatetag.md" start="" end="" %} - ??? question "Where is my templates folder?" If you do not have a `templates` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). From 827adf3439547ad40d9041a936fe809b90fa9d3d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 13 Jan 2023 17:22:33 -0800 Subject: [PATCH 35/56] move hooks back into `django_idom.hooks` --- CHANGELOG.md | 17 ++++++-------- docs/src/features/hooks.md | 14 ++++++------ src/django_idom/__init__.py | 4 ++-- src/django_idom/decorators.py | 3 ++- src/django_idom/hooks.py | 32 ++++++++++++++++++++++++--- src/django_idom/types.py | 22 +++++++++++++++--- src/django_idom/websocket/consumer.py | 4 ++-- tests/test_app/components.py | 20 +++++++---------- 8 files changed, 76 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed2617b6..bb087c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,33 +24,30 @@ Using the following categories, list your changes in this order: ### Added -- The built-in `idom` client will now automatically configure itself to debug mode depending on `settings.py:DEBUG`. +- The `idom` client will automatically configure itself to debug mode depending on `settings.py:DEBUG`. +- `use_connection` hook for returning the browser's active `Connection` ### Changed - The `component` template tag now supports both positional and keyword arguments. - The `component` template tag now supports non-serializable arguments. -- `use_location` and `use_scope` previously contained within `django_idom.hooks` have been migrated to `idom.backend.hooks`. -- `use_websocket` functionality has been moved into `idom.backend.hooks.use_connection`. -- Bumped the minimum IDOM version to 0.43.0 - `IDOM_WS_MAX_RECONNECT_TIMEOUT` setting has been renamed to `IDOM_RECONNECT_MAX`. -- `django_idom.types.IdomWebsocket` has been renamed to `WebsocketConnection`. - It is now mandatory to run `manage.py migrate` after installing IDOM. +- Bumped the minimum IDOM version to 0.43.0 ### Removed -- `django_idom.hooks.use_location` has been removed. The equivalent replacement is found at `idom.backend.hooks.use_location`. -- `django_idom.hooks.use_scope` has been removed. The equivalent replacement is found at `idom.backend.hooks.use_scope`. -- `django_idom.hooks.use_websocket` has been removed. The functionally similar replacement is found at `idom.backend.hooks.use_connection`. +- `django_idom.hooks.use_websocket` has been removed. The similar replacement is `django_idom.hooks.use_connection`. +- `django_idom.types.IdomWebsocket` has been removed. The similar replacement is `django_idom.types.Connection` ### Fixed - `view_to_component` will now retain any HTML that was defined in a `` tag. -- The React client is now set to `production` rather than `development`. +- React client is now set to `production` rather than `development`. ### Security -- Fixed a potential method of component argument spoofing. +- Fixed a potential method of component template tag argument spoofing. ## [2.2.1] - 2022-01-09 diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index c8bb6c06..5e1f9731 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -437,12 +437,12 @@ You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en ```python from idom import component, html - from idom.backend.hooks import use_connection + from django_idom.hooks import use_connection @component def my_component(): - my_websocket = use_connection() - return html.div(my_websocket) + my_connection = use_connection() + return html.div(my_connection) ``` ??? example "See Interface" @@ -455,7 +455,7 @@ You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en | Type | Description | | --- | --- | - | `WebsocketConnection` | The component's websocket. | + | `Connection` | The component's websocket. | ## Use Scope @@ -465,7 +465,7 @@ This is a shortcut that returns the Websocket's [`scope`](https://channels.readt ```python from idom import component, html - from idom.backend.hooks import use_scope + from django_idom.hooks import use_scope @component def my_component(): @@ -483,7 +483,7 @@ This is a shortcut that returns the Websocket's [`scope`](https://channels.readt | Type | Description | | --- | --- | - | `dict[str, Any]` | The websocket's `scope`. | + | `MutableMapping[str, Any]` | The websocket's `scope`. | ## Use Location @@ -495,7 +495,7 @@ You can expect this hook to provide strings such as `/idom/my_path`. ```python from idom import component, html - from idom.backend.hooks import use_location + from django_idom.hooks import use_location @component def my_component(): diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index aa912c6f..28708af9 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,12 +1,12 @@ from django_idom import components, decorators, hooks, types, utils -from django_idom.types import WebsocketConnection +from django_idom.types import ComponentWebsocket from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH __version__ = "2.2.1" __all__ = [ "IDOM_WEBSOCKET_PATH", - "WebsocketConnection", + "ComponentWebsocket", "hooks", "components", "decorators", diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py index 7e6918da..fb78f74e 100644 --- a/src/django_idom/decorators.py +++ b/src/django_idom/decorators.py @@ -3,9 +3,10 @@ from functools import wraps from typing import Callable -from idom.backend.hooks import use_scope from idom.core.types import ComponentType, VdomDict +from django_idom.hooks import use_scope + def auth_required( component: Callable | None = None, diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index a923c063..9508a4d0 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -7,6 +7,7 @@ Awaitable, Callable, DefaultDict, + MutableMapping, Sequence, Union, cast, @@ -15,10 +16,20 @@ from channels.db import database_sync_to_async as _database_sync_to_async from idom import use_callback, use_ref -from idom.backend.hooks import use_scope +from idom.backend.hooks import use_connection as _use_connection +from idom.backend.hooks import use_location as _use_location +from idom.backend.hooks import use_scope as _use_scope +from idom.backend.types import Location from idom.core.hooks import use_effect, use_state -from django_idom.types import Mutation, Query, QueryOptions, _Params, _Result +from django_idom.types import ( + Connection, + Mutation, + Query, + QueryOptions, + _Params, + _Result, +) from django_idom.utils import generate_obj_name @@ -32,10 +43,15 @@ ] = DefaultDict(set) +def use_location() -> Location: + """Get the current route as a `Location` object""" + return _use_location() + + def use_origin() -> str | None: """Get the current origin as a string. If the browser did not send an origin header, this will be None.""" - scope = use_scope() + scope = _use_scope() try: return next( ( @@ -49,6 +65,16 @@ def use_origin() -> str | None: return None +def use_scope() -> MutableMapping[str, Any]: + """Get the current ASGI scope dictionary""" + return _use_scope() + + +def use_connection() -> Connection: + """Get the current `Connection` object""" + return _use_connection() # type: ignore + + @overload def use_query( options: QueryOptions, diff --git a/src/django_idom/types.py b/src/django_idom/types.py index c2d16d6b..8eb009f1 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -6,6 +6,7 @@ Awaitable, Callable, Generic, + MutableMapping, Optional, Protocol, Sequence, @@ -16,12 +17,13 @@ from django.db.models.base import Model from django.db.models.query import QuerySet from django.views.generic import View +from idom.types import Location from typing_extensions import ParamSpec from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR -__all__ = ["_Result", "_Params", "_Data", "WebsocketConnection", "Query", "Mutation"] +__all__ = ["_Result", "_Params", "_Data", "ComponentWebsocket", "Query", "Mutation"] _Result = TypeVar("_Result", bound=Union[Model, QuerySet[Any]]) _Params = ParamSpec("_Params") @@ -29,8 +31,22 @@ @dataclass -class WebsocketConnection: - """Carrier type for `idom.backend.hooks.use_connection().carrier`""" +class Connection: + """Represents a connection with a client""" + + scope: MutableMapping[str, Any] + """An ASGI scope or WSGI environment dictionary""" + + location: Location + """The current location (URL)""" + + carrier: ComponentWebsocket + """The websocket that mediates the connection.""" + + +@dataclass +class ComponentWebsocket: + """Carrier type for the `use_connection` hook.""" close: Callable[[Optional[int]], Awaitable[None]] disconnect: Callable[[int], Awaitable[None]] diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index 25afdbc4..4a3c5d20 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -17,7 +17,7 @@ from idom.core.serve import serve_json_patch from django_idom.config import IDOM_REGISTERED_COMPONENTS -from django_idom.types import ComponentParamData, WebsocketConnection +from django_idom.types import ComponentParamData, ComponentWebsocket from django_idom.utils import db_cleanup, func_has_params @@ -69,7 +69,7 @@ async def _run_dispatch_loop(self): pathname=scope["path"], search=f"?{search}" if (search and (search != "undefined")) else "", ), - carrier=WebsocketConnection(self.close, self.disconnect, dotted_path), + carrier=ComponentWebsocket(self.close, self.disconnect, dotted_path), ) now = timezone.now() component_args: tuple[Any, ...] = tuple() diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 11939d8e..840577a3 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -4,14 +4,10 @@ from django.http import HttpRequest from django.shortcuts import render from idom import component, hooks, html, web -from idom.backend.hooks import use_connection as use_connection_hook -from idom.backend.hooks import use_location as use_location_hook -from idom.backend.hooks import use_scope as use_scope_hook from test_app.models import ForiegnChild, RelationalChild, RelationalParent, TodoItem import django_idom from django_idom.components import view_to_component -from django_idom.hooks import use_mutation, use_query from . import views from .types import TestObject @@ -90,7 +86,7 @@ def simple_button(): @component def use_connection(): - ws = use_connection_hook() + ws = django_idom.hooks.use_connection() success = bool( ws.scope and ws.location @@ -107,7 +103,7 @@ def use_connection(): @component def use_scope(): - scope = use_scope_hook() + scope = django_idom.hooks.use_scope() success = len(scope) >= 10 and scope["type"] == "websocket" return html.div( {"id": "use-scope", "data-success": success}, @@ -118,7 +114,7 @@ def use_scope(): @component def use_location(): - location = use_location_hook() + location = django_idom.hooks.use_location() success = bool(location) return html.div( {"id": "use-location", "data-success": success}, @@ -223,8 +219,8 @@ def get_foriegn_child_query(): @component def relational_query(): - foriegn_child = use_query(get_foriegn_child_query) - relational_parent = use_query(get_relational_parent_query) + foriegn_child = django_idom.hooks.use_query(get_foriegn_child_query) + relational_parent = django_idom.hooks.use_query(get_relational_parent_query) if not relational_parent.data or not foriegn_child.data: return @@ -271,8 +267,8 @@ def toggle_todo_mutation(item: TodoItem): @component def todo_list(): input_value, set_input_value = hooks.use_state("") - items = use_query(get_todo_query) - toggle_item = use_mutation(toggle_todo_mutation) + items = django_idom.hooks.use_query(get_todo_query) + toggle_item = django_idom.hooks.use_mutation(toggle_todo_mutation) if items.error: rendered_items = html.h2(f"Error when loading - {items.error}") @@ -286,7 +282,7 @@ def todo_list(): _render_todo_items([i for i in items.data if i.done], toggle_item), ) - add_item = use_mutation(add_todo_mutation, refetch=get_todo_query) + add_item = django_idom.hooks.use_mutation(add_todo_mutation, refetch=get_todo_query) if add_item.loading: mutation_status = html.h2("Working...") From 03f315bbc3dd6e24aa80916cdb8568eec33e3830 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 14 Jan 2023 00:09:15 -0800 Subject: [PATCH 36/56] v3.0.0 --- CHANGELOG.md | 7 +++++- src/django_idom/__init__.py | 2 +- .../0003_alter_relationalparent_one_to_one.py | 24 ------------------- 3 files changed, 7 insertions(+), 26 deletions(-) delete mode 100644 tests/test_app/migrations/0003_alter_relationalparent_one_to_one.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1607914c..e5457ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ Using the following categories, list your changes in this order: ## [Unreleased] +- Nothing (yet) + +## [3.0.0] - 2022-01-14 + ### Added - The `idom` client will automatically configure itself to debug mode depending on `settings.py:DEBUG`. @@ -217,7 +221,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.2.1...HEAD +[unreleased]: https://github.com/idom-team/django-idom/compare/3.0.0...HEAD +[3.0.0]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0 [2.2.1]: https://github.com/idom-team/django-idom/compare/2.2.0...2.2.1 [2.2.0]: https://github.com/idom-team/django-idom/compare/2.1.0...2.2.0 [2.1.0]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0 diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 28708af9..0cf317ac 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.2.1" +__version__ = "3.0.0" __all__ = [ "IDOM_WEBSOCKET_PATH", "ComponentWebsocket", diff --git a/tests/test_app/migrations/0003_alter_relationalparent_one_to_one.py b/tests/test_app/migrations/0003_alter_relationalparent_one_to_one.py deleted file mode 100644 index 1d3ab63d..00000000 --- a/tests/test_app/migrations/0003_alter_relationalparent_one_to_one.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.1.3 on 2023-01-10 00:52 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("test_app", "0002_relationalchild_relationalparent_foriegnchild"), - ] - - operations = [ - migrations.AlterField( - model_name="relationalparent", - name="one_to_one", - field=models.OneToOneField( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="one_to_one", - to="test_app.relationalchild", - ), - ), - ] From 8f83c1be6b0379913b5d036164612188be39601d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 14 Jan 2023 01:21:14 -0800 Subject: [PATCH 37/56] docs formatting --- docs/src/contribute/code.md | 6 +++--- docs/src/contribute/docs.md | 6 +++--- docs/src/contribute/running-tests.md | 6 +++--- docs/src/getting-started/installation.md | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/contribute/code.md b/docs/src/contribute/code.md index 2a6b9feb..3575671b 100644 --- a/docs/src/contribute/code.md +++ b/docs/src/contribute/code.md @@ -10,7 +10,7 @@ If you plan to make code changes to this repository, you will need to install th Once done, you should clone this repository: -```bash +```bash linenums="0" git clone https://github.com/idom-team/django-idom.git cd django-idom ``` @@ -20,13 +20,13 @@ Then, by running the command below you can: - Install an editable version of the Python code - Download, build, and install Javascript dependencies -```bash +```bash linenums="0" pip install -e . -r requirements.txt ``` Finally, to verify that everything is working properly, you can manually run the development webserver. -```bash +```bash linenums="0" cd tests python manage.py runserver ``` diff --git a/docs/src/contribute/docs.md b/docs/src/contribute/docs.md index d918c4b9..666cf6e1 100644 --- a/docs/src/contribute/docs.md +++ b/docs/src/contribute/docs.md @@ -5,7 +5,7 @@ If you plan to make changes to this documentation, you will need to install the Once done, you should clone this repository: -```bash +```bash linenums="0" git clone https://github.com/idom-team/django-idom.git cd django-idom ``` @@ -15,13 +15,13 @@ Then, by running the command below you can: - Install an editable version of the documentation - Self-host a test server for the documentation -```bash +```bash linenums="0" pip install -r ./requirements/build-docs.txt --upgrade ``` Finally, to verify that everything is working properly, you can manually run the docs preview webserver. -```bash +```bash linenums="0" mkdocs serve ``` diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md index 8f806e08..5d4c48cf 100644 --- a/docs/src/contribute/running-tests.md +++ b/docs/src/contribute/running-tests.md @@ -7,7 +7,7 @@ If you plan to run tests, you will need to install the following dependencies fi Once done, you should clone this repository: -```bash +```bash linenums="0" git clone https://github.com/idom-team/django-idom.git cd django-idom pip install -r ./requirements/test-run.txt --upgrade @@ -15,12 +15,12 @@ pip install -r ./requirements/test-run.txt --upgrade Then, by running the command below you can run the full test suite: -``` +```bash linenums="0" nox -s test ``` Or, if you want to run the tests in the foreground: -``` +```bash linenums="0" nox -s test -- --headed ``` diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 140e0c3f..f311abef 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -8,7 +8,7 @@ These docs assumes you have already created [a **Django project**](https://docs. ## Step 1: Install from PyPI -```bash +```bash linenums="0" pip install django-idom ``` @@ -101,6 +101,6 @@ Register IDOM's Websocket using `IDOM_WEBSOCKET_PATH`. Run Django's database migrations to initialize Django-IDOM's database table. -```bash +```bash linenums="0" python manage.py migrate ``` From be078518909303b22b3ac5cb8fa0dbd70c311eb1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 14 Jan 2023 20:54:36 -0800 Subject: [PATCH 38/56] better link for keys --- docs/src/features/templatetag.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md index 42f2bccd..7dc34c2e 100644 --- a/docs/src/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -46,7 +46,7 @@ The `component` template tag can be used to insert any number of IDOM components For this template tag, there are two reserved keyword arguments: `class` and `key` - `class` allows you to apply a HTML class to the top-level component div. This is useful for styling purposes. - - `key` allows you to force the component to use a [specific key value](https://idom-docs.herokuapp.com/docs/guides/understanding-idom/why-idom-needs-keys.html?highlight=key). Using `key` within a template tag is effectively useless. + - `key` allows you to force the component to use a [specific key value](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `key` within a template tag is effectively useless. === "my-template.html" From 6209c075a7332692857bbd944826c087d7fcde98 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 14 Jan 2023 21:05:53 -0800 Subject: [PATCH 39/56] subclass core's Connection --- src/django_idom/types.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 8eb009f1..f134bdc9 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -6,7 +6,6 @@ Awaitable, Callable, Generic, - MutableMapping, Optional, Protocol, Sequence, @@ -17,7 +16,7 @@ from django.db.models.base import Model from django.db.models.query import QuerySet from django.views.generic import View -from idom.types import Location +from idom.types import Connection as _Connection from typing_extensions import ParamSpec from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR @@ -30,20 +29,6 @@ _Data = TypeVar("_Data") -@dataclass -class Connection: - """Represents a connection with a client""" - - scope: MutableMapping[str, Any] - """An ASGI scope or WSGI environment dictionary""" - - location: Location - """The current location (URL)""" - - carrier: ComponentWebsocket - """The websocket that mediates the connection.""" - - @dataclass class ComponentWebsocket: """Carrier type for the `use_connection` hook.""" @@ -53,6 +38,9 @@ class ComponentWebsocket: dotted_path: str +Connection = _Connection[ComponentWebsocket] + + @dataclass class Query(Generic[_Data]): """Queries generated by the `use_query` hook.""" From 04a2efcf7885e7eaec29b06e0f875e25c51e36fb Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 14 Jan 2023 21:08:46 -0800 Subject: [PATCH 40/56] Complete types list --- src/django_idom/__init__.py | 2 -- src/django_idom/types.py | 14 +++++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 0cf317ac..6ab766f9 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,12 +1,10 @@ from django_idom import components, decorators, hooks, types, utils -from django_idom.types import ComponentWebsocket from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH __version__ = "3.0.0" __all__ = [ "IDOM_WEBSOCKET_PATH", - "ComponentWebsocket", "hooks", "components", "decorators", diff --git a/src/django_idom/types.py b/src/django_idom/types.py index f134bdc9..3a47d536 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -22,7 +22,19 @@ from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR -__all__ = ["_Result", "_Params", "_Data", "ComponentWebsocket", "Query", "Mutation"] +__all__ = [ + "_Result", + "_Params", + "_Data", + "ComponentWebsocket", + "Query", + "Mutation", + "Connection", + "ViewComponentIframe", + "Postprocessor", + "QueryOptions", + "ComponentParamData", +] _Result = TypeVar("_Result", bound=Union[Model, QuerySet[Any]]) _Params = ParamSpec("_Params") From 196c756c2e5d1cbbeeefde2c350d6dee83a02518 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 14 Jan 2023 22:10:19 -0800 Subject: [PATCH 41/56] remove unused ignore --- src/django_idom/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 9508a4d0..dd17ca16 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -72,7 +72,7 @@ def use_scope() -> MutableMapping[str, Any]: def use_connection() -> Connection: """Get the current `Connection` object""" - return _use_connection() # type: ignore + return _use_connection() @overload From ac415ddeb1e44d695f33c267d0a4149f78d6fb60 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 15 Jan 2023 16:35:29 -0800 Subject: [PATCH 42/56] tests for ComponentParams expiration --- tests/test_app/tests/test_components.py | 2 +- tests/test_app/tests/test_database.py | 39 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/test_app/tests/test_database.py diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index d5e3c61e..359b7dbe 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -9,7 +9,7 @@ CLICK_DELAY = 250 # Delay in miliseconds. Needed for GitHub Actions. -class TestIdomCapabilities(ChannelsLiveServerTestCase): +class ComponentTests(ChannelsLiveServerTestCase): @classmethod def setUpClass(cls): if sys.platform == "win32": diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py new file mode 100644 index 00000000..fc74aab5 --- /dev/null +++ b/tests/test_app/tests/test_database.py @@ -0,0 +1,39 @@ +from uuid import uuid4 + +import dill as pickle +from django.test import TransactionTestCase + +from django_idom import utils +from django_idom.models import ComponentParams +from django_idom.types import ComponentParamData + + +class DatabaseTests(TransactionTestCase): + def test_component_params(self): + # Make sure the ComponentParams table is empty + self.assertEqual(ComponentParams.objects.count(), 0) + + # Add component params to the database + params = ComponentParamData([1], {"test_value": 1}) + model = ComponentParams(uuid4().hex, data=pickle.dumps(params)) + model.full_clean() + model.save() + + # Check if a component params are in the database + self.assertEqual(ComponentParams.objects.count(), 1) + + # Check if the data is the same + self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params) # type: ignore + + # Check if the data is the same after a reload + ComponentParams.objects.first().refresh_from_db() # type: ignore + self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params) # type: ignore + + # Force the data to expire + from django_idom import config + + config.IDOM_RECONNECT_MAX = 0 + utils.db_cleanup() # Don't use `immediate` to better simulate a real world scenario + + # Make sure the data is gone + self.assertEqual(ComponentParams.objects.count(), 0) From 64478854fc25027f32793fbffae665d12edde025 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 15 Jan 2023 16:36:42 -0800 Subject: [PATCH 43/56] use tuple for ComponentParamData --- tests/test_app/tests/test_database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py index fc74aab5..25204c07 100644 --- a/tests/test_app/tests/test_database.py +++ b/tests/test_app/tests/test_database.py @@ -14,7 +14,7 @@ def test_component_params(self): self.assertEqual(ComponentParams.objects.count(), 0) # Add component params to the database - params = ComponentParamData([1], {"test_value": 1}) + params = ComponentParamData((1,), {"test_value": 1}) model = ComponentParams(uuid4().hex, data=pickle.dumps(params)) model.full_clean() model.save() From 1a9b06e60c3fae5cc67e82da8c748d8209b62dff Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 16 Jan 2023 02:21:10 -0800 Subject: [PATCH 44/56] better type hints for ComponentParamData --- src/django_idom/types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 3a47d536..a08df22a 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -6,6 +6,7 @@ Awaitable, Callable, Generic, + MutableMapping, Optional, Protocol, Sequence, @@ -111,5 +112,5 @@ class ComponentParamData: """Container used for serializing component parameters. This dataclass is pickled & stored in the database, then unpickled when needed.""" - args: tuple - kwargs: dict + args: Sequence + kwargs: MutableMapping[str, Any] From cb0bd4f49274cdab87534abc32aef22529f72845 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 16 Jan 2023 02:21:46 -0800 Subject: [PATCH 45/56] Better type hints for postprocessor_kwargs --- src/django_idom/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index a08df22a..98361f23 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -103,7 +103,7 @@ class QueryOptions: additionally can be configured via `postprocessor_kwargs` to recursively fetch `many_to_many` and `many_to_one` fields.""" - postprocessor_kwargs: dict[str, Any] = field(default_factory=lambda: {}) + postprocessor_kwargs: MutableMapping[str, Any] = field(default_factory=lambda: {}) """Keyworded arguments directly passed into the `postprocessor` for configuration.""" From 648d3a21b14ee55e47caf17c9613ef21461ba594 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 16 Jan 2023 02:35:58 -0800 Subject: [PATCH 46/56] type hint fixes --- src/django_idom/websocket/consumer.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index 4a3c5d20..e5bd985c 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -4,7 +4,7 @@ import asyncio import logging from datetime import timedelta -from typing import Any +from typing import Any, MutableMapping, Sequence import dill as pickle from channels.auth import login @@ -62,8 +62,8 @@ async def _run_dispatch_loop(self): dotted_path = scope["url_route"]["kwargs"]["dotted_path"] uuid = scope["url_route"]["kwargs"]["uuid"] search = scope["query_string"].decode() - self._idom_recv_queue = recv_queue = asyncio.Queue() # type: ignore - connection = Connection( # Set up the `idom.backend.hooks` using context values + self._idom_recv_queue: asyncio.Queue = asyncio.Queue() + connection = Connection( # For `use_connection` scope=scope, location=Location( pathname=scope["path"], @@ -72,8 +72,8 @@ async def _run_dispatch_loop(self): carrier=ComponentWebsocket(self.close, self.disconnect, dotted_path), ) now = timezone.now() - component_args: tuple[Any, ...] = tuple() - component_kwargs: dict = {} + component_args: Sequence[Any] = tuple() + component_kwargs: MutableMapping[str, Any] = {} # Verify the component has already been registered try: @@ -101,7 +101,8 @@ async def _run_dispatch_loop(self): except models.ComponentParams.DoesNotExist: _logger.warning( f"Browser has attempted to access '{dotted_path}', " - f"but the component has already expired beyond IDOM_RECONNECT_MAX." + f"but the component has already expired beyond IDOM_RECONNECT_MAX. " + "If this was expected, this warning can be ignored." ) return component_params: ComponentParamData = pickle.loads(params_query.data) @@ -124,7 +125,7 @@ async def _run_dispatch_loop(self): await serve_json_patch( Layout(ConnectionContext(component_instance, value=connection)), self.send_json, - recv_queue.get, + self._idom_recv_queue.get, ) except Exception: await self.close() From 54b9840c1d62b197c6cabafc7a9316fa12b482c3 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 16 Jan 2023 02:41:18 -0800 Subject: [PATCH 47/56] type hint for ComponentPreloader --- src/django_idom/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 70c6dc33..b49c1428 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -106,6 +106,9 @@ def import_dotted_path(dotted_path: str) -> Callable: class ComponentPreloader: + """Preloads all IDOM components found within Django templates. + This should only be `run` once on startup to maintain synchronization during mulitprocessing.""" + def run(self): """Registers all IDOM components found within Django templates.""" # Get all template folder paths From 976f10f0a53d6d7f8c45d7fe0243237750e2f439 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 20 Jan 2023 05:26:21 -0800 Subject: [PATCH 48/56] timezone might not always be available, so don't rely on it. --- src/django_idom/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index b49c1428..c2f4479b 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -37,7 +37,7 @@ + _component_kwargs + r"\s*%}" ) -DATE_FORMAT = "%Y-%m-%d %H:%M:%S.%f+%Z" +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" async def render_view( From 3f6069cc870cb8848e6f33e61020110e89864c26 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 22 Jan 2023 01:39:01 -0800 Subject: [PATCH 49/56] more robust ComponentParams tests --- CHANGELOG.md | 1 + tests/test_app/tests/test_database.py | 41 ++++++++++++++++----------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5457ba8..1907c5b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Using the following categories, list your changes in this order: ### Security - Fixed a potential method of component template tag argument spoofing. +- Exception information will no longer be displayed on the page, based on the value of `settings.py:DEBUG`. ## [2.2.1] - 2022-01-09 diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py index 25204c07..3709de08 100644 --- a/tests/test_app/tests/test_database.py +++ b/tests/test_app/tests/test_database.py @@ -1,3 +1,5 @@ +from time import sleep +from typing import Any from uuid import uuid4 import dill as pickle @@ -12,28 +14,33 @@ class DatabaseTests(TransactionTestCase): def test_component_params(self): # Make sure the ComponentParams table is empty self.assertEqual(ComponentParams.objects.count(), 0) - - # Add component params to the database - params = ComponentParamData((1,), {"test_value": 1}) - model = ComponentParams(uuid4().hex, data=pickle.dumps(params)) - model.full_clean() - model.save() + params_1 = self._save_params_to_db(1) # Check if a component params are in the database self.assertEqual(ComponentParams.objects.count(), 1) + self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params_1) # type: ignore - # Check if the data is the same - self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params) # type: ignore + # Force `params_1` to expire + from django_idom import config - # Check if the data is the same after a reload - ComponentParams.objects.first().refresh_from_db() # type: ignore - self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params) # type: ignore + config.IDOM_RECONNECT_MAX = 1 + sleep(config.IDOM_RECONNECT_MAX + 0.1) - # Force the data to expire - from django_idom import config + # Create a new, non-expired component params + params_2 = self._save_params_to_db(2) + self.assertEqual(ComponentParams.objects.count(), 2) - config.IDOM_RECONNECT_MAX = 0 - utils.db_cleanup() # Don't use `immediate` to better simulate a real world scenario + # Delete the first component params based on expiration time + utils.db_cleanup() # Don't use `immediate` to test cache timestamping logic - # Make sure the data is gone - self.assertEqual(ComponentParams.objects.count(), 0) + # Make sure `params_1` has expired + self.assertEqual(ComponentParams.objects.count(), 1) + self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params_2) # type: ignore + + def _save_params_to_db(self, value: Any) -> ComponentParamData: + param_data = ComponentParamData((value,), {"test_value": value}) + model = ComponentParams(uuid4().hex, data=pickle.dumps(param_data)) + model.full_clean() + model.save() + + return param_data From abc3439fcf31c4ed54bbc238a025211459a12009 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 22 Jan 2023 03:14:37 -0800 Subject: [PATCH 50/56] always lazy load config --- src/django_idom/components.py | 5 ++++- src/django_idom/http/views.py | 5 ++++- src/django_idom/types.py | 4 ++-- src/django_idom/websocket/consumer.py | 3 +-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 2733b614..fd9d5292 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -11,7 +11,6 @@ from idom import component, hooks, html, utils from idom.types import ComponentType, Key, VdomDict -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, render_view @@ -33,6 +32,8 @@ def _view_to_component( args: Sequence | None, kwargs: dict | None, ): + from django_idom.config import IDOM_VIEW_COMPONENT_IFRAMES + converted_view, set_converted_view = hooks.use_state( cast(Union[VdomDict, None], None) ) @@ -198,6 +199,8 @@ def django_js(static_path: str, key: Key | None = None): def _cached_static_contents(static_path: str): + from django_idom.config import IDOM_CACHE + # Try to find the file within Django's static files abs_path = find(static_path) if not abs_path: diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 4cf42b0a..d8858f4c 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -5,13 +5,14 @@ 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 create_cache_key, render_view async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: """Gets JavaScript required for IDOM modules at runtime. These modules are returned from cache if available.""" + from django_idom.config import IDOM_CACHE + web_modules_dir = IDOM_WED_MODULES_DIR.current path = os.path.abspath(web_modules_dir.joinpath(*file.split("/"))) @@ -40,6 +41,8 @@ async def view_to_component_iframe( ) -> HttpResponse: """Returns a view that was registered by view_to_component. This view is intended to be used as iframe, for compatibility purposes.""" + from django_idom.config import IDOM_VIEW_COMPONENT_IFRAMES + # Get the view from IDOM_REGISTERED_IFRAMES iframe = IDOM_VIEW_COMPONENT_IFRAMES.get(view_path) if not iframe: diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 98361f23..60547281 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -20,8 +20,6 @@ from idom.types import Connection as _Connection from typing_extensions import ParamSpec -from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR - __all__ = [ "_Result", @@ -90,6 +88,8 @@ def __call__(self, data: Any) -> Any: class QueryOptions: """Configuration options that can be provided to `use_query`.""" + from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR + postprocessor: Postprocessor | None = _DEFAULT_QUERY_POSTPROCESSOR """A callable that can modify the query `data` after the query has been executed. diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index e5bd985c..fa90ca48 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -16,7 +16,6 @@ from idom.core.layout import Layout, LayoutEvent from idom.core.serve import serve_json_patch -from django_idom.config import IDOM_REGISTERED_COMPONENTS from django_idom.types import ComponentParamData, ComponentWebsocket from django_idom.utils import db_cleanup, func_has_params @@ -56,7 +55,7 @@ async def receive_json(self, content: Any, **kwargs: Any) -> None: async def _run_dispatch_loop(self): from django_idom import models - from django_idom.config import IDOM_RECONNECT_MAX + from django_idom.config import IDOM_RECONNECT_MAX, IDOM_REGISTERED_COMPONENTS scope = self.scope dotted_path = scope["url_route"]["kwargs"]["dotted_path"] From d7809f968279889462ea97135fcc742d1167a64e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 22 Jan 2023 03:18:27 -0800 Subject: [PATCH 51/56] remove defaults.py --- src/django_idom/config.py | 10 ++++++++-- src/django_idom/defaults.py | 16 ---------------- src/django_idom/types.py | 4 ++-- 3 files changed, 10 insertions(+), 20 deletions(-) delete mode 100644 src/django_idom/defaults.py diff --git a/src/django_idom/config.py b/src/django_idom/config.py index 7931f12d..a7b4ca8d 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -7,8 +7,8 @@ from idom.config import IDOM_DEBUG_MODE from idom.core.types import ComponentConstructor -from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR from django_idom.types import Postprocessor, ViewComponentIframe +from django_idom.utils import import_dotted_path IDOM_DEBUG_MODE.set_current(getattr(settings, "DEBUG")) @@ -29,4 +29,10 @@ if "idom" in getattr(settings, "CACHES", {}) else caches[DEFAULT_CACHE_ALIAS] ) -IDOM_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = _DEFAULT_QUERY_POSTPROCESSOR +IDOM_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path( + getattr( + settings, + "IDOM_DEFAULT_QUERY_POSTPROCESSOR", + "django_idom.utils.django_query_postprocessor", + ) +) diff --git a/src/django_idom/defaults.py b/src/django_idom/defaults.py deleted file mode 100644 index 9e2f7582..00000000 --- a/src/django_idom/defaults.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import annotations - -from typing import Any, Callable - -from django.conf import settings - -from django_idom.utils import import_dotted_path - - -_DEFAULT_QUERY_POSTPROCESSOR: Callable[..., Any] | None = import_dotted_path( - getattr( - settings, - "IDOM_DEFAULT_QUERY_POSTPROCESSOR", - "django_idom.utils.django_query_postprocessor", - ) -) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 60547281..1427d62a 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -88,9 +88,9 @@ def __call__(self, data: Any) -> Any: class QueryOptions: """Configuration options that can be provided to `use_query`.""" - from django_idom.defaults import _DEFAULT_QUERY_POSTPROCESSOR + from django_idom.config import IDOM_DEFAULT_QUERY_POSTPROCESSOR - postprocessor: Postprocessor | None = _DEFAULT_QUERY_POSTPROCESSOR + postprocessor: Postprocessor | None = IDOM_DEFAULT_QUERY_POSTPROCESSOR """A callable that can modify the query `data` after the query has been executed. The first argument of postprocessor must be the query `data`. All proceeding arguments From a2a2c8cf0a3e67fe7b20222d8d6da30321e76ab7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 27 Jan 2023 16:10:28 -0800 Subject: [PATCH 52/56] remove version bump --- CHANGELOG.md | 7 +------ src/django_idom/__init__.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1907c5b7..9ac34c86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,6 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet) - -## [3.0.0] - 2022-01-14 - ### Added - The `idom` client will automatically configure itself to debug mode depending on `settings.py:DEBUG`. @@ -222,8 +218,7 @@ 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/3.0.0...HEAD -[3.0.0]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0 +[unreleased]: https://github.com/idom-team/django-idom/compare/2.2.1...HEAD [2.2.1]: https://github.com/idom-team/django-idom/compare/2.2.0...2.2.1 [2.2.0]: https://github.com/idom-team/django-idom/compare/2.1.0...2.2.0 [2.1.0]: https://github.com/idom-team/django-idom/compare/2.0.1...2.1.0 diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 6ab766f9..ca378289 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -2,7 +2,7 @@ from django_idom.websocket.paths import IDOM_WEBSOCKET_PATH -__version__ = "3.0.0" +__version__ = "2.2.1" __all__ = [ "IDOM_WEBSOCKET_PATH", "hooks", From 468d52f4216cd9997df5d7e0e6ccf88b6675c0ca Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 27 Jan 2023 16:28:40 -0800 Subject: [PATCH 53/56] fix type hint issues --- pyproject.toml | 1 + src/django_idom/utils.py | 2 +- tests/test_app/components.py | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8904b1b5..2bef7351 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,3 +21,4 @@ warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true check_untyped_defs = true +sqlite_cache = true diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index c2f4479b..c52c47fd 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -126,7 +126,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) # type: ignore + e.engine.get_template_loaders(e.engine.loaders) ) loaders = [] for loader in template_source_loaders: diff --git a/tests/test_app/components.py b/tests/test_app/components.py index bc8f56a7..38ba30a5 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -89,10 +89,10 @@ def use_connection(): ws = django_idom.hooks.use_connection() success = bool( ws.scope - and ws.location - and ws.carrier.close - and ws.carrier.disconnect - and ws.carrier.dotted_path + and getattr(ws, "location", None) + and getattr(ws.carrier, "close", None) + and getattr(ws.carrier, "disconnect", None) + and getattr(ws.carrier, "dotted_path", None) ) return html.div( {"id": "use-connection", "data-success": success}, From 0d9d4a9a3da4869cd7708bd59b325fc61b453eec Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 1 Feb 2023 15:12:39 -0800 Subject: [PATCH 54/56] change mypy to incremental --- pyproject.toml | 2 +- src/django_idom/utils.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2bef7351..c9321d8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,4 +21,4 @@ warn_unused_configs = true warn_redundant_casts = true warn_unused_ignores = true check_untyped_defs = true -sqlite_cache = true +incremental = false diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index c52c47fd..63c3bfab 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -330,6 +330,7 @@ def db_cleanup(immediate: bool = False): ) # Delete expired component parameters + # Use timestamps in cache (`cleaned_at_str`) as a no-dependency rate limiter if immediate or not cleaned_at_str or timezone.now() >= clean_needed_by: ComponentParams.objects.filter(last_accessed__lte=expires_by).delete() IDOM_CACHE.set(cache_key, now_str) From a0194c0cf1bb9fc264596e9752a032e690482d5c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 1 Feb 2023 15:18:46 -0800 Subject: [PATCH 55/56] format using new black version --- src/django_idom/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 63c3bfab..a0963b35 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -80,7 +80,8 @@ async def render_view( def _register_component(dotted_path: str) -> Callable: """Adds a component to the mapping of registered components. - This should only be called on startup to maintain synchronization during mulitprocessing.""" + This should only be called on startup to maintain synchronization during mulitprocessing. + """ from django_idom.config import IDOM_REGISTERED_COMPONENTS if dotted_path in IDOM_REGISTERED_COMPONENTS: @@ -107,7 +108,8 @@ def import_dotted_path(dotted_path: str) -> Callable: class ComponentPreloader: """Preloads all IDOM components found within Django templates. - This should only be `run` once on startup to maintain synchronization during mulitprocessing.""" + This should only be `run` once on startup to maintain synchronization during mulitprocessing. + """ def run(self): """Registers all IDOM components found within Django templates.""" From f5cc22910c35c319ddf10841a4364d055c4e08dd Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 1 Feb 2023 17:11:21 -0800 Subject: [PATCH 56/56] log when idom database is not ready at startup --- src/django_idom/apps.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/django_idom/apps.py b/src/django_idom/apps.py index 8c202f33..f91b50b7 100644 --- a/src/django_idom/apps.py +++ b/src/django_idom/apps.py @@ -1,4 +1,4 @@ -import contextlib +import logging from django.apps import AppConfig from django.db.utils import OperationalError @@ -6,6 +6,9 @@ from django_idom.utils import ComponentPreloader, db_cleanup +_logger = logging.getLogger(__name__) + + class DjangoIdomConfig(AppConfig): name = "django_idom" @@ -16,6 +19,8 @@ def ready(self): # Delete expired database entries # Suppress exceptions to avoid issues with `manage.py` commands such as # `test`, `migrate`, `makemigrations`, or any custom user created commands - # where the database may not be preconfigured. - with contextlib.suppress(OperationalError): + # where the database may not be ready. + try: db_cleanup(immediate=True) + except OperationalError: + _logger.debug("IDOM database was not ready at startup. Skipping cleanup...") pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy