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()