From ec00e0081dbfffe0e51a41cdb9002b1d90a23e57 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:59:13 -0800 Subject: [PATCH 01/11] 3.0.0a3 --- CHANGELOG.md | 6 +++--- src/django_idom/__init__.py | 2 +- src/django_idom/apps.py | 8 +++++--- src/django_idom/hooks.py | 10 +++++++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4546daec..4c7ac15b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ Using the following categories, list your changes in this order: - Nothing (yet) -## [3.0.0a2] - 2023-02-02 +## [3.0.0a3] - 2023-02-02 ???+ note @@ -246,8 +246,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/3.0.0a2...HEAD -[3.0.0a2]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0a2 +[unreleased]: https://github.com/idom-team/django-idom/compare/3.0.0a3...HEAD +[3.0.0a3]: https://github.com/idom-team/django-idom/compare/2.2.1...3.0.0a3 [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 2856d386..7cfa23a9 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.0a2" +__version__ = "3.0.0a3" __all__ = [ "IDOM_WEBSOCKET_PATH", "hooks", diff --git a/src/django_idom/apps.py b/src/django_idom/apps.py index f91b50b7..bde9fb98 100644 --- a/src/django_idom/apps.py +++ b/src/django_idom/apps.py @@ -1,7 +1,7 @@ import logging from django.apps import AppConfig -from django.db.utils import OperationalError +from django.db.utils import DatabaseError from django_idom.utils import ComponentPreloader, db_cleanup @@ -22,5 +22,7 @@ def ready(self): # 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...") + except DatabaseError: + _logger.debug( + "Could not access IDOM database at startup. Skipping cleanup..." + ) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index dd17ca16..ad40d301 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -7,7 +7,6 @@ Awaitable, Callable, DefaultDict, - MutableMapping, Sequence, Union, cast, @@ -65,9 +64,14 @@ def use_origin() -> str | None: return None -def use_scope() -> MutableMapping[str, Any]: +def use_scope() -> dict[str, Any]: """Get the current ASGI scope dictionary""" - return _use_scope() + scope = _use_scope() + + if isinstance(scope, dict): + return scope + + raise TypeError(f"Expected scope to be a dict, got {type(scope)}") def use_connection() -> Connection: From 3877e8d2f582e8283b942ac0b1b6f7a0bf1b03b3 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 21 Feb 2023 01:52:15 -0800 Subject: [PATCH 02/11] IDOM_DATABASE param --- CHANGELOG.md | 2 +- docs/python/settings.py | 3 +++ src/django_idom/config.py | 6 ++++++ src/django_idom/models.py | 4 ++++ src/django_idom/utils.py | 8 +++++--- src/django_idom/websocket/consumer.py | 10 ++++++++-- 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7ac15b..4bdc63d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ Using the following categories, list your changes in this order: ### Fixed -- `view_to_component` will now retain any HTML that was defined in a `
` tag. +- `view_to_component` will now retain the contents of a `` tag when rendering. - React client is now set to `production` rather than `development`. - `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships diff --git a/docs/python/settings.py b/docs/python/settings.py index f7ee0f15..d8573d22 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -4,6 +4,9 @@ "idom": {"BACKEND": ...}, } +# IDOM works best with a multiprocessing-safe and thread-safe database backend. +IDOM_DATABASE = "default" + # Maximum seconds between reconnection attempts before giving up. # Use `0` to prevent component reconnection. IDOM_RECONNECT_MAX = 259200 diff --git a/src/django_idom/config.py b/src/django_idom/config.py index a7b4ca8d..37292de9 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -4,6 +4,7 @@ from django.conf import settings from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches +from django.db import DEFAULT_DB_ALIAS from idom.config import IDOM_DEBUG_MODE from idom.core.types import ComponentConstructor @@ -29,6 +30,11 @@ if "idom" in getattr(settings, "CACHES", {}) else caches[DEFAULT_CACHE_ALIAS] ) +IDOM_DATABASE: str = getattr( + settings, + "IDOM_DATABASE", + DEFAULT_DB_ALIAS, +) IDOM_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path( getattr( settings, diff --git a/src/django_idom/models.py b/src/django_idom/models.py index 1e67f368..ad0e4aa8 100644 --- a/src/django_idom/models.py +++ b/src/django_idom/models.py @@ -2,6 +2,10 @@ class ComponentParams(models.Model): + """A model for storing component parameters. + All queries must be routed through `django_idom.config.IDOM_DATABASE`. + """ + uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore data = models.BinaryField(editable=False) # type: ignore last_accessed = models.DateTimeField(auto_now_add=True) # type: ignore diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index a0963b35..28caf741 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -311,7 +311,7 @@ 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, IDOM_DATABASE, IDOM_RECONNECT_MAX from .models import ComponentParams cache_key: str = create_cache_key("last_cleaned") @@ -324,7 +324,7 @@ def db_cleanup(immediate: bool = False): 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 cleaned_at_str and ComponentParams.objects.all(): + if not cleaned_at_str and ComponentParams.objects.using(IDOM_DATABASE).all(): _logger.warning( "IDOM has detected component sessions in the database, " "but no timestamp was found in cache. This may indicate that " @@ -334,5 +334,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() + ComponentParams.objects.using(IDOM_DATABASE).filter( + last_accessed__lte=expires_by + ).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 99f860ba..864942a1 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -55,7 +55,11 @@ async def receive_json(self, content: Any, **_) -> None: async def _run_dispatch_loop(self): from django_idom import models - from django_idom.config import IDOM_RECONNECT_MAX, IDOM_REGISTERED_COMPONENTS + from django_idom.config import ( + IDOM_DATABASE, + IDOM_RECONNECT_MAX, + IDOM_REGISTERED_COMPONENTS, + ) scope = self.scope dotted_path = scope["url_route"]["kwargs"]["dotted_path"] @@ -91,7 +95,9 @@ async def _run_dispatch_loop(self): await convert_to_async(db_cleanup)() # Get the queries from a DB - params_query = await models.ComponentParams.objects.aget( + params_query = await models.ComponentParams.objects.using( + IDOM_DATABASE + ).aget( uuid=uuid, last_accessed__gt=now - timedelta(seconds=IDOM_RECONNECT_MAX), ) From 811ad8418a4cfcf944c579b560bdf4df080b55fa Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 21 Feb 2023 02:46:14 -0800 Subject: [PATCH 03/11] IDOM_CACHE settings param --- CHANGELOG.md | 8 +++++--- docs/python/settings.py | 5 +---- src/django_idom/components.py | 7 ++++--- src/django_idom/config.py | 10 +++++----- src/django_idom/http/views.py | 7 ++++--- src/django_idom/utils.py | 5 +++-- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bdc63d8..d9d106d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,8 @@ Using the following categories, list your changes in this order: ### Added - 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` +- `use_connection` hook for returning the browser's active `Connection`. +- `IDOM_CACHE` is now configurable within `settings.py` to whatever cache name you wish. ### Changed @@ -66,13 +67,14 @@ Using the following categories, list your changes in this order: ### Removed - `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` +- `django_idom.types.IdomWebsocket` has been removed. The similar replacement is `django_idom.types.Connection`. +- `settings.py:CACHE['idom']` is no longer used by default. The name of the cache backend must now be specified with the `IDOM_CACHE` setting. ### Fixed - `view_to_component` will now retain the contents of a `` tag when rendering. - React client is now set to `production` rather than `development`. -- `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships +- `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships. ### Security diff --git a/docs/python/settings.py b/docs/python/settings.py index d8573d22..50216575 100644 --- a/docs/python/settings.py +++ b/docs/python/settings.py @@ -1,8 +1,5 @@ -# If "idom" cache is not configured, then "default" will be used # IDOM works best with a multiprocessing-safe and thread-safe cache backend. -CACHES = { - "idom": {"BACKEND": ...}, -} +IDOM_CACHE = "default" # IDOM works best with a multiprocessing-safe and thread-safe database backend. IDOM_DATABASE = "default" diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 4448371f..47894321 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -5,6 +5,7 @@ from typing import Any, Callable, Protocol, Sequence, Union, cast, overload from django.contrib.staticfiles.finders import find +from django.core.cache import caches from django.http import HttpRequest from django.urls import reverse from django.views import View @@ -209,12 +210,12 @@ def _cached_static_contents(static_path: str): # Cache is preferrable to `use_memo` due to multiprocessing capabilities last_modified_time = os.stat(abs_path).st_mtime cache_key = f"django_idom:static_contents:{static_path}" - file_contents = IDOM_CACHE.get(cache_key, version=int(last_modified_time)) + file_contents = caches[IDOM_CACHE].get(cache_key, version=int(last_modified_time)) if file_contents is None: with open(abs_path, encoding="utf-8") as static_file: file_contents = static_file.read() - IDOM_CACHE.delete(cache_key) - IDOM_CACHE.set( + caches[IDOM_CACHE].delete(cache_key) + caches[IDOM_CACHE].set( cache_key, file_contents, timeout=None, version=int(last_modified_time) ) diff --git a/src/django_idom/config.py b/src/django_idom/config.py index 37292de9..b276b006 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -3,7 +3,7 @@ from typing import Dict from django.conf import settings -from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches +from django.core.cache import DEFAULT_CACHE_ALIAS from django.db import DEFAULT_DB_ALIAS from idom.config import IDOM_DEBUG_MODE from idom.core.types import ComponentConstructor @@ -25,10 +25,10 @@ "IDOM_RECONNECT_MAX", 259200, # Default to 3 days ) -IDOM_CACHE: BaseCache = ( - caches["idom"] - if "idom" in getattr(settings, "CACHES", {}) - else caches[DEFAULT_CACHE_ALIAS] +IDOM_CACHE: str = getattr( + settings, + "IDOM_CACHE", + DEFAULT_CACHE_ALIAS, ) IDOM_DATABASE: str = getattr( settings, diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 5fffde7e..384c4740 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -1,6 +1,7 @@ import os from aiofile import async_open +from django.core.cache import caches from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest, HttpResponse, HttpResponseNotFound from idom.config import IDOM_WEB_MODULES_DIR @@ -25,12 +26,12 @@ 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))) - response = await IDOM_CACHE.aget(cache_key, version=int(last_modified_time)) + response = await caches[IDOM_CACHE].aget(cache_key, version=int(last_modified_time)) if response is None: async with async_open(path, "r") as fp: response = HttpResponse(await fp.read(), content_type="text/javascript") - await IDOM_CACHE.adelete(cache_key) - await IDOM_CACHE.aset( + await caches[IDOM_CACHE].adelete(cache_key) + await caches[IDOM_CACHE].aset( cache_key, response, timeout=None, version=int(last_modified_time) ) return response diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 28caf741..0d4be7bc 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -12,6 +12,7 @@ from typing import Any, Callable, Sequence from channels.db import database_sync_to_async +from django.core.cache import caches from django.db.models import ManyToManyField, prefetch_related_objects from django.db.models.base import Model from django.db.models.fields.reverse_related import ManyToOneRel @@ -316,7 +317,7 @@ def db_cleanup(immediate: bool = False): cache_key: str = create_cache_key("last_cleaned") now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT) - cleaned_at_str: str = IDOM_CACHE.get(cache_key) + cleaned_at_str: str = caches[IDOM_CACHE].get(cache_key) cleaned_at: datetime = timezone.make_aware( datetime.strptime(cleaned_at_str or now_str, DATE_FORMAT) ) @@ -337,4 +338,4 @@ def db_cleanup(immediate: bool = False): ComponentParams.objects.using(IDOM_DATABASE).filter( last_accessed__lte=expires_by ).delete() - IDOM_CACHE.set(cache_key, now_str) + caches[IDOM_CACHE].set(cache_key, now_str) From c664863be0992148762ee03fa268c59af8bdae95 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 21 Feb 2023 03:09:46 -0800 Subject: [PATCH 04/11] ComponentParams -> ComponentSession --- ...componentsession_delete_componentparams.py | 28 +++++++++++++++++++ src/django_idom/models.py | 4 +-- src/django_idom/templatetags/idom.py | 2 +- src/django_idom/utils.py | 6 ++-- src/django_idom/websocket/consumer.py | 6 ++-- tests/test_app/admin.py | 4 +-- tests/test_app/tests/test_database.py | 16 +++++------ 7 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 src/django_idom/migrations/0003_componentsession_delete_componentparams.py diff --git a/src/django_idom/migrations/0003_componentsession_delete_componentparams.py b/src/django_idom/migrations/0003_componentsession_delete_componentparams.py new file mode 100644 index 00000000..7a5c98e6 --- /dev/null +++ b/src/django_idom/migrations/0003_componentsession_delete_componentparams.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.6 on 2023-02-21 10:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("django_idom", "0002_rename_created_at_componentparams_last_accessed"), + ] + + operations = [ + migrations.CreateModel( + name="ComponentSession", + fields=[ + ( + "uuid", + models.UUIDField( + editable=False, primary_key=True, serialize=False, unique=True + ), + ), + ("params", models.BinaryField()), + ("last_accessed", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.DeleteModel( + name="ComponentParams", + ), + ] diff --git a/src/django_idom/models.py b/src/django_idom/models.py index ad0e4aa8..219866f2 100644 --- a/src/django_idom/models.py +++ b/src/django_idom/models.py @@ -1,11 +1,11 @@ from django.db import models -class ComponentParams(models.Model): +class ComponentSession(models.Model): """A model for storing component parameters. All queries must be routed through `django_idom.config.IDOM_DATABASE`. """ uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore - data = models.BinaryField(editable=False) # type: ignore + params = models.BinaryField(editable=False) # type: ignore last_accessed = models.DateTimeField(auto_now_add=True) # type: ignore diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index 4309177b..da13b352 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -45,7 +45,7 @@ def component(dotted_path: str, *args, **kwargs): try: if func_has_params(component, *args, **kwargs): params = ComponentParamData(args, kwargs) - model = models.ComponentParams(uuid=uuid, data=pickle.dumps(params)) + model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params)) model.full_clean() model.save() except TypeError as e: diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 0d4be7bc..c360bddd 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -313,7 +313,7 @@ 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_DATABASE, IDOM_RECONNECT_MAX - from .models import ComponentParams + from .models import ComponentSession cache_key: str = create_cache_key("last_cleaned") now_str: str = datetime.strftime(timezone.now(), DATE_FORMAT) @@ -325,7 +325,7 @@ def db_cleanup(immediate: bool = False): 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 cleaned_at_str and ComponentParams.objects.using(IDOM_DATABASE).all(): + if not cleaned_at_str and ComponentSession.objects.using(IDOM_DATABASE).all(): _logger.warning( "IDOM has detected component sessions in the database, " "but no timestamp was found in cache. This may indicate that " @@ -335,7 +335,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.using(IDOM_DATABASE).filter( + ComponentSession.objects.using(IDOM_DATABASE).filter( last_accessed__lte=expires_by ).delete() caches[IDOM_CACHE].set(cache_key, now_str) diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index 864942a1..e0cf56c4 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -95,7 +95,7 @@ async def _run_dispatch_loop(self): await convert_to_async(db_cleanup)() # Get the queries from a DB - params_query = await models.ComponentParams.objects.using( + params_query = await models.ComponentSession.objects.using( IDOM_DATABASE ).aget( uuid=uuid, @@ -103,14 +103,14 @@ async def _run_dispatch_loop(self): ) params_query.last_accessed = timezone.now() await convert_to_async(params_query.save)() - except models.ComponentParams.DoesNotExist: + except models.ComponentSession.DoesNotExist: _logger.warning( f"Browser has attempted to access '{dotted_path}', " 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) + component_params: ComponentParamData = pickle.loads(params_query.params) 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 e0701a96..0ae73f9f 100644 --- a/tests/test_app/admin.py +++ b/tests/test_app/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from test_app.models import ForiegnChild, RelationalChild, RelationalParent, TodoItem -from django_idom.models import ComponentParams +from django_idom.models import ComponentSession @admin.register(TodoItem) @@ -24,6 +24,6 @@ class ForiegnChildAdmin(admin.ModelAdmin): pass -@admin.register(ComponentParams) +@admin.register(ComponentSession) class ComponentParamsAdmin(admin.ModelAdmin): list_display = ("uuid", "last_accessed") diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py index 3709de08..6a86c9b4 100644 --- a/tests/test_app/tests/test_database.py +++ b/tests/test_app/tests/test_database.py @@ -6,19 +6,19 @@ from django.test import TransactionTestCase from django_idom import utils -from django_idom.models import ComponentParams +from django_idom.models import ComponentSession 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) + self.assertEqual(ComponentSession.objects.count(), 0) 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 + self.assertEqual(ComponentSession.objects.count(), 1) + self.assertEqual(pickle.loads(ComponentSession.objects.first().params), params_1) # type: ignore # Force `params_1` to expire from django_idom import config @@ -28,18 +28,18 @@ def test_component_params(self): # Create a new, non-expired component params params_2 = self._save_params_to_db(2) - self.assertEqual(ComponentParams.objects.count(), 2) + self.assertEqual(ComponentSession.objects.count(), 2) # Delete the first component params based on expiration time utils.db_cleanup() # Don't use `immediate` to test cache timestamping logic # Make sure `params_1` has expired - self.assertEqual(ComponentParams.objects.count(), 1) - self.assertEqual(pickle.loads(ComponentParams.objects.first().data), params_2) # type: ignore + self.assertEqual(ComponentSession.objects.count(), 1) + self.assertEqual(pickle.loads(ComponentSession.objects.first().params), 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 = ComponentSession(uuid4().hex, params=pickle.dumps(param_data)) model.full_clean() model.save() From e575fc97d2ecfcaef4a06689ec649c16c03979b0 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 21 Feb 2023 03:29:24 -0800 Subject: [PATCH 05/11] Undo `idom.html` API changes --- src/django_idom/components.py | 5 +- tests/test_app/components.py | 116 +++++++++++++++++++++------------- 2 files changed, 77 insertions(+), 44 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 47894321..c57f87ae 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -77,7 +77,10 @@ async def async_render(): view, _args, _kwargs ) return html.iframe( - src=reverse("idom:view_to_component", args=[dotted_path]), loading="lazy" + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } ) # Return the view if it's been rendered via the `async_render` hook diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 042b9aa7..38ba30a5 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -15,7 +15,7 @@ @component def hello_world(): - return html._(html.div("Hello World!", id="hello-world"), html.hr()) + return html._(html.div({"id": "hello-world"}, "Hello World!"), html.hr()) @component @@ -25,11 +25,13 @@ def button(): html.div( "button:", html.button( + {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)}, "Click me!", - id="counter-inc", - on_click=lambda event: set_count(count + 1), ), - html.p(f"Current count is: {count}", id="counter-num", data_count=count), + html.p( + {"id": "counter-num", "data-count": count}, + f"Current count is: {count}", + ), ), html.hr(), ) @@ -40,9 +42,8 @@ def parameterized_component(x, y): total = x + y return html._( html.div( + {"id": "parametrized-component", "data-value": total}, f"parameterized_component: {total}", - id="parametrized-component", - data_value=total, ), html.hr(), ) @@ -53,7 +54,14 @@ 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(f"{co_name}: ", str(my_object), id=co_name, data_success=success), + html.div( + { + "id": co_name, + "data-success": success, + }, + f"{co_name}: ", + str(my_object), + ), html.hr(), ) @@ -71,7 +79,7 @@ def object_in_templatetag(my_object: TestObject): def simple_button(): return html._( "simple_button:", - SimpleButton(id="simple-button"), + SimpleButton({"id": "simple-button"}), html.hr(), ) @@ -87,7 +95,9 @@ def use_connection(): and getattr(ws.carrier, "dotted_path", None) ) return html.div( - f"use_connection: {ws}", html.hr(), id="use-connection", data_success=success + {"id": "use-connection", "data-success": success}, + f"use_connection: {ws}", + html.hr(), ) @@ -96,7 +106,9 @@ def use_scope(): scope = django_idom.hooks.use_scope() success = len(scope) >= 10 and scope["type"] == "websocket" return html.div( - f"use_scope: {scope}", html.hr(), id="use-scope", data_success=success + {"id": "use-scope", "data-success": success}, + f"use_scope: {scope}", + html.hr(), ) @@ -105,7 +117,9 @@ def use_location(): location = django_idom.hooks.use_location() success = bool(location) return html.div( - f"use_location: {location}", html.hr(), id="use-location", data_success=success + {"id": "use-location", "data-success": success}, + f"use_location: {location}", + html.hr(), ) @@ -114,18 +128,20 @@ def use_origin(): origin = django_idom.hooks.use_origin() success = bool(origin) return html.div( - f"use_origin: {origin}", html.hr(), id="use-origin", data_success=success + {"id": "use-origin", "data-success": success}, + f"use_origin: {origin}", + html.hr(), ) @component def django_css(): return html.div( + {"id": "django-css"}, django_idom.components.django_css("django-css-test.css", key="test"), - html.div("django_css: ", style={"display": "inline"}), + html.div({"style": {"display": "inline"}}, "django_css: "), html.button("This text should be blue."), html.hr(), - id="django-css", ) @@ -134,10 +150,9 @@ def django_js(): success = False return html._( html.div( + {"id": "django-js", "data-success": success}, f"django_js: {success}", django_idom.components.django_js("django-js-test.js", key="test"), - id="django-js", - data_success=success, ), html.hr(), ) @@ -146,22 +161,34 @@ def django_js(): @component @django_idom.decorators.auth_required( fallback=html.div( - "unauthorized_user: Success", html.hr(), id="unauthorized-user-fallback" + {"id": "unauthorized-user-fallback"}, + "unauthorized_user: Success", + html.hr(), ) ) def unauthorized_user(): - return html.div("unauthorized_user: Fail", html.hr(), id="unauthorized-user") + return html.div( + {"id": "unauthorized-user"}, + "unauthorized_user: Fail", + html.hr(), + ) @component @django_idom.decorators.auth_required( auth_attribute="is_anonymous", fallback=html.div( - "authorized_user: Fail", html.hr(), id="authorized-user-fallback" + {"id": "authorized-user-fallback"}, + "authorized_user: Fail", + html.hr(), ), ) def authorized_user(): - return html.div("authorized_user: Success", html.hr(), id="authorized-user") + return html.div( + {"id": "authorized-user"}, + "authorized_user: Success", + html.hr(), + ) def create_relational_parent() -> RelationalParent: @@ -204,13 +231,15 @@ def relational_query(): fk = foriegn_child.data.parent return html.div( + { + "id": "relational-query", + "data-success": bool(mtm) and bool(oto) and bool(mto) and bool(fk), + }, html.div(f"Relational Parent Many To Many: {mtm}"), html.div(f"Relational Parent One To One: {oto}"), html.div(f"Relational Parent Many to One: {mto}"), html.div(f"Relational Child Foreign Key: {fk}"), html.hr(), - id="relational-query", - data_success=bool(mtm) and bool(oto) and bool(mto) and bool(fk), ) @@ -273,11 +302,13 @@ def on_change(event): return html.div( html.label("Add an item:"), html.input( - type="text", - id="todo-input", - value=input_value, - on_key_press=on_submit, - on_change=on_change, + { + "type": "text", + "id": "todo-input", + "value": input_value, + "onKeyPress": on_submit, + "onChange": on_change, + } ), mutation_status, rendered_items, @@ -289,15 +320,17 @@ def _render_todo_items(items, toggle_item): return html.ul( [ html.li( + {"id": f"todo-item-{item.text}"}, item.text, html.input( - id=f"todo-item-{item.text}-checkbox", - type="checkbox", - checked=item.done, - on_change=lambda event, i=item: toggle_item.execute(i), + { + "id": f"todo-item-{item.text}-checkbox", + "type": "checkbox", + "checked": item.done, + "onChange": lambda event, i=item: toggle_item.execute(i), + } ), key=item.text, - id=f"todo-item-{item.text}", ) for item in items ] @@ -335,45 +368,45 @@ def _render_todo_items(items, toggle_item): @component def view_to_component_sync_func_compatibility(): return html.div( + {"id": inspect.currentframe().f_code.co_name}, # type: ignore _view_to_component_sync_func_compatibility(key="test"), html.hr(), - id=inspect.currentframe().f_code.co_name, # type: ignore ) @component def view_to_component_async_func_compatibility(): return html.div( + {"id": inspect.currentframe().f_code.co_name}, # type: ignore _view_to_component_async_func_compatibility(), html.hr(), - id=inspect.currentframe().f_code.co_name, # type: ignore ) @component def view_to_component_sync_class_compatibility(): return html.div( + {"id": inspect.currentframe().f_code.co_name}, # type: ignore _view_to_component_sync_class_compatibility(), html.hr(), - id=inspect.currentframe().f_code.co_name, # type: ignore ) @component def view_to_component_async_class_compatibility(): return html.div( + {"id": inspect.currentframe().f_code.co_name}, # type: ignore _view_to_component_async_class_compatibility(), html.hr(), - id=inspect.currentframe().f_code.co_name, # type: ignore ) @component def view_to_component_template_view_class_compatibility(): return html.div( + {"id": inspect.currentframe().f_code.co_name}, # type: ignore _view_to_component_template_view_class_compatibility(), html.hr(), - id=inspect.currentframe().f_code.co_name, # type: ignore ) @@ -388,9 +421,8 @@ def on_click(_): return html._( html.button( + {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore "Click me", - id=f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore - on_click=on_click, ), _view_to_component_request(request=request), ) @@ -405,9 +437,8 @@ def on_click(_): return html._( html.button( + {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore "Click me", - id=f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore - on_click=on_click, ), _view_to_component_args(None, success), ) @@ -422,9 +453,8 @@ def on_click(_): return html._( html.button( + {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, # type: ignore "Click me", - id=f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore - on_click=on_click, ), _view_to_component_kwargs(success=success), ) From f8dda957bf66eec8416216cfb2cd42ba6f7f44d2 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 21 Feb 2023 03:54:38 -0800 Subject: [PATCH 06/11] bump idom version --- CHANGELOG.md | 6 ++++-- requirements/pkg-deps.txt | 2 +- src/js/package-lock.json | 14 ++++++------- src/js/package.json | 2 +- tests/test_app/components.py | 40 ++++++++++++++++++------------------ 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9d106d5..1d8703c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ Using the following categories, list your changes in this order: To upgrade from previous version you will need to... 1. Install `django-idom >= 3.0.0` - 2. Run `idom update-html-usages