From f0bdd8f71d80b1a1dfa23ae71daff618f62363e3 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 19 Feb 2024 23:59:23 -0800 Subject: [PATCH 01/15] make globals all caps in consumer --- src/reactpy_django/websocket/consumer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 195528ba..fca13a79 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -30,16 +30,16 @@ from django.contrib.auth.models import AbstractUser _logger = logging.getLogger(__name__) -backhaul_loop = asyncio.new_event_loop() +BACKHAUL_LOOP = asyncio.new_event_loop() def start_backhaul_loop(): """Starts the asyncio event loop that will perform component rendering tasks.""" - asyncio.set_event_loop(backhaul_loop) - backhaul_loop.run_forever() + asyncio.set_event_loop(BACKHAUL_LOOP) + BACKHAUL_LOOP.run_forever() -backhaul_thread = Thread( +BACKHAUL_THREAD = Thread( target=start_backhaul_loop, daemon=True, name="ReactPyBackhaul" ) @@ -83,13 +83,13 @@ async def connect(self) -> None: self.threaded = REACTPY_BACKHAUL_THREAD self.component_session: models.ComponentSession | None = None if self.threaded: - if not backhaul_thread.is_alive(): + if not BACKHAUL_THREAD.is_alive(): await asyncio.to_thread( _logger.debug, "Starting ReactPy backhaul thread." ) - backhaul_thread.start() + BACKHAUL_THREAD.start() self.dispatcher = asyncio.run_coroutine_threadsafe( - self.run_dispatcher(), backhaul_loop + self.run_dispatcher(), BACKHAUL_LOOP ) else: self.dispatcher = asyncio.create_task(self.run_dispatcher()) @@ -127,7 +127,7 @@ async def receive_json(self, content: Any, **_) -> None: """Receive a message from the browser. Typically, messages are event signals.""" if self.threaded: asyncio.run_coroutine_threadsafe( - self.recv_queue.put(content), backhaul_loop + self.recv_queue.put(content), BACKHAUL_LOOP ) else: await self.recv_queue.put(content) From fd13177022157d114378260b730b179024eac9ec Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:00:27 -0800 Subject: [PATCH 02/15] always propogate uuid via websocket URL --- src/reactpy_django/templatetags/reactpy.py | 4 +--- src/reactpy_django/websocket/paths.py | 10 ++-------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 8c175bc2..786e722d 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -159,9 +159,7 @@ def component( "reactpy_uuid": uuid, "reactpy_host": host or perceived_host, "reactpy_url_prefix": config.REACTPY_URL_PREFIX, - "reactpy_component_path": f"{dotted_path}/{uuid}/" - if component_has_args - else f"{dotted_path}/", + "reactpy_component_path": f"{dotted_path}/{uuid}/", "reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH, "reactpy_reconnect_interval": config.REACTPY_RECONNECT_INTERVAL, "reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL, diff --git a/src/reactpy_django/websocket/paths.py b/src/reactpy_django/websocket/paths.py index fa185565..6ddc5f95 100644 --- a/src/reactpy_django/websocket/paths.py +++ b/src/reactpy_django/websocket/paths.py @@ -1,4 +1,3 @@ -from channels.routing import URLRouter # noqa: E402 from django.urls import path from reactpy_django.config import REACTPY_URL_PREFIX @@ -6,13 +5,8 @@ from .consumer import ReactpyAsyncWebsocketConsumer REACTPY_WEBSOCKET_ROUTE = path( - f"{REACTPY_URL_PREFIX}//", - URLRouter( - [ - path("/", ReactpyAsyncWebsocketConsumer.as_asgi()), - path("", ReactpyAsyncWebsocketConsumer.as_asgi()), - ] - ), + f"{REACTPY_URL_PREFIX}///", + ReactpyAsyncWebsocketConsumer.as_asgi(), ) """A URL path for :class:`ReactpyAsyncWebsocketConsumer`. From 1c2607680ed166f5282c56bbf3ce81eddb099e9b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:09:40 -0800 Subject: [PATCH 03/15] embed has_args into the url --- src/reactpy_django/templatetags/reactpy.py | 6 +++--- src/reactpy_django/websocket/consumer.py | 3 ++- src/reactpy_django/websocket/paths.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 786e722d..2fd8f749 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -79,7 +79,7 @@ def component( is_local = not host or host.startswith(perceived_host) uuid = uuid4().hex class_ = kwargs.pop("class", "") - component_has_args = args or kwargs + has_args = bool(args or kwargs) user_component: ComponentConstructor | None = None _prerender_html = "" _offline_html = "" @@ -108,7 +108,7 @@ def component( return failure_context(dotted_path, e) # Store args & kwargs in the database (fetched by our websocket later) - if component_has_args: + if has_args: try: save_component_params(args, kwargs, uuid) except Exception as e: @@ -159,7 +159,7 @@ def component( "reactpy_uuid": uuid, "reactpy_host": host or perceived_host, "reactpy_url_prefix": config.REACTPY_URL_PREFIX, - "reactpy_component_path": f"{dotted_path}/{uuid}/", + "reactpy_component_path": f"{dotted_path}/{uuid}/{int(has_args)}/", "reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH, "reactpy_reconnect_interval": config.REACTPY_RECONNECT_INTERVAL, "reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL, diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index fca13a79..0b32671b 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -151,6 +151,7 @@ async def run_dispatcher(self): scope = self.scope self.dotted_path = dotted_path = scope["url_route"]["kwargs"]["dotted_path"] uuid = scope["url_route"]["kwargs"].get("uuid") + has_args = scope["url_route"]["kwargs"].get("has_args") query_string = parse_qs(scope["query_string"].decode(), strict_parsing=True) http_pathname = query_string.get("http_pathname", [""])[0] http_search = query_string.get("http_search", [""])[0] @@ -176,7 +177,7 @@ async def run_dispatcher(self): # Fetch the component's args/kwargs from the database, if needed try: - if uuid: + if has_args: # Get the component session from the DB self.component_session = await models.ComponentSession.objects.aget( uuid=uuid, diff --git a/src/reactpy_django/websocket/paths.py b/src/reactpy_django/websocket/paths.py index 6ddc5f95..17a9c48e 100644 --- a/src/reactpy_django/websocket/paths.py +++ b/src/reactpy_django/websocket/paths.py @@ -5,7 +5,7 @@ from .consumer import ReactpyAsyncWebsocketConsumer REACTPY_WEBSOCKET_ROUTE = path( - f"{REACTPY_URL_PREFIX}///", + f"{REACTPY_URL_PREFIX}////", ReactpyAsyncWebsocketConsumer.as_asgi(), ) """A URL path for :class:`ReactpyAsyncWebsocketConsumer`. From c234718a5dbaec0abf1262309a6cf16d3aeefa74 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:17:09 -0800 Subject: [PATCH 04/15] always propogate uuid via scope --- src/reactpy_django/templatetags/reactpy.py | 13 +++++++++---- src/reactpy_django/websocket/consumer.py | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 2fd8f749..f1705e83 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -135,7 +135,9 @@ def component( ) _logger.error(msg) return failure_context(dotted_path, ComponentCarrierError(msg)) - _prerender_html = prerender_component(user_component, args, kwargs, request) + _prerender_html = prerender_component( + user_component, args, kwargs, uuid, request + ) # Fetch the offline component's HTML, if requested if offline: @@ -151,7 +153,7 @@ def component( ) _logger.error(msg) return failure_context(dotted_path, ComponentCarrierError(msg)) - _offline_html = prerender_component(offline_component, [], {}, request) + _offline_html = prerender_component(offline_component, [], {}, uuid, request) # Return the template rendering context return { @@ -197,14 +199,17 @@ def validate_host(host: str): def prerender_component( - user_component: ComponentConstructor, args, kwargs, request: HttpRequest + user_component: ComponentConstructor, args, kwargs, uuid, request: HttpRequest ): search = request.GET.urlencode() + scope = getattr(request, "scope", {}) + scope["reactpy"] = {"uuid": uuid} + with SyncLayout( ConnectionContext( user_component(*args, **kwargs), value=Connection( - scope=getattr(request, "scope", {}), + scope=scope, location=Location( pathname=request.path, search=f"?{search}" if search else "" ), diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 0b32671b..02b9cfe5 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -152,6 +152,7 @@ async def run_dispatcher(self): self.dotted_path = dotted_path = scope["url_route"]["kwargs"]["dotted_path"] uuid = scope["url_route"]["kwargs"].get("uuid") has_args = scope["url_route"]["kwargs"].get("has_args") + scope["reactpy"] = {"uuid": uuid} query_string = parse_qs(scope["query_string"].decode(), strict_parsing=True) http_pathname = query_string.get("http_pathname", [""])[0] http_search = query_string.get("http_search", [""])[0] @@ -196,7 +197,7 @@ async def run_dispatcher(self): _logger.warning, f"Component session for '{dotted_path}:{uuid}' not found. The " "session may have already expired beyond REACTPY_SESSION_MAX_AGE. " - "If you are using a custom host, you may have forgotten to provide " + "If you are using a custom `host`, you may have forgotten to provide " "args/kwargs.", ) return From 490fe5419a1cc7869ce03a8ed73075995bcf2f78 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 01:04:20 -0800 Subject: [PATCH 05/15] fix URL routing schema --- src/reactpy_django/templatetags/reactpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index f1705e83..6d15b332 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -77,7 +77,7 @@ def component( or (next(config.REACTPY_DEFAULT_HOSTS) if config.REACTPY_DEFAULT_HOSTS else "") ).strip("/") is_local = not host or host.startswith(perceived_host) - uuid = uuid4().hex + uuid = str(uuid4()) class_ = kwargs.pop("class", "") has_args = bool(args or kwargs) user_component: ComponentConstructor | None = None From dc1e064f64f4974c4a37b0ca9e11f9f787200113 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 01:04:37 -0800 Subject: [PATCH 06/15] clean up tests --- tests/test_app/tests/test_database.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py index b8da31fd..6daa516f 100644 --- a/tests/test_app/tests/test_database.py +++ b/tests/test_app/tests/test_database.py @@ -63,7 +63,7 @@ def test_component_params(self): def _save_params_to_db(self, value: Any) -> ComponentParams: db = list(self.databases)[0] param_data = ComponentParams((value,), {"test_value": value}) - model = ComponentSession(uuid4().hex, params=pickle.dumps(param_data)) + model = ComponentSession(str(uuid4()), params=pickle.dumps(param_data)) model.clean_fields() model.clean() model.save(using=db) @@ -82,12 +82,12 @@ def test_user_data_cleanup(self): from django.contrib.auth.models import User # Create UserData for real user #1 - user = User.objects.create_user(username=uuid4().hex, password=uuid4().hex) + user = User.objects.create_user(username=str(uuid4()), password=str(uuid4())) user_data = UserDataModel(user_pk=user.pk) user_data.save() # Create UserData for real user #2 - user = User.objects.create_user(username=uuid4().hex, password=uuid4().hex) + user = User.objects.create_user(username=str(uuid4()), password=str(uuid4())) user_data = UserDataModel(user_pk=user.pk) user_data.save() @@ -95,7 +95,7 @@ def test_user_data_cleanup(self): initial_count = UserDataModel.objects.count() # Create UserData for a user that doesn't exist (effectively orphaned) - user_data = UserDataModel(user_pk=uuid4().hex) + user_data = UserDataModel(user_pk=str(uuid4())) user_data.save() # Make sure the orphaned user data object is deleted From b8526b7ec2fea2c8eaec29d9a2833eabb0e9bdf6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 01:53:50 -0800 Subject: [PATCH 07/15] use_root_id hook --- src/reactpy_django/hooks.py | 8 ++++++++ src/reactpy_django/templatetags/reactpy.py | 2 +- src/reactpy_django/websocket/consumer.py | 2 +- tests/test_app/prerender/components.py | 17 +++++++++++++++++ tests/test_app/templates/prerender.html | 2 ++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index 448d43b9..1b09a4b0 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -436,6 +436,14 @@ async def message_sender(message: dict): return message_sender +def use_root_id() -> str: + """Get the root element's ID. This value is guaranteed to be unique. Current versions of \ + ReactPy-Django use a UUID4 string.""" + scope = use_scope() + + return scope["reactpy"]["id"] + + def _use_query_args_1(options: QueryOptions, /, query: Query, *args, **kwargs): return options, query, args, kwargs diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 6d15b332..396c850d 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -203,7 +203,7 @@ def prerender_component( ): search = request.GET.urlencode() scope = getattr(request, "scope", {}) - scope["reactpy"] = {"uuid": uuid} + scope["reactpy"] = {"id": str(uuid)} with SyncLayout( ConnectionContext( diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 02b9cfe5..1ee33af0 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -152,7 +152,7 @@ async def run_dispatcher(self): self.dotted_path = dotted_path = scope["url_route"]["kwargs"]["dotted_path"] uuid = scope["url_route"]["kwargs"].get("uuid") has_args = scope["url_route"]["kwargs"].get("has_args") - scope["reactpy"] = {"uuid": uuid} + scope["reactpy"] = {"id": str(uuid)} query_string = parse_qs(scope["query_string"].decode(), strict_parsing=True) http_pathname = query_string.get("http_pathname", [""])[0] http_search = query_string.get("http_search", [""])[0] diff --git a/tests/test_app/prerender/components.py b/tests/test_app/prerender/components.py index 1dc013f3..93a25902 100644 --- a/tests/test_app/prerender/components.py +++ b/tests/test_app/prerender/components.py @@ -54,3 +54,20 @@ def use_user(): return html.div( {"id": "use-user-ws", "data-success": success}, f"use_user: {user} (WebSocket)" ) + + +@component +def use_root_id(): + scope = reactpy_django.hooks.use_scope() + root_id = reactpy_django.hooks.use_root_id() + + if scope.get("type") == "http": + return html.div( + {"id": "use-root-id-http", "data-root-id": root_id}, + f"use_root_id: {root_id} (HTTP)", + ) + + return html.div( + {"id": "use-root-id-ws", "data-root-id": root_id}, + f"use_root_id: {root_id} (WebSocket)", + ) diff --git a/tests/test_app/templates/prerender.html b/tests/test_app/templates/prerender.html index ed571554..dab4ba01 100644 --- a/tests/test_app/templates/prerender.html +++ b/tests/test_app/templates/prerender.html @@ -27,6 +27,8 @@

ReactPy Prerender Test Page


{% component "test_app.prerender.components.use_user" prerender="true" %}
+ {% component "test_app.prerender.components.use_root_id" prerender="true" %} +
From 37082698ce6e891fd46ed932f33cead1f508815f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:22:56 -0800 Subject: [PATCH 08/15] docstrings --- src/reactpy_django/models.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/reactpy_django/models.py b/src/reactpy_django/models.py index 7c413b76..212a572c 100644 --- a/src/reactpy_django/models.py +++ b/src/reactpy_django/models.py @@ -7,9 +7,7 @@ class ComponentSession(models.Model): - """A model for storing component sessions. - All queries must be routed through `reactpy_django.config.REACTPY_DATABASE`. - """ + """A model for storing component sessions.""" uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore params = models.BinaryField(editable=False) # type: ignore @@ -36,15 +34,15 @@ class UserDataModel(models.Model): """A model for storing `user_state` data.""" # We can't store User as a ForeignKey/OneToOneField because it may not be in the same database - # and Django does not allow cross-database relations. Also, we can't know the type of the UserModel PK, - # so we store it as a string. + # and Django does not allow cross-database relations. Also, since we can't know the type of the UserModel PK, + # we store it as a string to normalize. user_pk = models.CharField(max_length=255, unique=True) # type: ignore data = models.BinaryField(null=True, blank=True) # type: ignore @receiver(pre_delete, sender=get_user_model(), dispatch_uid="reactpy_delete_user_data") def delete_user_data(sender, instance, **kwargs): - """Delete `UserDataModel` when the `User` is deleted.""" + """Delete ReactPy's `UserDataModel` when a Django `User` is deleted.""" pk = getattr(instance, instance._meta.pk.name) with contextlib.suppress(Exception): From 7cd23b1119fc1bf083856eb1384c62209629741f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:46:23 -0800 Subject: [PATCH 09/15] simplier delete_user_data --- CHANGELOG.md | 4 ++++ src/reactpy_django/models.py | 5 +---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a63334..45a4821b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,10 @@ Using the following categories, list your changes in this order: - `settings.py:REACTPY_CLEAN_USER_DATA` to control whether ReactPy automatically cleans up orphaned user data. - `python manage.py clean_reactpy` command to manually perform ReactPy clean up tasks. +### Changed + +- Simplified code for casading deletion of UserData. + ## [3.7.0] - 2024-01-30 ### Added diff --git a/src/reactpy_django/models.py b/src/reactpy_django/models.py index 212a572c..180b0b31 100644 --- a/src/reactpy_django/models.py +++ b/src/reactpy_django/models.py @@ -1,5 +1,3 @@ -import contextlib - from django.contrib.auth import get_user_model from django.db import models from django.db.models.signals import pre_delete @@ -45,5 +43,4 @@ def delete_user_data(sender, instance, **kwargs): """Delete ReactPy's `UserDataModel` when a Django `User` is deleted.""" pk = getattr(instance, instance._meta.pk.name) - with contextlib.suppress(Exception): - UserDataModel.objects.get(user_pk=pk).delete() + UserDataModel.objects.filter(user_pk=pk).delete() From ce449f8f4c9cb879647983421cacfc088922f2bf Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:48:15 -0800 Subject: [PATCH 10/15] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a4821b..0947a903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Using the following categories, list your changes in this order: ### Added - Built-in cross-process communication mechanism via the `reactpy_django.hooks.use_channel_layer` hook. +- Access to the root component's `id` via the `reactpy_django.hooks.use_root_id` hook. - More robust control over ReactPy clean up tasks! - `settings.py:REACTPY_CLEAN_INTERVAL` to control how often ReactPy automatically performs cleaning tasks. - `settings.py:REACTPY_CLEAN_SESSIONS` to control whether ReactPy automatically cleans up expired sessions. From 34df38d5b3fb7f1595d95c3220903e819a28a6c1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:59:54 -0800 Subject: [PATCH 11/15] add docs --- CHANGELOG.md | 2 +- docs/examples/python/use-root-id.py | 9 +++++++++ docs/src/reference/hooks.md | 30 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 docs/examples/python/use-root-id.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0947a903..5aa0b819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,7 @@ Using the following categories, list your changes in this order: ### Changed -- Simplified code for casading deletion of UserData. +- Simplified code for cascading deletion of UserData. ## [3.7.0] - 2024-01-30 diff --git a/docs/examples/python/use-root-id.py b/docs/examples/python/use-root-id.py new file mode 100644 index 00000000..f2088cc4 --- /dev/null +++ b/docs/examples/python/use-root-id.py @@ -0,0 +1,9 @@ +from reactpy import component, html +from reactpy_django.hooks import use_root_id + + +@component +def my_component(): + root_id = use_root_id() + + return html.div(f"Root ID: {root_id}") diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index 5ceb2fe6..bd96b6a1 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -492,6 +492,8 @@ Shortcut that returns the browser's current `#!python Location`. | --- | --- | | `#!python Location` | An object containing the current URL's `#!python pathname` and `#!python search` query. | +--- + ### Use Origin Shortcut that returns the WebSocket or HTTP connection's `#!python origin`. @@ -518,6 +520,34 @@ You can expect this hook to provide strings such as `http://example.com`. --- +### Use Root ID + +Shortcut that returns the root component's `#!python id` from the WebSocket or HTTP connection. + +The root ID is currently a randomly generated `#!python uuid4` (unique across all root component). + +This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance, and retain a backlog of messages in case that component is disconnected via `#!python group_discard=False`. + +=== "components.py" + + ```python + {% include "../../examples/python/use-root-id.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python str` | A string containing the root component's `#!python id`. | + +--- + ### Use User Shortcut that returns the WebSocket or HTTP connection's `#!python User`. From 08d6efd559412655fcbfc2a52dc48a9e2ce0014f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:05:58 -0800 Subject: [PATCH 12/15] upd docs --- docs/src/reference/hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index bd96b6a1..d715874e 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -526,7 +526,7 @@ Shortcut that returns the root component's `#!python id` from the WebSocket or H The root ID is currently a randomly generated `#!python uuid4` (unique across all root component). -This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance, and retain a backlog of messages in case that component is disconnected via `#!python group_discard=False`. +This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance, and/or retain a backlog of messages in case that component is disconnected via `#!python use_channel_layer( ... , group_discard=False)`. === "components.py" From 8642a0e0427d1f625c97b55ab56fbb83497ca693 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:15:15 -0800 Subject: [PATCH 13/15] Add tests --- tests/test_app/prerender/components.py | 4 ++-- tests/test_app/tests/test_components.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_app/prerender/components.py b/tests/test_app/prerender/components.py index 93a25902..dd312195 100644 --- a/tests/test_app/prerender/components.py +++ b/tests/test_app/prerender/components.py @@ -63,11 +63,11 @@ def use_root_id(): if scope.get("type") == "http": return html.div( - {"id": "use-root-id-http", "data-root-id": root_id}, + {"id": "use-root-id-http", "data-value": root_id}, f"use_root_id: {root_id} (HTTP)", ) return html.div( - {"id": "use-root-id-ws", "data-root-id": root_id}, + {"id": "use-root-id-ws", "data-value": root_id}, f"use_root_id: {root_id} (WebSocket)", ) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 241e5659..c7a0d2fd 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -399,12 +399,15 @@ def test_prerender(self): string = new_page.locator("#prerender_string") vdom = new_page.locator("#prerender_vdom") component = new_page.locator("#prerender_component") + use_root_id_http = new_page.locator("#use-root-id-http") + use_root_id_ws = new_page.locator("#use-root-id-ws") use_user_http = new_page.locator("#use-user-http[data-success=True]") use_user_ws = new_page.locator("#use-user-ws[data-success=true]") string.wait_for() vdom.wait_for() component.wait_for() + use_root_id_http.wait_for() use_user_http.wait_for() # Check if the prerender occurred @@ -415,7 +418,10 @@ def test_prerender(self): self.assertEqual( component.all_inner_texts(), ["prerender_component: Prerendered"] ) + root_id_value = use_root_id_http.get_attribute("data-value") + self.assertEqual(len(root_id_value), 36) + # Check if the full render occurred sleep(1) self.assertEqual( string.all_inner_texts(), ["prerender_string: Fully Rendered"] @@ -424,7 +430,10 @@ def test_prerender(self): self.assertEqual( component.all_inner_texts(), ["prerender_component: Fully Rendered"] ) + use_root_id_ws.wait_for() use_user_ws.wait_for() + self.assertEqual(use_root_id_ws.get_attribute("data-value"), root_id_value) + finally: new_page.close() From cfb933c6ca225459fc85348876309b09d1f02e92 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:26:34 -0800 Subject: [PATCH 14/15] docstrings --- src/reactpy_django/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index 1b09a4b0..d2574751 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -438,7 +438,7 @@ async def message_sender(message: dict): def use_root_id() -> str: """Get the root element's ID. This value is guaranteed to be unique. Current versions of \ - ReactPy-Django use a UUID4 string.""" + ReactPy-Django return a `uuid4` string.""" scope = use_scope() return scope["reactpy"]["id"] From 1d565652207920729a4ca57956dc9b2d42028745 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:28:23 -0800 Subject: [PATCH 15/15] minor consumer refactoring --- src/reactpy_django/websocket/consumer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 1ee33af0..0d4c62d1 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -168,7 +168,7 @@ async def run_dispatcher(self): # Verify the component has already been registered try: - component_constructor = REACTPY_REGISTERED_COMPONENTS[dotted_path] + root_component_constructor = REACTPY_REGISTERED_COMPONENTS[dotted_path] except KeyError: await asyncio.to_thread( _logger.warning, @@ -176,10 +176,9 @@ async def run_dispatcher(self): ) return - # Fetch the component's args/kwargs from the database, if needed + # Construct the component. This may require fetching the component's args/kwargs from the database. try: if has_args: - # Get the component session from the DB self.component_session = await models.ComponentSession.objects.aget( uuid=uuid, last_accessed__gt=now - timedelta(seconds=REACTPY_SESSION_MAX_AGE), @@ -189,7 +188,7 @@ async def run_dispatcher(self): component_session_kwargs = params.kwargs # Generate the initial component instance - component_instance = component_constructor( + root_component = root_component_constructor( *component_session_args, **component_session_kwargs ) except models.ComponentSession.DoesNotExist: @@ -204,7 +203,7 @@ async def run_dispatcher(self): except Exception: await asyncio.to_thread( _logger.error, - f"Failed to construct component {component_constructor} " + f"Failed to construct component {root_component_constructor} " f"with args='{component_session_args}' kwargs='{component_session_kwargs}'!\n" f"{traceback.format_exc()}", ) @@ -213,7 +212,7 @@ async def run_dispatcher(self): # Start the ReactPy component rendering loop with contextlib.suppress(Exception): await serve_layout( - Layout(ConnectionContext(component_instance, value=connection)), + Layout(ConnectionContext(root_component, value=connection)), self.send_json, self.recv_queue.get, )