diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4546daec..92d870d2 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-21
???+ note
@@ -45,20 +45,21 @@ 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
` to update your `idom.html.*` calls to the new syntax
+ 2. Run `idom rewrite-keys ` and `idom rewrite-camel-case-props ` to update your `idom.html.*` calls to the new syntax
3. Run `python manage.py migrate` to create the new Django-IDOM database entries
### 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
- It is now mandatory to run `manage.py migrate` after installing IDOM.
-- Bumped the minimum IDOM version to 1.0.0
- - Due to IDOM 1.0.0, `idom.html.*`, HTML properties are now `snake_case` `**kwargs` rather than a `dict` of values.
- - You can auto-convert to the new style using `idom update-html-usages `.
+- Bumped the minimum IDOM version to 1.0.0. Due to IDOM 1.0.0, `idom.html.*`...
+ - HTML properties can now be `snake_case`. For example `className` now becomes `class_name`.
+ - `key=...` is now declared within the props `dict` (rather than as a `kwarg`).
- The `component` template tag now supports both positional and keyword arguments.
- The `component` template tag now supports non-serializable arguments.
- `IDOM_WS_MAX_RECONNECT_TIMEOUT` setting has been renamed to `IDOM_RECONNECT_MAX`.
@@ -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 back-end must now be specified with the `IDOM_CACHE` setting.
### 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
+- `use_query` will now utilize `field.related_name` when postprocessing many-to-one relationships.
### Security
@@ -246,8 +248,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/docs/python/settings.py b/docs/python/settings.py
index f7ee0f15..c8559b4a 100644
--- a/docs/python/settings.py
+++ b/docs/python/settings.py
@@ -1,8 +1,8 @@
-# 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 requires a multiprocessing-safe and thread-safe cache.
+IDOM_CACHE = "default"
+
+# IDOM requires a multiprocessing-safe and thread-safe database.
+IDOM_DATABASE = "default"
# Maximum seconds between reconnection attempts before giving up.
# Use `0` to prevent component reconnection.
@@ -11,5 +11,5 @@
# The URL for IDOM to serve the component rendering websocket
IDOM_WEBSOCKET_URL = "idom/"
-# Dotted path to the default postprocessor function, or `None`
+# Dotted path to the default `django_idom.hooks.use_query` postprocessor function, or `None`
IDOM_DEFAULT_QUERY_POSTPROCESSOR = "example_project.utils.my_postprocessor"
diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt
index dc87a347..c17d5d71 100644
--- a/requirements/pkg-deps.txt
+++ b/requirements/pkg-deps.txt
@@ -1,5 +1,5 @@
channels >=4.0.0
-idom >=1.0.0a3, <1.1.0
+idom >=1.0.0a5, <1.1.0
aiofile >=3.0
dill >=0.3.5
typing_extensions
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/components.py b/src/django_idom/components.py
index 4448371f..c57f87ae 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
@@ -76,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
@@ -209,12 +213,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 a7b4ca8d..b276b006 100644
--- a/src/django_idom/config.py
+++ b/src/django_idom/config.py
@@ -3,7 +3,8 @@
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
@@ -24,10 +25,15 @@
"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,
+ "IDOM_DATABASE",
+ DEFAULT_DB_ALIAS,
)
IDOM_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path(
getattr(
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:
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/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 1e67f368..219866f2 100644
--- a/src/django_idom/models.py
+++ b/src/django_idom/models.py
@@ -1,7 +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 a0963b35..c360bddd 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
@@ -311,12 +312,12 @@ 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 .models import ComponentParams
+ from .config import IDOM_CACHE, IDOM_DATABASE, IDOM_RECONNECT_MAX
+ from .models import ComponentSession
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)
)
@@ -324,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.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 "
@@ -334,5 +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.filter(last_accessed__lte=expires_by).delete()
- IDOM_CACHE.set(cache_key, now_str)
+ 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 99f860ba..e0cf56c4 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,20 +95,22 @@ 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.ComponentSession.objects.using(
+ IDOM_DATABASE
+ ).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:
+ 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/src/js/package-lock.json b/src/js/package-lock.json
index 83e0ec39..d58618b9 100644
--- a/src/js/package-lock.json
+++ b/src/js/package-lock.json
@@ -5,7 +5,7 @@
"packages": {
"": {
"dependencies": {
- "idom-client-react": "^1.0.0-a2"
+ "idom-client-react": "^1.0.0-a5"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^24.0.1",
@@ -242,9 +242,9 @@
"integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
},
"node_modules/idom-client-react": {
- "version": "1.0.0-a2",
- "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-1.0.0-a2.tgz",
- "integrity": "sha512-mfpyPXfM8R4lvgd45DJg+tn/tc5gKNxM32sQPaUr5oWFmt81f1nhWHLmM6RlNv/hB1n51023QCcU4Fj0NCmleg==",
+ "version": "1.0.0-a5",
+ "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-1.0.0-a5.tgz",
+ "integrity": "sha512-uzkg0qqtEY7xGcc3Yyc3IUzve6aGs0zvxQjf1xFrCxl8XMitdjPYQHdGpOxj+QHvmxgjETkH+5mcR/BO3OlZCQ==",
"dependencies": {
"htm": "^3.0.3",
"json-pointer": "^0.6.2"
@@ -661,9 +661,9 @@
"integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q=="
},
"idom-client-react": {
- "version": "1.0.0-a2",
- "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-1.0.0-a2.tgz",
- "integrity": "sha512-mfpyPXfM8R4lvgd45DJg+tn/tc5gKNxM32sQPaUr5oWFmt81f1nhWHLmM6RlNv/hB1n51023QCcU4Fj0NCmleg==",
+ "version": "1.0.0-a5",
+ "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-1.0.0-a5.tgz",
+ "integrity": "sha512-uzkg0qqtEY7xGcc3Yyc3IUzve6aGs0zvxQjf1xFrCxl8XMitdjPYQHdGpOxj+QHvmxgjETkH+5mcR/BO3OlZCQ==",
"requires": {
"htm": "^3.0.3",
"json-pointer": "^0.6.2"
diff --git a/src/js/package.json b/src/js/package.json
index 06e00606..03ad2b57 100644
--- a/src/js/package.json
+++ b/src/js/package.json
@@ -17,6 +17,6 @@
"@rollup/plugin-replace": "^5.0.2"
},
"dependencies": {
- "idom-client-react": "^1.0.0-a3"
+ "idom-client-react": "^1.0.0-a5"
}
}
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/components.py b/tests/test_app/components.py
index 042b9aa7..f9ea53ce 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,12 @@ def button():
html.div(
"button:",
html.button(
+ {"id": "counter-inc", "on_click": 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 +41,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 +53,9 @@ 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 +73,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 +89,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 +100,7 @@ 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 +109,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 +120,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 +142,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 +153,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 +223,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 +294,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,
+ "on_key_press": on_submit,
+ "on_change": on_change,
+ }
),
mutation_status,
rendered_items,
@@ -289,15 +312,16 @@ def _render_todo_items(items, toggle_item):
return html.ul(
[
html.li(
+ {"id": f"todo-item-{item.text}", "key": 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,
+ "on_change": lambda event, i=item: toggle_item.execute(i),
+ }
),
- key=item.text,
- id=f"todo-item-{item.text}",
)
for item in items
]
@@ -335,45 +359,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 +412,11 @@ def on_click(_):
return html._(
html.button(
+ {
+ "id": f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore
+ "on_click": on_click,
+ },
"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 +431,11 @@ def on_click(_):
return html._(
html.button(
+ {
+ "id": f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore
+ "on_click": on_click,
+ },
"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 +450,11 @@ def on_click(_):
return html._(
html.button(
+ {
+ "id": f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore
+ "on_click": on_click,
+ },
"Click me",
- id=f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore
- on_click=on_click,
),
_view_to_component_kwargs(success=success),
)
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()