From 51a473a9cf658a2efaf944046493ffdd5ba9d36c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 02:45:50 -0700 Subject: [PATCH 001/110] functional static_css implementation --- src/django_idom/__init__.py | 4 ++-- src/django_idom/components.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/django_idom/components.py diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index af2dec38..7bf01173 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,7 +1,7 @@ -from . import hooks +from . import components, hooks from .websocket.consumer import IdomWebsocket from .websocket.paths import IDOM_WEBSOCKET_PATH __version__ = "1.0.0" -__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks"] +__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks", "components"] diff --git a/src/django_idom/components.py b/src/django_idom/components.py new file mode 100644 index 00000000..e4a68d10 --- /dev/null +++ b/src/django_idom/components.py @@ -0,0 +1,31 @@ +import os + +from django.contrib.staticfiles.finders import find +from idom import component, html + +from django_idom.config import IDOM_CACHE + + +@component +def static_css(static_path: str): + """Returns a Django CSS static file CSS stylesheet within a style tag. + This helps avoid the need to wait for CSS files to load.""" + # Try to find the file within Django's static files + abs_path = find(static_path) + if not abs_path: + raise FileNotFoundError(f"Could not find static file {static_path} within Django's static files.") + + # Fetch the file from cache, if available + last_modified_time = os.stat(abs_path).st_mtime + cache_key = f"django_idom:css_contents:{static_path}" + file_contents = IDOM_CACHE.get(cache_key, version=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( + cache_key, file_contents, timeout=None, version=last_modified_time + ) + + # Return the file contents as a style tag + return html.style(file_contents) From b1a2bcf452129d2394cab87b397870ff0793a69e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 02:56:07 -0700 Subject: [PATCH 002/110] Bump Django IDOM version --- CHANGELOG.md | 7 +++++++ src/django_idom/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a696e82..dc80a74a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ Types of changes are to be listed in this order - Nothing (yet) +## [1.1.0] - 2022-06-25 + +### Added + +- `static_css` component that can be used to defer loading CSS for your IDOM components until you need them. + ## [1.0.0] - 2022-05-22 ### Added @@ -103,6 +109,7 @@ Types of changes are to be listed in this order - Support for IDOM within the Django [unreleased]: https://github.com/idom-team/django-idom/compare/1.0.0...HEAD +[1.1.0]: https://github.com/idom-team/django-idom/compare/1.0.0...1.1.0 [1.0.0]: https://github.com/idom-team/django-idom/compare/0.0.5...1.0.0 [0.0.5]: https://github.com/idom-team/django-idom/compare/0.0.4...0.0.5 [0.0.4]: https://github.com/idom-team/django-idom/compare/0.0.3...0.0.4 diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 7bf01173..857f92e1 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -3,5 +3,5 @@ from .websocket.paths import IDOM_WEBSOCKET_PATH -__version__ = "1.0.0" +__version__ = "1.1.0" __all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks", "components"] From 6b82359f8c4e3ea16f9650910f048be9947ff3d1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 02:57:14 -0700 Subject: [PATCH 003/110] format components.py --- src/django_idom/components.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index e4a68d10..b76246fc 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -13,7 +13,9 @@ def static_css(static_path: str): # Try to find the file within Django's static files abs_path = find(static_path) if not abs_path: - raise FileNotFoundError(f"Could not find static file {static_path} within Django's static files.") + raise FileNotFoundError( + f"Could not find static file {static_path} within Django's static files." + ) # Fetch the file from cache, if available last_modified_time = os.stat(abs_path).st_mtime From 551b6f1ca5f1805d3b718a553c70da23490532f2 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 03:26:28 -0700 Subject: [PATCH 004/110] rudimentary docs --- docs/features/components.md | 31 +++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 32 insertions(+) create mode 100644 docs/features/components.md diff --git a/docs/features/components.md b/docs/features/components.md new file mode 100644 index 00000000..1ae683d6 --- /dev/null +++ b/docs/features/components.md @@ -0,0 +1,31 @@ +## Static CSS + +Allows you to defer loading a CSS stylesheet until a component's begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). + +```python title="components.py" +from idom import component, html +from django_idom.components import static_css + +@component +def MyComponent(): + return html.div( + static_css("/static/css/buttons.css"), + html.button("My Button!") + ) +``` + +??? question "Should I put `static_css` at the top of my component?" + + + +??? question "Can I load HTML using `html.link`?" + +??? question "What about external stylesheets?" + +??? question "Why not load my CSS in `#!html `?" + + + +## Static JavaScript + + diff --git a/mkdocs.yml b/mkdocs.yml index 1cfff5ab..50cef329 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ nav: - 4. Render Your View: getting-started/render-view.md - 5. Learn More: getting-started/learn-more.md - Exclusive Features: + - Components: features/components.md - Hooks: features/hooks.md - Template Tag: features/templatetag.md - Contribute: From d5b0d9cd7c78398cf450ad521e2a00b806d166a6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 03:29:35 -0700 Subject: [PATCH 005/110] fix docstring --- src/django_idom/components.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index b76246fc..977ac1a8 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -8,8 +8,7 @@ @component def static_css(static_path: str): - """Returns a Django CSS static file CSS stylesheet within a style tag. - This helps avoid the need to wait for CSS files to load.""" + """Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading.""" # Try to find the file within Django's static files abs_path = find(static_path) if not abs_path: From 11b3e42aa40ce6c5d3207d7782ece262c0e016d8 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 04:16:18 -0700 Subject: [PATCH 006/110] clean up readme --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index fe0165df..5fcde22f 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ Any Python web framework with Websockets can support IDOM. See below for what fr ---- - # At a Glance ## `my_app/components.py` @@ -65,8 +63,6 @@ Additonally, you can pass in keyword arguments into your component function. For ---- - # Resources From e9339fa6cec84438bb2abe4f943cf839cf96f2ed Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 04:16:28 -0700 Subject: [PATCH 007/110] static_js --- src/django_idom/components.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 977ac1a8..17800372 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -9,6 +9,16 @@ @component def static_css(static_path: str): """Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading.""" + return html.style(_cached_static_contents(static_path, "css_contents")) + + +@component +def static_js(static_path: str): + """Fetches a JS static file for use within IDOM. This allows for deferred JS loading.""" + return html.script(_cached_static_contents(static_path, "js_contents")) + + +def _cached_static_contents(static_path: str, cache_name: str): # Try to find the file within Django's static files abs_path = find(static_path) if not abs_path: @@ -18,7 +28,7 @@ def static_css(static_path: str): # Fetch the file from cache, if available last_modified_time = os.stat(abs_path).st_mtime - cache_key = f"django_idom:css_contents:{static_path}" + cache_key = f"django_idom:{cache_name}:{static_path}" file_contents = IDOM_CACHE.get(cache_key, version=last_modified_time) if file_contents is None: with open(abs_path, encoding="utf-8") as static_file: @@ -28,5 +38,4 @@ def static_css(static_path: str): cache_key, file_contents, timeout=None, version=last_modified_time ) - # Return the file contents as a style tag - return html.style(file_contents) + return file_contents From 836fe718a1824b99bc45f33f2078c65427622e52 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 04:16:49 -0700 Subject: [PATCH 008/110] minor docs styling tweaks --- docs/features/hooks.md | 3 +-- docs/stylesheets/extra.css | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 64721d31..e1cc4d04 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -18,7 +18,7 @@ def MyComponent(): return html.div(my_websocket) ``` ---- + ## Use Scope @@ -34,7 +34,6 @@ def MyComponent(): return html.div(my_scope) ``` ---- ## Use Location diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 13121f71..2f8c7dab 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -6,3 +6,7 @@ .md-header { background-color: var(--md-footer-bg-color--dark); } + +.md-typeset :is(.admonition, details) { + margin: 1em 0; +} From 0ee94021a8e46aded023e63539bca996f16ad634 Mon Sep 17 00:00:00 2001 From: Mark <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 15:14:35 -0700 Subject: [PATCH 009/110] Update docs/features/components.md Co-authored-by: Ryan Morshead --- docs/features/components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/components.md b/docs/features/components.md index 1ae683d6..3d9208ca 100644 --- a/docs/features/components.md +++ b/docs/features/components.md @@ -1,6 +1,6 @@ ## Static CSS -Allows you to defer loading a CSS stylesheet until a component's begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). +Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). ```python title="components.py" from idom import component, html From 4cc86aa655350a08b359e158387da4a4b6842867 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:28:14 -0700 Subject: [PATCH 010/110] flesh out the docs --- docs/features/components.md | 108 +++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 7 deletions(-) diff --git a/docs/features/components.md b/docs/features/components.md index 3d9208ca..52315e42 100644 --- a/docs/features/components.md +++ b/docs/features/components.md @@ -9,23 +9,117 @@ from django_idom.components import static_css @component def MyComponent(): return html.div( - static_css("/static/css/buttons.css"), - html.button("My Button!") + static_css("css/buttons.css"), + html.button("My Button!"), ) ``` ??? question "Should I put `static_css` at the top of my component?" - + Yes, if the stylesheet is contains styling for your component. -??? question "Can I load HTML using `html.link`?" +??? question "Can I load CSS using `html.link` instead?" -??? question "What about external stylesheets?" + While you can load a with `html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will likely be loaded after your component is displayed. + + Here's an example on this use case: + + ```python + from idom import component, html + from django_idom.components import static_js + from django.templatetags.static import static + + @component + def MyComponent(): + return html.div( + html.link({"rel": "stylesheet", "href": static("css/buttons.css")}), + html.button("My Button!"), + ) + ``` + +??? question "How do I load external CSS?" + + `static_css` can only be used with local static files. + + For external CSS, substitute `static_css` with `html.link` as such: + + ```python + from idom import component, html + from django_idom.components import static_js + + @component + def MyComponent(): + return html.div( + html.link({"rel": "stylesheet", "href": "https://example.com/external-styles.css"}), + html.button("My Button!"), + ) + ``` ??? question "Why not load my CSS in `#!html `?" - + Traditionally, stylesheets are loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. + + Instead, you can use the `static_css` component to help improve webpage load times to deferring loading stylesheets until they are needed. ## Static JavaScript - +Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). + +```python +from idom import component, html +from django_idom.components import static_js + +@component +def MyComponent(): + return html.div( + html.button("My Button!"), + static_js("js/scripts.js"), + ) +``` + +??? question "Should I put `static_js` at the bottom of my component?" + + Yes, if your scripts are reliant on the contents of the component. + +??? question "Can I load JavaScript using `html.script` instead?" + + While you can load with `html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed. + + Here's an example on this use case: + + ```python + from idom import component, html + from django_idom.components import static_js + from django.templatetags.static import static + + @component + def MyComponent(): + return html.div( + html.script({"src": static("js/scripts.js")}), + html.button("My Button!"), + ) + ``` + +??? question "How do I load external JS?" + + `static_js` can only be used with local static files. + + For external JavaScript, substitute `static_js` with `html.script` as such: + + ```python + from idom import component, html + from django_idom.components import static_js + + @component + def MyComponent(): + return html.div( + html.script({"src": static("https://example.com/external-scripts.js")}), + html.button("My Button!"), + ) + ``` + +??? question "Why not load my JS in `#!html `?" + + Traditionally, JavaScript is loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. + + Instead, you can use the `static_js` component to help improve webpage load times to deferring loading scripts until they are needed. From 526bc8f9b0cea4adb8804de8d8a396149c3399e9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:28:48 -0700 Subject: [PATCH 011/110] simplify _cached_static_contents --- src/django_idom/components.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 17800372..efe32a0e 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -9,16 +9,16 @@ @component def static_css(static_path: str): """Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading.""" - return html.style(_cached_static_contents(static_path, "css_contents")) + return html.style(_cached_static_contents(static_path)) @component def static_js(static_path: str): """Fetches a JS static file for use within IDOM. This allows for deferred JS loading.""" - return html.script(_cached_static_contents(static_path, "js_contents")) + return html.script(_cached_static_contents(static_path)) -def _cached_static_contents(static_path: str, cache_name: str): +def _cached_static_contents(static_path: str): # Try to find the file within Django's static files abs_path = find(static_path) if not abs_path: @@ -28,7 +28,7 @@ def _cached_static_contents(static_path: str, cache_name: str): # Fetch the file from cache, if available last_modified_time = os.stat(abs_path).st_mtime - cache_key = f"django_idom:{cache_name}:{static_path}" + cache_key = f"django_idom:static_contents:{static_path}" file_contents = IDOM_CACHE.get(cache_key, version=last_modified_time) if file_contents is None: with open(abs_path, encoding="utf-8") as static_file: From 6b0dfb8a0512d5c80df557ac773ec7df61e0997a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:29:06 -0700 Subject: [PATCH 012/110] add static js to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc80a74a..5becd123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ Types of changes are to be listed in this order ### Added -- `static_css` component that can be used to defer loading CSS for your IDOM components until you need them. +- `static_css` and `static_js` components to defer loading CSS & JS files until you need them. ## [1.0.0] - 2022-05-22 From 70d78fcf7214852354aae51e297eb23d28f5d6cc Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:30:37 -0700 Subject: [PATCH 013/110] clean up changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5becd123..315dc78a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ Types of changes are to be listed in this order ### Added -- `static_css` and `static_js` components to defer loading CSS & JS files until you need them. +- `static_css` and `static_js` components to defer loading CSS & JS files until needed. ## [1.0.0] - 2022-05-22 From 0ffb5a58847e2feacb35b8242af2893da9e71bbf Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:48:09 -0700 Subject: [PATCH 014/110] Selenium 4.3 compatibility --- tests/test_app/tests/test_components.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index fbfda829..1f102286 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -26,18 +26,18 @@ def wait_until(self, condition, timeout=10): return self.wait(timeout).until(lambda driver: condition()) def test_hello_world(self): - self.driver.find_element_by_id("hello-world") + self.driver.find_element("hello-world") def test_counter(self): - button = self.driver.find_element_by_id("counter-inc") - count = self.driver.find_element_by_id("counter-num") + button = self.driver.find_element("counter-inc") + count = self.driver.find_element("counter-num") for i in range(5): self.wait_until(lambda: count.get_attribute("data-count") == str(i)) button.click() def test_parametrized_component(self): - element = self.driver.find_element_by_id("parametrized-component") + element = self.driver.find_element("parametrized-component") self.assertEqual(element.get_attribute("data-value"), "579") def test_component_from_web_module(self): @@ -48,15 +48,15 @@ def test_component_from_web_module(self): ) def test_use_websocket(self): - element = self.driver.find_element_by_id("use-websocket") + element = self.driver.find_element("use-websocket") self.assertEqual(element.get_attribute("data-success"), "true") def test_use_scope(self): - element = self.driver.find_element_by_id("use-scope") + element = self.driver.find_element("use-scope") self.assertEqual(element.get_attribute("data-success"), "true") def test_use_location(self): - element = self.driver.find_element_by_id("use-location") + element = self.driver.find_element("use-location") self.assertEqual(element.get_attribute("data-success"), "true") From 6cc9dede62b43e49aaf97c352b7172ce367830a7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:48:19 -0700 Subject: [PATCH 015/110] add title to readme example --- docs/features/components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/components.md b/docs/features/components.md index 52315e42..a2c3ddfe 100644 --- a/docs/features/components.md +++ b/docs/features/components.md @@ -65,7 +65,7 @@ def MyComponent(): Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). -```python +```python title="components.py" from idom import component, html from django_idom.components import static_js From 77332436535ea7adf1c41c632d9386810995dcc9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:52:58 -0700 Subject: [PATCH 016/110] pin selenium to older version 4.2 --- requirements/test-env.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/test-env.txt b/requirements/test-env.txt index 6f2e151e..61ee65ee 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -1,3 +1,3 @@ django -selenium +selenium <= 4.2.0 twisted From 568a49e057fa70a3070f315758a5c366be2493c7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:54:53 -0700 Subject: [PATCH 017/110] Revert "Selenium 4.3 compatibility" This reverts commit 0ffb5a58847e2feacb35b8242af2893da9e71bbf. --- tests/test_app/tests/test_components.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 1f102286..fbfda829 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -26,18 +26,18 @@ def wait_until(self, condition, timeout=10): return self.wait(timeout).until(lambda driver: condition()) def test_hello_world(self): - self.driver.find_element("hello-world") + self.driver.find_element_by_id("hello-world") def test_counter(self): - button = self.driver.find_element("counter-inc") - count = self.driver.find_element("counter-num") + button = self.driver.find_element_by_id("counter-inc") + count = self.driver.find_element_by_id("counter-num") for i in range(5): self.wait_until(lambda: count.get_attribute("data-count") == str(i)) button.click() def test_parametrized_component(self): - element = self.driver.find_element("parametrized-component") + element = self.driver.find_element_by_id("parametrized-component") self.assertEqual(element.get_attribute("data-value"), "579") def test_component_from_web_module(self): @@ -48,15 +48,15 @@ def test_component_from_web_module(self): ) def test_use_websocket(self): - element = self.driver.find_element("use-websocket") + element = self.driver.find_element_by_id("use-websocket") self.assertEqual(element.get_attribute("data-success"), "true") def test_use_scope(self): - element = self.driver.find_element("use-scope") + element = self.driver.find_element_by_id("use-scope") self.assertEqual(element.get_attribute("data-success"), "true") def test_use_location(self): - element = self.driver.find_element("use-location") + element = self.driver.find_element_by_id("use-location") self.assertEqual(element.get_attribute("data-success"), "true") From 5f981a058dd91ebbd6d4c662ab28dc78ef2b1d72 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 17:28:42 -0700 Subject: [PATCH 018/110] add new tests --- tests/test_app/components.py | 22 ++++++++++++++++++++++ tests/test_app/static/static-css-test.css | 3 +++ tests/test_app/static/static-js-test.js | 3 +++ tests/test_app/templates/base.html | 2 ++ tests/test_app/tests/test_components.py | 7 +++++++ 5 files changed, 37 insertions(+) create mode 100644 tests/test_app/static/static-css-test.css create mode 100644 tests/test_app/static/static-js-test.js diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 2efd878c..ba1cb1be 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -71,3 +71,25 @@ def UseLocation(): f"UseLocation: {location}", idom.html.hr(), ) + + +@idom.component +def StaticCSS(): + return idom.html.div( + {"id": "static-css"}, + django_idom.components.static_css("static-css-test.css"), + idom.html.div({"style": {"display": "inline"}}, "StaticCSS: "), + idom.html.button("This text should be blue."), + idom.html.hr(), + ) + + +@idom.component +def StaticJS(): + success = False + return idom.html.div( + {"id": "static-js", "data-success": success}, + f"StaticJS: {success}", + django_idom.components.static_js("static-js-test.js"), + idom.html.hr(), + ) diff --git a/tests/test_app/static/static-css-test.css b/tests/test_app/static/static-css-test.css new file mode 100644 index 00000000..8a4766b9 --- /dev/null +++ b/tests/test_app/static/static-css-test.css @@ -0,0 +1,3 @@ +#static-css button { + color: blue; +} \ No newline at end of file diff --git a/tests/test_app/static/static-js-test.js b/tests/test_app/static/static-js-test.js new file mode 100644 index 00000000..005b363a --- /dev/null +++ b/tests/test_app/static/static-js-test.js @@ -0,0 +1,3 @@ +let el = document.body.querySelector("#static-js"); +el.textContent = "StaticJS: True"; +el.dataset.success = "True"; \ No newline at end of file diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 0c52a8cc..c597cdf9 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -19,6 +19,8 @@

IDOM Test Page

{% component "test_app.components.UseWebsocket" %}
{% component "test_app.components.UseScope" %}
{% component "test_app.components.UseLocation" %}
+
{% component "test_app.components.StaticCSS" %}
+
{% component "test_app.components.StaticJS" %}
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index fbfda829..8c21c5f6 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -59,6 +59,13 @@ def test_use_location(self): element = self.driver.find_element_by_id("use-location") self.assertEqual(element.get_attribute("data-success"), "true") + def test_static_css(self): + element = self.driver.find_element_by_css_selector("#static-css button") + self.assertEqual(element.value_of_css_property("color"), "blue") + + def test_static_js(self): + element = self.driver.find_element_by_id("static-js") + self.assertEqual(element.get_attribute("data-success"), "true") def make_driver(page_load_timeout, implicit_wait_timeout): options = webdriver.ChromeOptions() From f9aa39f66ee9ce3787982cf652b94b5f3cb3074d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 17:34:13 -0700 Subject: [PATCH 019/110] fix tests --- tests/test_app/static/static-css-test.css | 2 +- tests/test_app/static/static-js-test.js | 2 +- tests/test_app/tests/test_components.py | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_app/static/static-css-test.css b/tests/test_app/static/static-css-test.css index 8a4766b9..6589cf89 100644 --- a/tests/test_app/static/static-css-test.css +++ b/tests/test_app/static/static-css-test.css @@ -1,3 +1,3 @@ #static-css button { - color: blue; + color: rgba(0, 0, 255, 1); } \ No newline at end of file diff --git a/tests/test_app/static/static-js-test.js b/tests/test_app/static/static-js-test.js index 005b363a..c2d06e40 100644 --- a/tests/test_app/static/static-js-test.js +++ b/tests/test_app/static/static-js-test.js @@ -1,3 +1,3 @@ let el = document.body.querySelector("#static-js"); el.textContent = "StaticJS: True"; -el.dataset.success = "True"; \ No newline at end of file +el.dataset.success = "true"; \ No newline at end of file diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 8c21c5f6..33a8c6e8 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -61,12 +61,15 @@ def test_use_location(self): def test_static_css(self): element = self.driver.find_element_by_css_selector("#static-css button") - self.assertEqual(element.value_of_css_property("color"), "blue") + self.assertEqual( + element.value_of_css_property("color"), "rgba(0, 0, 255, 1)" + ) def test_static_js(self): element = self.driver.find_element_by_id("static-js") self.assertEqual(element.get_attribute("data-success"), "true") + def make_driver(page_load_timeout, implicit_wait_timeout): options = webdriver.ChromeOptions() options.headless = bool(int(os.environ.get("SELENIUM_HEADLESS", 0))) From aa55ad76a0d0deea0a2c5151692909cea281b1ba Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 17:57:03 -0700 Subject: [PATCH 020/110] docs cleanup --- docs/features/components.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/features/components.md b/docs/features/components.md index a2c3ddfe..b24dde3e 100644 --- a/docs/features/components.md +++ b/docs/features/components.md @@ -18,11 +18,11 @@ def MyComponent(): Yes, if the stylesheet is contains styling for your component. -??? question "Can I load CSS using `html.link` instead?" +??? question "Can I load static CSS using `html.link` instead?" - While you can load a with `html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will likely be loaded after your component is displayed. + While you can load stylesheets with `html.link`, keep in mind that loading this way **does not** ensure load order. Thus, your stylesheet will be loaded after your component is displayed. This would likely cause some visual jankiness, so use this at your own discretion. - Here's an example on this use case: + Here's an example on what you should typically avoid doing: ```python from idom import component, html @@ -41,7 +41,7 @@ def MyComponent(): `static_css` can only be used with local static files. - For external CSS, substitute `static_css` with `html.link` as such: + For external CSS, substitute `static_css` with `html.link`. ```python from idom import component, html @@ -59,7 +59,7 @@ def MyComponent(): Traditionally, stylesheets are loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. - Instead, you can use the `static_css` component to help improve webpage load times to deferring loading stylesheets until they are needed. + To help improve webpage load times, you can use the `static_css` component to defer loading your stylesheet until it is needed. ## Static JavaScript @@ -81,11 +81,11 @@ def MyComponent(): Yes, if your scripts are reliant on the contents of the component. -??? question "Can I load JavaScript using `html.script` instead?" +??? question "Can I load static JavaScript using `html.script` instead?" - While you can load with `html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed. + While you can load JavaScript with `html.script`, keep in mind that loading this way **does not** ensure load order. Thus, your JavaScript will likely be loaded at an arbitrary time after your component is displayed. - Here's an example on this use case: + Here's an example on what you should typically avoid doing: ```python from idom import component, html @@ -104,7 +104,7 @@ def MyComponent(): `static_js` can only be used with local static files. - For external JavaScript, substitute `static_js` with `html.script` as such: + For external JavaScript, substitute `static_js` with `html.script`. ```python from idom import component, html @@ -122,4 +122,4 @@ def MyComponent(): Traditionally, JavaScript is loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. - Instead, you can use the `static_js` component to help improve webpage load times to deferring loading scripts until they are needed. + To help improve webpage load times, you can use the `static_js` component to defer loading your JavaScript until it is needed. From fa11504eb616633223d255ee4c344ad604a0fea1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 23 Jun 2022 18:08:30 -0700 Subject: [PATCH 021/110] Create PR template --- .github/pull_request_template.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..92ac3c3d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +# Description + +A summary of the changes. + +# Checklist: + +Please update this checklist as you complete each item: + +- [ ] Tests have been included for all bug fixes or added functionality. +- [ ] The `changelog.rst` has been updated with any significant changes, if necessary. +- [ ] GitHub Issues which may be closed by this PR have been linked. From 03ac98024f6155b9d32a11a5f9fac65ae09b6a79 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 24 Jun 2022 14:59:34 -0700 Subject: [PATCH 022/110] bump the minimum idom version --- CHANGELOG.md | 4 ++++ requirements/pkg-deps.txt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 315dc78a..141d1101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ Types of changes are to be listed in this order - `static_css` and `static_js` components to defer loading CSS & JS files until needed. +### Changed + +- Bumped the minimum IDOM version to 0.39.0 + ## [1.0.0] - 2022-05-22 ### Added diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index 7b9f9420..08e42fad 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,3 +1,3 @@ channels >=3.0.0 -idom >=0.38.0, <0.39.0 +idom >=0.39.0, <0.40.0 aiofile >=3.0 From 7095f648597307e57dbd75b8d4daf86d44f808c0 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 25 Jun 2022 19:05:54 -0700 Subject: [PATCH 023/110] update issue form --- .github/ISSUE_TEMPLATE/issue-form.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue-form.yml b/.github/ISSUE_TEMPLATE/issue-form.yml index 342316ad..45a21446 100644 --- a/.github/ISSUE_TEMPLATE/issue-form.yml +++ b/.github/ISSUE_TEMPLATE/issue-form.yml @@ -13,11 +13,4 @@ body: label: Proposed Actions description: Describe what ought to be done, and why that will address the reasons for action mentioned above. validations: - required: false -- type: textarea - attributes: - label: Work Items - description: | - An itemized list or detailed description of the work to be done to based on the proposed actions above. - validations: - required: false + required: false \ No newline at end of file From bdb3431f8b77d33ea7d292f2b666d97298687cab Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 25 Jun 2022 19:16:54 -0700 Subject: [PATCH 024/110] minor readme wordsmithing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5fcde22f..58f86611 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ def HelloWorld(recipient: str): -In your **Django app**'s HTML located within your `templates` folder, you can now embed your IDOM component using the `component` template tag. Within this tag, you will need to type in your dotted path to the component function as the first argument. +In your **Django app**'s HTML template, you can now embed your IDOM component using the `component` template tag. Within this tag, you will need to type in your dotted path to the component function as the first argument. Additonally, you can pass in keyword arguments into your component function. For example, after reading the code below, pay attention to how the function definition for `HelloWorld` (_in the previous example_) accepts a `recipient` argument. From 7ea6306d6d0cfa4e47a9b1e6a07f0a33bd3db682 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 25 Jun 2022 23:53:36 -0700 Subject: [PATCH 025/110] docstring for component template tag --- src/django_idom/templatetags/idom.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index 5707733d..386721d2 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -15,6 +15,21 @@ @register.inclusion_tag("idom/component.html") def component(_component_id_, **kwargs): + """ + This tag is used to embed an existing IDOM component into your HTML template. + + The first arguement within this tag is your dotted path to the component function. + + Subsequent values are keyworded arguements are passed into your component:: + + {% load idom %} + + + + {% component "example_project.my_app.components.HelloWorld" recipient="World" %} + + + """ _register_component(_component_id_) class_ = kwargs.pop("class", "") From 2de0deef8822dce259ea58650c7e3928780352d7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 03:52:19 -0700 Subject: [PATCH 026/110] use PascalCase for component names --- CHANGELOG.md | 2 +- docs/features/components.md | 36 +++++++++++++++++------------------ src/django_idom/components.py | 18 ++++++++++++++---- tests/test_app/components.py | 4 ++-- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 141d1101..87a010cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ Types of changes are to be listed in this order ### Added -- `static_css` and `static_js` components to defer loading CSS & JS files until needed. +- `DjangoCSS` and `DjangoJS` components to defer loading CSS & JS files until needed. ### Changed diff --git a/docs/features/components.md b/docs/features/components.md index b24dde3e..bf50aef5 100644 --- a/docs/features/components.md +++ b/docs/features/components.md @@ -1,20 +1,20 @@ -## Static CSS +## Django CSS Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). ```python title="components.py" from idom import component, html -from django_idom.components import static_css +from django_idom.components import DjangoCSS @component def MyComponent(): return html.div( - static_css("css/buttons.css"), + DjangoCSS("css/buttons.css"), html.button("My Button!"), ) ``` -??? question "Should I put `static_css` at the top of my component?" +??? question "Should I put `DjangoCSS` at the top of my component?" Yes, if the stylesheet is contains styling for your component. @@ -26,7 +26,7 @@ def MyComponent(): ```python from idom import component, html - from django_idom.components import static_js + from django_idom.components import DjangoJS from django.templatetags.static import static @component @@ -39,13 +39,13 @@ def MyComponent(): ??? question "How do I load external CSS?" - `static_css` can only be used with local static files. + `DjangoCSS` can only be used with local static files. - For external CSS, substitute `static_css` with `html.link`. + For external CSS, substitute `DjangoCSS` with `html.link`. ```python from idom import component, html - from django_idom.components import static_js + from django_idom.components import DjangoJS @component def MyComponent(): @@ -59,25 +59,25 @@ def MyComponent(): Traditionally, stylesheets are loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. - To help improve webpage load times, you can use the `static_css` component to defer loading your stylesheet until it is needed. + To help improve webpage load times, you can use the `DjangoCSS` component to defer loading your stylesheet until it is needed. -## Static JavaScript +## Django JS Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). ```python title="components.py" from idom import component, html -from django_idom.components import static_js +from django_idom.components import DjangoJS @component def MyComponent(): return html.div( html.button("My Button!"), - static_js("js/scripts.js"), + DjangoJS("js/scripts.js"), ) ``` -??? question "Should I put `static_js` at the bottom of my component?" +??? question "Should I put `DjangoJS` at the bottom of my component?" Yes, if your scripts are reliant on the contents of the component. @@ -89,7 +89,7 @@ def MyComponent(): ```python from idom import component, html - from django_idom.components import static_js + from django_idom.components import DjangoJS from django.templatetags.static import static @component @@ -102,13 +102,13 @@ def MyComponent(): ??? question "How do I load external JS?" - `static_js` can only be used with local static files. + `DjangoJS` can only be used with local static files. - For external JavaScript, substitute `static_js` with `html.script`. + For external JavaScript, substitute `DjangoJS` with `html.script`. ```python from idom import component, html - from django_idom.components import static_js + from django_idom.components import DjangoJS @component def MyComponent(): @@ -122,4 +122,4 @@ def MyComponent(): Traditionally, JavaScript is loaded in your `#!html ` using the `#!jinja {% load static %}` template tag. - To help improve webpage load times, you can use the `static_js` component to defer loading your JavaScript until it is needed. + To help improve webpage load times, you can use the `DjangoJS` component to defer loading your JavaScript until it is needed. diff --git a/src/django_idom/components.py b/src/django_idom/components.py index efe32a0e..16207644 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -7,14 +7,24 @@ @component -def static_css(static_path: str): - """Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading.""" +def DjangoCSS(static_path: str): + """Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading. + + Args: + static_path: The path to the static file. This path is identical to what you would + use on a `static` template tag. + """ return html.style(_cached_static_contents(static_path)) @component -def static_js(static_path: str): - """Fetches a JS static file for use within IDOM. This allows for deferred JS loading.""" +def DjangoJS(static_path: str): + """Fetches a JS static file for use within IDOM. This allows for deferred JS loading. + + Args: + static_path: The path to the static file. This path is identical to what you would + use on a `static` template tag. + """ return html.script(_cached_static_contents(static_path)) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index ba1cb1be..13939f8b 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -77,7 +77,7 @@ def UseLocation(): def StaticCSS(): return idom.html.div( {"id": "static-css"}, - django_idom.components.static_css("static-css-test.css"), + django_idom.components.DjangoCSS("static-css-test.css"), idom.html.div({"style": {"display": "inline"}}, "StaticCSS: "), idom.html.button("This text should be blue."), idom.html.hr(), @@ -90,6 +90,6 @@ def StaticJS(): return idom.html.div( {"id": "static-js", "data-success": success}, f"StaticJS: {success}", - django_idom.components.static_js("static-js-test.js"), + django_idom.components.DjangoJS("static-js-test.js"), idom.html.hr(), ) From 1db08a5797b00edea0100738adb7b6f322551301 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 03:57:09 -0700 Subject: [PATCH 027/110] Fix formatting --- src/django_idom/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 16207644..eb087230 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -9,7 +9,7 @@ @component def DjangoCSS(static_path: str): """Fetches a CSS static file for use within IDOM. This allows for deferred CSS loading. - + Args: static_path: The path to the static file. This path is identical to what you would use on a `static` template tag. @@ -20,7 +20,7 @@ def DjangoCSS(static_path: str): @component def DjangoJS(static_path: str): """Fetches a JS static file for use within IDOM. This allows for deferred JS loading. - + Args: static_path: The path to the static file. This path is identical to what you would use on a `static` template tag. From 93debb715a695351237bec885bf918bd6adbff14 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 05:08:46 -0700 Subject: [PATCH 028/110] Quick draft of view_to_component --- src/django_idom/__init__.py | 4 +-- src/django_idom/config.py | 3 ++ src/django_idom/decorators.py | 59 +++++++++++++++++++++++++++++++++++ src/django_idom/http/urls.py | 7 ++++- src/django_idom/http/views.py | 23 +++++++++++++- src/django_idom/types.py | 14 +++++++++ 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 src/django_idom/decorators.py create mode 100644 src/django_idom/types.py diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index 857f92e1..fcb25a18 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,7 +1,7 @@ -from . import components, hooks +from . import components, hooks, decorators from .websocket.consumer import IdomWebsocket from .websocket.paths import IDOM_WEBSOCKET_PATH __version__ = "1.1.0" -__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks", "components"] +__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks", "components", "decorators"] diff --git a/src/django_idom/config.py b/src/django_idom/config.py index cb037ed1..47f19455 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -4,8 +4,11 @@ from django.core.cache import DEFAULT_CACHE_ALIAS, caches from idom.core.types import ComponentConstructor +from django_idom.types import ViewToComponentIframe + IDOM_REGISTERED_COMPONENTS: Dict[str, ComponentConstructor] = {} +IDOM_REGISTERED_IFRAMES: Dict[str, ViewToComponentIframe] = {} IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "idom/") IDOM_WS_MAX_RECONNECT_TIMEOUT = getattr( diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py new file mode 100644 index 00000000..61d9f469 --- /dev/null +++ b/src/django_idom/decorators.py @@ -0,0 +1,59 @@ +from inspect import isclass, iscoroutinefunction +from typing import Callable + +from asgiref.sync import async_to_sync +from django.urls import reverse +from idom import component, html, utils +from idom.core.component import Component + +from django_idom.config import IDOM_REGISTERED_IFRAMES +from django_idom.types import ViewToComponentIframe + + +def view_to_component( + middleware: list[Callable | str], compatibility: bool = False, *args, **kwargs +) -> Component | object: + """Converts a Django view to an IDOM component. + + Args: + middleware: The list of middleware to use when rendering the component. + compatibility: If True, the component will be rendered in an iframe. + *args: The positional arguments to pass to the view. + + Keyword Args: + **kwargs: The keyword arguments to pass to the view. + """ + + def decorator(view): + + dotted_path = f"{view.__module__}.{view.__name__}".replace("<", "").replace( + ">", "" + ) + + @component + def new_component(): + if compatibility: + return html.iframe( + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } + ) + + # TODO: Apply middleware using some helper function + if isclass(view): + rendered_view = view.as_view()(*args, **kwargs) + elif iscoroutinefunction(view): + rendered_view = async_to_sync(view)(*args, **kwargs) + else: + rendered_view = view(*args, **kwargs) + + return html._(utils.html_to_vdom(rendered_view)) + + IDOM_REGISTERED_IFRAMES[dotted_path] = ViewToComponentIframe( + middleware, view, new_component, args, kwargs + ) + + return new_component + + return decorator diff --git a/src/django_idom/http/urls.py b/src/django_idom/http/urls.py index 019a603e..c6e8cd1c 100644 --- a/src/django_idom/http/urls.py +++ b/src/django_idom/http/urls.py @@ -10,5 +10,10 @@ "web_module/", views.web_modules_file, name="web_modules", - ) + ), + path( + "iframe/", + views.view_to_component_iframe, + name="view_to_component", + ), ] diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 4134c058..9623cb2f 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -1,11 +1,13 @@ import os +from inspect import isclass, iscoroutinefunction from aiofile import async_open +from channels.db import database_sync_to_async from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest, HttpResponse from idom.config import IDOM_WED_MODULES_DIR -from django_idom.config import IDOM_CACHE +from django_idom.config import IDOM_CACHE, IDOM_REGISTERED_IFRAMES async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: @@ -32,3 +34,22 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: cache_key, response, timeout=None, version=last_modified_time ) return response + + +async def view_to_component_iframe(view_path: str) -> HttpResponse: + """Returns a Django view that was registered by view_to_component. + This is used by `view_to_component` to render the view within an iframe.""" + # Get the view from IDOM_REGISTERED_IFRAMES + iframe = IDOM_REGISTERED_IFRAMES.get(view_path) + if not iframe: + raise ValueError(f"No view registered for component {view_path}.") + + # Render the view + # TODO: Apply middleware using some helper function~ + if isclass(iframe): + return await database_sync_to_async(iframe.view.as_view())() + + if iscoroutinefunction(iframe): + return await iframe.view() + + return await database_sync_to_async(iframe.view)() diff --git a/src/django_idom/types.py b/src/django_idom/types.py new file mode 100644 index 00000000..52efa461 --- /dev/null +++ b/src/django_idom/types.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Callable, Tuple + +from django.views.generic import View +from idom.core.component import Component + + +@dataclass +class ViewToComponentIframe: + middleware: list[Callable | str] + view: View | Callable + component: Component | object + args: Tuple + kwargs: dict From bb8ea09ba26bda2d30d92d26ee090648717f0c94 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 15:46:28 -0700 Subject: [PATCH 029/110] functional compatibility mode --- src/django_idom/__init__.py | 4 +- src/django_idom/config.py | 4 +- src/django_idom/decorators.py | 59 ----------------- src/django_idom/http/views.py | 14 ++-- src/django_idom/types.py | 4 +- src/django_idom/utils.py | 65 ++++++++++++++++++- tests/test_app/components.py | 29 +++++++++ tests/test_app/settings.py | 1 + tests/test_app/templates/base.html | 5 ++ .../test_app/templates/view_to_component.html | 2 + tests/test_app/views.py | 27 ++++++++ 11 files changed, 141 insertions(+), 73 deletions(-) delete mode 100644 src/django_idom/decorators.py create mode 100644 tests/test_app/templates/view_to_component.html diff --git a/src/django_idom/__init__.py b/src/django_idom/__init__.py index fcb25a18..96d8df76 100644 --- a/src/django_idom/__init__.py +++ b/src/django_idom/__init__.py @@ -1,7 +1,7 @@ -from . import components, hooks, decorators +from . import components, hooks, utils from .websocket.consumer import IdomWebsocket from .websocket.paths import IDOM_WEBSOCKET_PATH __version__ = "1.1.0" -__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks", "components", "decorators"] +__all__ = ["IDOM_WEBSOCKET_PATH", "IdomWebsocket", "hooks", "components", "utils"] diff --git a/src/django_idom/config.py b/src/django_idom/config.py index 47f19455..17c0bdb7 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -4,11 +4,11 @@ from django.core.cache import DEFAULT_CACHE_ALIAS, caches from idom.core.types import ComponentConstructor -from django_idom.types import ViewToComponentIframe +from django_idom.types import ViewComponentIframe IDOM_REGISTERED_COMPONENTS: Dict[str, ComponentConstructor] = {} -IDOM_REGISTERED_IFRAMES: Dict[str, ViewToComponentIframe] = {} +IDOM_VIEW_COMPONENT_IFRAMES: Dict[str, ViewComponentIframe] = {} IDOM_WEBSOCKET_URL = getattr(settings, "IDOM_WEBSOCKET_URL", "idom/") IDOM_WS_MAX_RECONNECT_TIMEOUT = getattr( diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py deleted file mode 100644 index 61d9f469..00000000 --- a/src/django_idom/decorators.py +++ /dev/null @@ -1,59 +0,0 @@ -from inspect import isclass, iscoroutinefunction -from typing import Callable - -from asgiref.sync import async_to_sync -from django.urls import reverse -from idom import component, html, utils -from idom.core.component import Component - -from django_idom.config import IDOM_REGISTERED_IFRAMES -from django_idom.types import ViewToComponentIframe - - -def view_to_component( - middleware: list[Callable | str], compatibility: bool = False, *args, **kwargs -) -> Component | object: - """Converts a Django view to an IDOM component. - - Args: - middleware: The list of middleware to use when rendering the component. - compatibility: If True, the component will be rendered in an iframe. - *args: The positional arguments to pass to the view. - - Keyword Args: - **kwargs: The keyword arguments to pass to the view. - """ - - def decorator(view): - - dotted_path = f"{view.__module__}.{view.__name__}".replace("<", "").replace( - ">", "" - ) - - @component - def new_component(): - if compatibility: - return html.iframe( - { - "src": reverse("idom:view_to_component", args=[dotted_path]), - "loading": "lazy", - } - ) - - # TODO: Apply middleware using some helper function - if isclass(view): - rendered_view = view.as_view()(*args, **kwargs) - elif iscoroutinefunction(view): - rendered_view = async_to_sync(view)(*args, **kwargs) - else: - rendered_view = view(*args, **kwargs) - - return html._(utils.html_to_vdom(rendered_view)) - - IDOM_REGISTERED_IFRAMES[dotted_path] = ViewToComponentIframe( - middleware, view, new_component, args, kwargs - ) - - return new_component - - return decorator diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 9623cb2f..5542052a 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -7,7 +7,7 @@ from django.http import HttpRequest, HttpResponse from idom.config import IDOM_WED_MODULES_DIR -from django_idom.config import IDOM_CACHE, IDOM_REGISTERED_IFRAMES +from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: @@ -36,20 +36,22 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: return response -async def view_to_component_iframe(view_path: str) -> HttpResponse: +async def view_to_component_iframe( + request: HttpRequest, view_path: str +) -> HttpResponse: """Returns a Django view that was registered by view_to_component. This is used by `view_to_component` to render the view within an iframe.""" # Get the view from IDOM_REGISTERED_IFRAMES - iframe = IDOM_REGISTERED_IFRAMES.get(view_path) + iframe = IDOM_VIEW_COMPONENT_IFRAMES.get(view_path) if not iframe: raise ValueError(f"No view registered for component {view_path}.") # Render the view # TODO: Apply middleware using some helper function~ if isclass(iframe): - return await database_sync_to_async(iframe.view.as_view())() + return await database_sync_to_async(iframe.view.as_view())(request) if iscoroutinefunction(iframe): - return await iframe.view() + return await iframe.view(request) - return await database_sync_to_async(iframe.view)() + return await database_sync_to_async(iframe.view)(request) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 52efa461..407af065 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -6,8 +6,8 @@ @dataclass -class ViewToComponentIframe: - middleware: list[Callable | str] +class ViewComponentIframe: + middleware: list[Callable | str] | None view: View | Callable component: Component | object args: Tuple diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 81783013..bd9b449a 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -4,18 +4,79 @@ import re from fnmatch import fnmatch from importlib import import_module -from typing import Set +from inspect import isclass, iscoroutinefunction +from typing import Callable, Set +from asgiref.sync import async_to_sync from django.template import engines +from django.urls import reverse from django.utils.encoding import smart_str +from idom import component, html, utils -from django_idom.config import IDOM_REGISTERED_COMPONENTS +from django_idom.config import IDOM_REGISTERED_COMPONENTS, IDOM_VIEW_COMPONENT_IFRAMES +from django_idom.types import ViewComponentIframe COMPONENT_REGEX = re.compile(r"{% *component +((\"[^\"']*\")|('[^\"']*'))(.*?)%}") _logger = logging.getLogger(__name__) +def view_to_component( + view: Callable, + middleware: list[Callable | str] | None = None, + compatibility: bool = False, + *args, + **kwargs, +): + """Converts a Django view to an IDOM component. + + Args: + middleware: The list of middleware to use when rendering the component. + compatibility: If True, the component will be rendered in an iframe. + This requires X_FRAME_OPTIONS = 'SAMEORIGIN' in settings.py. + *args: The positional arguments to pass to the view. + + Keyword Args: + **kwargs: The keyword arguments to pass to the view. + """ + + dotted_path = f"{view.__module__}.{view.__name__}".replace("<", "").replace(">", "") + + @component + def new_component(): + + # Use compatibility mode if requested + if compatibility: + return html.iframe( + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } + ) + + # TODO: Apply middleware using some helper function + if isclass(view): + print("class view") + rendered_view = view.as_view()(*args, **kwargs) + elif iscoroutinefunction(view): + print("async view") + rendered_view = async_to_sync(view)(*args, **kwargs) + else: + print("function view") + rendered_view = view(*args, **kwargs) + + print("vdom") + return html._(utils.html_to_vdom(rendered_view)) + + # Register the component as an iFrame if using compatibility mode + if compatibility: + IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( + middleware, view, new_component, args, kwargs + ) + + return new_component() + + def _register_component(full_component_name: str) -> None: if full_component_name in IDOM_REGISTERED_COMPONENTS: return diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 13939f8b..c39e30d2 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -2,6 +2,8 @@ import django_idom +from . import views + @idom.component def HelloWorld(): @@ -93,3 +95,30 @@ def StaticJS(): django_idom.components.DjangoJS("static-js-test.js"), idom.html.hr(), ) + + +@idom.component +def ViewToComponent(): + return django_idom.utils.view_to_component(views.view_to_component) + + +@idom.component +def ViewToComponentAsync(): + return django_idom.utils.view_to_component(views.view_to_component_async) + + +@idom.component +def ViewToComponentClass(): + return django_idom.utils.view_to_component(views.ViewToComponentClass) + + +@idom.component +def ViewToComponentCompat(): + return django_idom.utils.view_to_component( + views.view_to_component_compat, compatibility=True + ) + + +@idom.component +def ViewToComponentMiddleware(): + return django_idom.utils.view_to_component(views.view_to_component_middleware) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index fbacac81..5aa62076 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -27,6 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ["*"] +X_FRAME_OPTIONS = "SAMEORIGIN" # Application definition INSTALLED_APPS = [ diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index c597cdf9..965facc8 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -21,6 +21,11 @@

IDOM Test Page

{% component "test_app.components.UseLocation" %}
{% component "test_app.components.StaticCSS" %}
{% component "test_app.components.StaticJS" %}
+ + + +
{% component "test_app.components.ViewToComponentCompat" %}
+ diff --git a/tests/test_app/templates/view_to_component.html b/tests/test_app/templates/view_to_component.html new file mode 100644 index 00000000..76ed053c --- /dev/null +++ b/tests/test_app/templates/view_to_component.html @@ -0,0 +1,2 @@ +
{{test_name}}: Success
+
\ No newline at end of file diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 11874908..2cc72456 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -1,6 +1,33 @@ from django.shortcuts import render +from django.views.generic import TemplateView def base_template(request): context = {} return render(request, "base.html", context) + + +def view_to_component(request): + return render(request, "view_to_component.html", {"test_name": "view_to_component"}) + + +async def view_to_component_async(request): + return render( + request, "view_to_component.html", {"test_name": "view_to_component_async"} + ) + + +class ViewToComponentClass(TemplateView): + template_name = "view_to_component.html" + + +def view_to_component_compat(request): + return render( + request, "view_to_component.html", {"test_name": "view_to_component_compat"} + ) + + +def view_to_component_middleware(request): + return render( + request, "view_to_component.html", {"test_name": "view_to_component_middleware"} + ) From e53c15df497d8fdd6eca9f3254e99d9f797d8b12 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 15:57:57 -0700 Subject: [PATCH 030/110] functional standard view to component --- src/django_idom/utils.py | 9 +++++---- tests/test_app/templates/base.html | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index bd9b449a..530a42b4 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -8,6 +8,7 @@ from typing import Callable, Set from asgiref.sync import async_to_sync +from django.http import HttpRequest from django.template import engines from django.urls import reverse from django.utils.encoding import smart_str @@ -57,16 +58,16 @@ def new_component(): # TODO: Apply middleware using some helper function if isclass(view): print("class view") - rendered_view = view.as_view()(*args, **kwargs) + rendered_view = view.as_view()(HttpRequest(), *args, **kwargs) elif iscoroutinefunction(view): print("async view") - rendered_view = async_to_sync(view)(*args, **kwargs) + rendered_view = async_to_sync(view)(HttpRequest(), *args, **kwargs) else: print("function view") - rendered_view = view(*args, **kwargs) + rendered_view = view(HttpRequest(), *args, **kwargs) print("vdom") - return html._(utils.html_to_vdom(rendered_view)) + return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8"))) # Register the component as an iFrame if using compatibility mode if compatibility: diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 965facc8..5b530046 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -21,7 +21,7 @@

IDOM Test Page

{% component "test_app.components.UseLocation" %}
{% component "test_app.components.StaticCSS" %}
{% component "test_app.components.StaticJS" %}
- +
{% component "test_app.components.ViewToComponent" %}
{% component "test_app.components.ViewToComponentCompat" %}
From 86318e50a655fb40bc403e83997a387c580ddee5 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:07:04 -0700 Subject: [PATCH 031/110] class view to component --- src/django_idom/utils.py | 10 ++++------ tests/test_app/components.py | 4 ++-- tests/test_app/templates/base.html | 4 ++-- tests/test_app/views.py | 9 +++++++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 530a42b4..a6c16da5 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -45,7 +45,6 @@ def view_to_component( @component def new_component(): - # Use compatibility mode if requested if compatibility: return html.iframe( @@ -57,16 +56,15 @@ def new_component(): # TODO: Apply middleware using some helper function if isclass(view): - print("class view") - rendered_view = view.as_view()(HttpRequest(), *args, **kwargs) + request = HttpRequest() + request.method = "GET" + rendered_view = view.as_view()(request, *args, **kwargs) + rendered_view.render() elif iscoroutinefunction(view): - print("async view") rendered_view = async_to_sync(view)(HttpRequest(), *args, **kwargs) else: - print("function view") rendered_view = view(HttpRequest(), *args, **kwargs) - print("vdom") return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8"))) # Register the component as an iFrame if using compatibility mode diff --git a/tests/test_app/components.py b/tests/test_app/components.py index c39e30d2..a06be920 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -113,9 +113,9 @@ def ViewToComponentClass(): @idom.component -def ViewToComponentCompat(): +def ViewToComponentCompatibility(): return django_idom.utils.view_to_component( - views.view_to_component_compat, compatibility=True + views.view_to_component_compatibility, compatibility=True ) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 5b530046..2076f2d6 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -23,8 +23,8 @@

IDOM Test Page

{% component "test_app.components.StaticJS" %}
{% component "test_app.components.ViewToComponent" %}
- -
{% component "test_app.components.ViewToComponentCompat" %}
+
{% component "test_app.components.ViewToComponentClass" %}
+
{% component "test_app.components.ViewToComponentCompatibility" %}
diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 2cc72456..40683a09 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -20,10 +20,15 @@ async def view_to_component_async(request): class ViewToComponentClass(TemplateView): template_name = "view_to_component.html" + def get_context_data(self, **kwargs): + return {"test_name": "view_to_component_class"} -def view_to_component_compat(request): + +def view_to_component_compatibility(request): return render( - request, "view_to_component.html", {"test_name": "view_to_component_compat"} + request, + "view_to_component.html", + {"test_name": "view_to_component_compatibility"}, ) From 1e26dfad9d88fe6dcdeb1e2e9907d5ab92025591 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:08:32 -0700 Subject: [PATCH 032/110] fix HR on JS test --- tests/test_app/components.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 13939f8b..54fc3dd8 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -87,9 +87,11 @@ def StaticCSS(): @idom.component def StaticJS(): success = False - return idom.html.div( - {"id": "static-js", "data-success": success}, - f"StaticJS: {success}", - django_idom.components.DjangoJS("static-js-test.js"), + return idom.html._( + idom.html.div( + {"id": "static-js", "data-success": success}, + f"StaticJS: {success}", + django_idom.components.DjangoJS("static-js-test.js"), + ), idom.html.hr(), ) From 823600e3b51024dfc40cf2e9cc42a23f8ea42938 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:15:37 -0700 Subject: [PATCH 033/110] add ID to compat --- tests/test_app/components.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 74b8090b..0e362d5d 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -116,8 +116,11 @@ def ViewToComponentClass(): @idom.component def ViewToComponentCompatibility(): - return django_idom.utils.view_to_component( - views.view_to_component_compatibility, compatibility=True + return idom.html.div( + {"id": "view_to_component_compatibility"}, + django_idom.utils.view_to_component( + views.view_to_component_compatibility, compatibility=True + ), ) From fa8efd310edb931d292f03daa0d79a62b0f1a364 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:15:46 -0700 Subject: [PATCH 034/110] type hinting cleanup --- src/django_idom/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index a6c16da5..de077423 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -13,6 +13,7 @@ from django.urls import reverse from django.utils.encoding import smart_str from idom import component, html, utils +from idom.types import ComponentType from django_idom.config import IDOM_REGISTERED_COMPONENTS, IDOM_VIEW_COMPONENT_IFRAMES from django_idom.types import ViewComponentIframe @@ -28,7 +29,7 @@ def view_to_component( compatibility: bool = False, *args, **kwargs, -): +) -> ComponentType: """Converts a Django view to an IDOM component. Args: From 1cdc5c2abe30afd737fe0035bba5eb06e44e61ab Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:56:41 -0700 Subject: [PATCH 035/110] ViewToComponentAsync --- src/django_idom/utils.py | 24 +++++++++++++++++++----- tests/test_app/templates/base.html | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index de077423..64be1f56 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -7,12 +7,11 @@ from inspect import isclass, iscoroutinefunction from typing import Callable, Set -from asgiref.sync import async_to_sync from django.http import HttpRequest from django.template import engines from django.urls import reverse from django.utils.encoding import smart_str -from idom import component, html, utils +from idom import component, hooks, html, utils from idom.types import ComponentType from django_idom.config import IDOM_REGISTERED_COMPONENTS, IDOM_VIEW_COMPONENT_IFRAMES @@ -46,7 +45,20 @@ def view_to_component( @component def new_component(): - # Use compatibility mode if requested + # Hack for getting around some of Django's Async/Sync protections + async_view = False + async_render, set_async_render = hooks.use_state(None) + if async_render: + return html._(utils.html_to_vdom(async_render.content.decode("utf-8"))) + + async def async_renderer(): + if async_view is True and not async_render: + rendered_view = await view(HttpRequest(), *args, **kwargs) + set_async_render(rendered_view) + + hooks.use_effect(async_renderer, dependencies=[async_view]) + + # Generate an iFrame component for compatibility, if requested if compatibility: return html.iframe( { @@ -55,6 +67,7 @@ def new_component(): } ) + # Convert the view HTML to VDOM # TODO: Apply middleware using some helper function if isclass(view): request = HttpRequest() @@ -62,13 +75,14 @@ def new_component(): rendered_view = view.as_view()(request, *args, **kwargs) rendered_view.render() elif iscoroutinefunction(view): - rendered_view = async_to_sync(view)(HttpRequest(), *args, **kwargs) + async_view = True + return None else: rendered_view = view(HttpRequest(), *args, **kwargs) return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8"))) - # Register the component as an iFrame if using compatibility mode + # Register the iFrame component for compatibility, if requested if compatibility: IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( middleware, view, new_component, args, kwargs diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 2076f2d6..22038c5b 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -22,7 +22,7 @@

IDOM Test Page

{% component "test_app.components.StaticCSS" %}
{% component "test_app.components.StaticJS" %}
{% component "test_app.components.ViewToComponent" %}
- +
{% component "test_app.components.ViewToComponentAsync" %}
{% component "test_app.components.ViewToComponentClass" %}
{% component "test_app.components.ViewToComponentCompatibility" %}
From 643940ef2c63d2bd1c28bd98e2ce6ee812e1c9cd Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:20:54 -0700 Subject: [PATCH 036/110] ViewToComponentMiddleware --- src/django_idom/utils.py | 64 +++++++++++++++++++++++------- tests/test_app/components.py | 17 +++++++- tests/test_app/templates/base.html | 3 +- tests/test_app/views.py | 8 +++- 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 64be1f56..66bdcbb9 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -26,6 +26,7 @@ def view_to_component( view: Callable, middleware: list[Callable | str] | None = None, compatibility: bool = False, + request: HttpRequest | None = None, *args, **kwargs, ) -> ComponentType: @@ -35,6 +36,7 @@ def view_to_component( middleware: The list of middleware to use when rendering the component. compatibility: If True, the component will be rendered in an iframe. This requires X_FRAME_OPTIONS = 'SAMEORIGIN' in settings.py. + request: Request object to provide to the view. *args: The positional arguments to pass to the view. Keyword Args: @@ -45,7 +47,14 @@ def view_to_component( @component def new_component(): + # Create a synthetic request object. + request_obj = request + if not request: + request_obj = HttpRequest() + request_obj.method = "GET" + # Hack for getting around some of Django's Async/Sync protections + # Without this, we wouldn't be able to render async views within components async_view = False async_render, set_async_render = hooks.use_state(None) if async_render: @@ -53,12 +62,14 @@ def new_component(): async def async_renderer(): if async_view is True and not async_render: - rendered_view = await view(HttpRequest(), *args, **kwargs) + rendered_view = await _view_middleware(middleware, view)( + request_obj, *args, **kwargs + ) set_async_render(rendered_view) hooks.use_effect(async_renderer, dependencies=[async_view]) - # Generate an iFrame component for compatibility, if requested + # Generate an iframe component for compatibility, if requested if compatibility: return html.iframe( { @@ -70,19 +81,23 @@ async def async_renderer(): # Convert the view HTML to VDOM # TODO: Apply middleware using some helper function if isclass(view): - request = HttpRequest() - request.method = "GET" - rendered_view = view.as_view()(request, *args, **kwargs) + rendered_view = _view_middleware(middleware, view.as_view())( + request_obj, *args, **kwargs + ) rendered_view.render() elif iscoroutinefunction(view): + # Queue the view to be rendered within an UseEffect hook due to + # async/sync limitations async_view = True return None else: - rendered_view = view(HttpRequest(), *args, **kwargs) + rendered_view = _view_middleware(middleware, view)( + request_obj, *args, **kwargs + ) return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8"))) - # Register the iFrame component for compatibility, if requested + # Register the iframe component for compatibility, if requested if compatibility: IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( middleware, view, new_component, args, kwargs @@ -91,11 +106,27 @@ async def async_renderer(): return new_component() -def _register_component(full_component_name: str) -> None: - if full_component_name in IDOM_REGISTERED_COMPONENTS: - return +def _view_middleware( + middleware: list[Callable | str] | None, view: Callable +) -> Callable: + """Applies middleware to a view.""" + if not middleware: + return view + + def _wrapper(*args, **kwargs): + new_view = view + for middleware_item in middleware: + if isinstance(middleware_item, str): + middleware_item = _import_dotted_path(middleware_item) + new_view = middleware_item(new_view)(*args, **kwargs) + return new_view - module_name, component_name = full_component_name.rsplit(".", 1) + return _wrapper + + +def _import_dotted_path(dotted_path: str) -> Callable: + """Imports a dotted path and returns the callable.""" + module_name, component_name = dotted_path.rsplit(".", 1) try: module = import_module(module_name) @@ -105,14 +136,19 @@ def _register_component(full_component_name: str) -> None: ) from error try: - component = getattr(module, component_name) + return getattr(module, component_name) except AttributeError as error: raise RuntimeError( f"Module {module_name!r} has no component named {component_name!r}" ) from error - IDOM_REGISTERED_COMPONENTS[full_component_name] = component - _logger.debug("IDOM has registered component %s", full_component_name) + +def _register_component(dotted_path: str) -> None: + if dotted_path in IDOM_REGISTERED_COMPONENTS: + return + + IDOM_REGISTERED_COMPONENTS[dotted_path] = _import_dotted_path(dotted_path) + _logger.debug("IDOM has registered component %s", dotted_path) class ComponentPreloader: diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 0e362d5d..b4b631ee 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -121,9 +121,24 @@ def ViewToComponentCompatibility(): django_idom.utils.view_to_component( views.view_to_component_compatibility, compatibility=True ), + idom.html.hr(), ) @idom.component def ViewToComponentMiddleware(): - return django_idom.utils.view_to_component(views.view_to_component_middleware) + def str_replace_middleware(view): + def middleware(request, *args, **kwargs): + render = view(request, *args, **kwargs) + render.content = render.content.decode("utf-8").replace("_not_working", "") + return render + return middleware + + return django_idom.utils.view_to_component( + views.view_to_component_middleware, middleware=[str_replace_middleware] + ) + + +@idom.component +def ViewToComponentScripts(): + return django_idom.utils.view_to_component(views.view_to_component_scripts) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 22038c5b..6a6d2d75 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -25,7 +25,8 @@

IDOM Test Page

{% component "test_app.components.ViewToComponentAsync" %}
{% component "test_app.components.ViewToComponentClass" %}
{% component "test_app.components.ViewToComponentCompatibility" %}
- +
{% component "test_app.components.ViewToComponentMiddleware" %}
+ diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 40683a09..be845f75 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -34,5 +34,11 @@ def view_to_component_compatibility(request): def view_to_component_middleware(request): return render( - request, "view_to_component.html", {"test_name": "view_to_component_middleware"} + request, "view_to_component.html", {"test_name": "view_to_component_middleware_not_working"} + ) + + +def view_to_component_scripts(request): + return render( + request, "view_to_component.html", {"test_name": "view_to_component_scripts_not_working"} ) From 683300887392274a162bfcd95a739a33711ef142 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:26:58 -0700 Subject: [PATCH 037/110] fix flake8 warning --- src/django_idom/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 66bdcbb9..d0d0d696 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -7,11 +7,12 @@ from inspect import isclass, iscoroutinefunction from typing import Callable, Set +import idom from django.http import HttpRequest from django.template import engines from django.urls import reverse from django.utils.encoding import smart_str -from idom import component, hooks, html, utils +from idom import hooks, html, utils from idom.types import ComponentType from django_idom.config import IDOM_REGISTERED_COMPONENTS, IDOM_VIEW_COMPONENT_IFRAMES @@ -45,7 +46,7 @@ def view_to_component( dotted_path = f"{view.__module__}.{view.__name__}".replace("<", "").replace(">", "") - @component + @idom.component def new_component(): # Create a synthetic request object. request_obj = request From 5371b24c62a6c60dd0c539a0a24b2b7b96a5b470 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:28:40 -0700 Subject: [PATCH 038/110] format tests folder --- tests/test_app/components.py | 1 + tests/test_app/views.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index b4b631ee..db053d89 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -132,6 +132,7 @@ def middleware(request, *args, **kwargs): render = view(request, *args, **kwargs) render.content = render.content.decode("utf-8").replace("_not_working", "") return render + return middleware return django_idom.utils.view_to_component( diff --git a/tests/test_app/views.py b/tests/test_app/views.py index be845f75..13f807c3 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -34,11 +34,15 @@ def view_to_component_compatibility(request): def view_to_component_middleware(request): return render( - request, "view_to_component.html", {"test_name": "view_to_component_middleware_not_working"} + request, + "view_to_component.html", + {"test_name": "view_to_component_middleware_not_working"}, ) def view_to_component_scripts(request): return render( - request, "view_to_component.html", {"test_name": "view_to_component_scripts_not_working"} + request, + "view_to_component.html", + {"test_name": "view_to_component_scripts_not_working"}, ) From a2cd686bcf9a9c768c898da32f4d4fbcd6d7c2b6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 20:54:14 -0700 Subject: [PATCH 039/110] ViewToComponentScript --- src/django_idom/utils.py | 2 +- tests/test_app/components.py | 6 +++--- tests/test_app/templates/base.html | 2 +- tests/test_app/templates/view_to_component.html | 4 ++-- tests/test_app/templates/view_to_component_script.html | 8 ++++++++ tests/test_app/views.py | 8 ++++---- 6 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 tests/test_app/templates/view_to_component_script.html diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index d0d0d696..29165640 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -96,7 +96,7 @@ async def async_renderer(): request_obj, *args, **kwargs ) - return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8"))) + return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8").strip())) # Register the iframe component for compatibility, if requested if compatibility: diff --git a/tests/test_app/components.py b/tests/test_app/components.py index db053d89..7a78f69c 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -130,7 +130,7 @@ def ViewToComponentMiddleware(): def str_replace_middleware(view): def middleware(request, *args, **kwargs): render = view(request, *args, **kwargs) - render.content = render.content.decode("utf-8").replace("_not_working", "") + render.content = render.content.decode("utf-8").replace("Fail", "Success") return render return middleware @@ -141,5 +141,5 @@ def middleware(request, *args, **kwargs): @idom.component -def ViewToComponentScripts(): - return django_idom.utils.view_to_component(views.view_to_component_scripts) +def ViewToComponentScript(): + return django_idom.utils.view_to_component(views.view_to_component_script) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 6a6d2d75..63f00908 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -26,7 +26,7 @@

IDOM Test Page

{% component "test_app.components.ViewToComponentClass" %}
{% component "test_app.components.ViewToComponentCompatibility" %}
{% component "test_app.components.ViewToComponentMiddleware" %}
- +
{% component "test_app.components.ViewToComponentScript" %}
diff --git a/tests/test_app/templates/view_to_component.html b/tests/test_app/templates/view_to_component.html index 76ed053c..7ed7d406 100644 --- a/tests/test_app/templates/view_to_component.html +++ b/tests/test_app/templates/view_to_component.html @@ -1,2 +1,2 @@ -
{{test_name}}: Success
-
\ No newline at end of file +
{{ test_name}}: {% firstof status "Success" %}
+
{% block bottom %}{% endblock %} \ No newline at end of file diff --git a/tests/test_app/templates/view_to_component_script.html b/tests/test_app/templates/view_to_component_script.html new file mode 100644 index 00000000..f63a0a56 --- /dev/null +++ b/tests/test_app/templates/view_to_component_script.html @@ -0,0 +1,8 @@ +{% extends "view_to_component.html" %} +{% block bottom %} + +{% endblock %} \ No newline at end of file diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 13f807c3..edea425a 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -36,13 +36,13 @@ def view_to_component_middleware(request): return render( request, "view_to_component.html", - {"test_name": "view_to_component_middleware_not_working"}, + {"test_name": "view_to_component_middleware", "status": "Fail"}, ) -def view_to_component_scripts(request): +def view_to_component_script(request): return render( request, - "view_to_component.html", - {"test_name": "view_to_component_scripts_not_working"}, + "view_to_component_script.html", + {"test_name": "view_to_component_script", "status": "Fail"}, ) From 1dda52d01022cca47da25610c2cf97a45c6a34d3 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 21:00:53 -0700 Subject: [PATCH 040/110] swap pipe operator with union --- src/django_idom/types.py | 8 ++++---- src/django_idom/utils.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 407af065..673ed207 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Callable, Tuple +from typing import Callable, Tuple, Union from django.views.generic import View from idom.core.component import Component @@ -7,8 +7,8 @@ @dataclass class ViewComponentIframe: - middleware: list[Callable | str] | None - view: View | Callable - component: Component | object + middleware: Union[list[Union[Callable, str]], None] + view: Union[View, Callable] + component: Union[Component, object] args: Tuple kwargs: dict diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 29165640..34651d10 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -5,7 +5,7 @@ from fnmatch import fnmatch from importlib import import_module from inspect import isclass, iscoroutinefunction -from typing import Callable, Set +from typing import Callable, Set, Union import idom from django.http import HttpRequest @@ -25,9 +25,9 @@ def view_to_component( view: Callable, - middleware: list[Callable | str] | None = None, + middleware: Union[list[Union[Callable, str]], None] = None, compatibility: bool = False, - request: HttpRequest | None = None, + request: Union[HttpRequest, None] = None, *args, **kwargs, ) -> ComponentType: @@ -108,7 +108,7 @@ async def async_renderer(): def _view_middleware( - middleware: list[Callable | str] | None, view: Callable + middleware: Union[list[Union[Callable, str]], None], view: Callable ) -> Callable: """Applies middleware to a view.""" if not middleware: From ada489c425b08c7aa8e300870c2ffcb566574b58 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 21:05:26 -0700 Subject: [PATCH 041/110] swap list with typing.List --- src/django_idom/types.py | 4 ++-- src/django_idom/utils.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 673ed207..f1792d15 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Callable, Tuple, Union +from typing import Callable, List, Tuple, Union from django.views.generic import View from idom.core.component import Component @@ -7,7 +7,7 @@ @dataclass class ViewComponentIframe: - middleware: Union[list[Union[Callable, str]], None] + middleware: Union[List[Union[Callable, str]], None] view: Union[View, Callable] component: Union[Component, object] args: Tuple diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 34651d10..089b7ca4 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -5,7 +5,7 @@ from fnmatch import fnmatch from importlib import import_module from inspect import isclass, iscoroutinefunction -from typing import Callable, Set, Union +from typing import Callable, List, Set, Union import idom from django.http import HttpRequest @@ -25,7 +25,7 @@ def view_to_component( view: Callable, - middleware: Union[list[Union[Callable, str]], None] = None, + middleware: Union[List[Union[Callable, str]], None] = None, compatibility: bool = False, request: Union[HttpRequest, None] = None, *args, @@ -108,7 +108,7 @@ async def async_renderer(): def _view_middleware( - middleware: Union[list[Union[Callable, str]], None], view: Callable + middleware: Union[List[Union[Callable, str]], None], view: Callable ) -> Callable: """Applies middleware to a view.""" if not middleware: From 3aca1377d35ad1170d804694221b56dd98846dc1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:00:11 -0700 Subject: [PATCH 042/110] minor refactoring --- src/django_idom/utils.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 089b7ca4..a7783721 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -54,12 +54,21 @@ def new_component(): request_obj = HttpRequest() request_obj.method = "GET" + # Generate an iframe if compatibility mode is enabled. + if compatibility: + return html.iframe( + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } + ) + # Hack for getting around some of Django's Async/Sync protections # Without this, we wouldn't be able to render async views within components async_view = False async_render, set_async_render = hooks.use_state(None) if async_render: - return html._(utils.html_to_vdom(async_render.content.decode("utf-8"))) + return html._(utils.html_to_vdom(async_render.content.decode("utf-8").strip())) async def async_renderer(): if async_view is True and not async_render: @@ -70,15 +79,6 @@ async def async_renderer(): hooks.use_effect(async_renderer, dependencies=[async_view]) - # Generate an iframe component for compatibility, if requested - if compatibility: - return html.iframe( - { - "src": reverse("idom:view_to_component", args=[dotted_path]), - "loading": "lazy", - } - ) - # Convert the view HTML to VDOM # TODO: Apply middleware using some helper function if isclass(view): @@ -98,7 +98,7 @@ async def async_renderer(): return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8").strip())) - # Register the iframe component for compatibility, if requested + # Register an iframe if compatibility mode is enabled if compatibility: IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( middleware, view, new_component, args, kwargs From df6202bc2995283052642b5ec7ffdc971723c93e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:11:38 -0700 Subject: [PATCH 043/110] Add todos --- src/django_idom/http/views.py | 1 - src/django_idom/utils.py | 1 - tests/test_app/templates/base.html | 6 +++++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 5542052a..223fc75a 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -47,7 +47,6 @@ async def view_to_component_iframe( raise ValueError(f"No view registered for component {view_path}.") # Render the view - # TODO: Apply middleware using some helper function~ if isclass(iframe): return await database_sync_to_async(iframe.view.as_view())(request) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index a7783721..91996dde 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -80,7 +80,6 @@ async def async_renderer(): hooks.use_effect(async_renderer, dependencies=[async_view]) # Convert the view HTML to VDOM - # TODO: Apply middleware using some helper function if isclass(view): rendered_view = _view_middleware(middleware, view.as_view())( request_obj, *args, **kwargs diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 63f00908..703aeffd 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -22,11 +22,15 @@

IDOM Test Page

{% component "test_app.components.StaticCSS" %}
{% component "test_app.components.StaticJS" %}
{% component "test_app.components.ViewToComponent" %}
+
{% component "test_app.components.ViewToComponentMiddleware" %}
{% component "test_app.components.ViewToComponentAsync" %}
+
{% component "test_app.components.ViewToComponentClass" %}
+
{% component "test_app.components.ViewToComponentCompatibility" %}
-
{% component "test_app.components.ViewToComponentMiddleware" %}
+
{% component "test_app.components.ViewToComponentScript" %}
+ From b1bf561f3d8ae23566839eea27c9807331677002 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:17:06 -0700 Subject: [PATCH 044/110] format --- src/django_idom/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 91996dde..439c1177 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -68,7 +68,9 @@ def new_component(): async_view = False async_render, set_async_render = hooks.use_state(None) if async_render: - return html._(utils.html_to_vdom(async_render.content.decode("utf-8").strip())) + return html._( + utils.html_to_vdom(async_render.content.decode("utf-8").strip()) + ) async def async_renderer(): if async_view is True and not async_render: From a245b86c2396924bbc70da279cd98f5a70766da0 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 30 Jun 2022 06:14:35 -0700 Subject: [PATCH 045/110] fix spacing --- tests/test_app/templates/view_to_component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app/templates/view_to_component.html b/tests/test_app/templates/view_to_component.html index 7ed7d406..b0d02da6 100644 --- a/tests/test_app/templates/view_to_component.html +++ b/tests/test_app/templates/view_to_component.html @@ -1,2 +1,2 @@ -
{{ test_name}}: {% firstof status "Success" %}
+
{{ test_name }}: {% firstof status "Success" %}

{% block bottom %}{% endblock %} \ No newline at end of file From 1f32edc92929c5259c422bcf707859705fc6736c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 1 Jul 2022 17:30:19 -0700 Subject: [PATCH 046/110] docstring changes --- src/django_idom/http/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 223fc75a..3e595e00 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -39,8 +39,8 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: async def view_to_component_iframe( request: HttpRequest, view_path: str ) -> HttpResponse: - """Returns a Django view that was registered by view_to_component. - This is used by `view_to_component` to render the view within an iframe.""" + """Returns a view that was registered by view_to_component. + This view is intended to be used as iframe, for compatibility purposes.""" # Get the view from IDOM_REGISTERED_IFRAMES iframe = IDOM_VIEW_COMPONENT_IFRAMES.get(view_path) if not iframe: @@ -49,7 +49,6 @@ async def view_to_component_iframe( # Render the view if isclass(iframe): return await database_sync_to_async(iframe.view.as_view())(request) - if iscoroutinefunction(iframe): return await iframe.view(request) From 68ce85efd064a3d8b393e2267a38793876217b86 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 4 Jul 2022 07:08:26 -0700 Subject: [PATCH 047/110] type hint fixes --- src/django_idom/utils.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 439c1177..3a0aa24f 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -4,7 +4,7 @@ import re from fnmatch import fnmatch from importlib import import_module -from inspect import isclass, iscoroutinefunction +from inspect import iscoroutinefunction from typing import Callable, List, Set, Union import idom @@ -12,6 +12,7 @@ from django.template import engines from django.urls import reverse from django.utils.encoding import smart_str +from django.views import View from idom import hooks, html, utils from idom.types import ComponentType @@ -24,7 +25,7 @@ def view_to_component( - view: Callable, + view: Union[Callable, View], middleware: Union[List[Union[Callable, str]], None] = None, compatibility: bool = False, request: Union[HttpRequest, None] = None, @@ -34,7 +35,8 @@ def view_to_component( """Converts a Django view to an IDOM component. Args: - middleware: The list of middleware to use when rendering the component. + middleware: The list of Django middleware to use when rendering the view. + This can either be a list of middleware functions or string dotted paths. compatibility: If True, the component will be rendered in an iframe. This requires X_FRAME_OPTIONS = 'SAMEORIGIN' in settings.py. request: Request object to provide to the view. @@ -72,6 +74,7 @@ def new_component(): utils.html_to_vdom(async_render.content.decode("utf-8").strip()) ) + @hooks.use_effect(dependencies=[async_view]) async def async_renderer(): if async_view is True and not async_render: rendered_view = await _view_middleware(middleware, view)( @@ -79,10 +82,8 @@ async def async_renderer(): ) set_async_render(rendered_view) - hooks.use_effect(async_renderer, dependencies=[async_view]) - # Convert the view HTML to VDOM - if isclass(view): + if isinstance(view, View): rendered_view = _view_middleware(middleware, view.as_view())( request_obj, *args, **kwargs ) @@ -109,7 +110,7 @@ async def async_renderer(): def _view_middleware( - middleware: Union[List[Union[Callable, str]], None], view: Callable + middleware: Union[List[Union[Callable, str]], None], view: Union[Callable, View] ) -> Callable: """Applies middleware to a view.""" if not middleware: From 761246612450e7086fbe4b6ca349907c29bfcd0f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 4 Jul 2022 08:42:45 -0700 Subject: [PATCH 048/110] clean up learn more section --- docs/getting-started/learn-more.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/learn-more.md b/docs/getting-started/learn-more.md index abbc1099..31449ea9 100644 --- a/docs/getting-started/learn-more.md +++ b/docs/getting-started/learn-more.md @@ -2,10 +2,10 @@ If you followed the previous steps, you've now created a "Hello World" component! -The docs you are reading only covers our Django integration. +The docs you are reading only covers our Django integration. To learn more about features, such as interactive events and hooks, check out the [IDOM Core Documentation](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html)! -To learn more about our advanced features, such as interactive events and hooks, check out the [IDOM Core Documentation](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html)! +Additionally, the vast majority of tutorials/guides you find for React can be applied to IDOM. | Learn More | | --- | -| [Django-IDOM — Exclusive Features](../features/hooks.md){ .md-button } [IDOM Core — Hooks, Events, and More](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html){ .md-button } | +| [Django-IDOM Exclusive Features](../features/hooks.md){ .md-button } [IDOM Hooks, Events, and More](https://idom-docs.herokuapp.com/docs/guides/creating-interfaces/index.html){ .md-button } [Ask Questions on GitHub Discussions](https://github.com/idom-team/idom/discussions){ .md-button .md-button--primary } | From 3fa65ae79f67e1093a9ca16b90340136bbb035c6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 2 Aug 2022 15:59:24 -0700 Subject: [PATCH 049/110] fix class based views --- src/django_idom/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 3a0aa24f..73abf979 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -83,7 +83,7 @@ async def async_renderer(): set_async_render(rendered_view) # Convert the view HTML to VDOM - if isinstance(view, View): + if getattr(view, "as_view", None): rendered_view = _view_middleware(middleware, view.as_view())( request_obj, *args, **kwargs ) From 49b928f304ba666fe7a827a9e73e455f1a0fd721 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sat, 13 Aug 2022 23:20:46 -0700 Subject: [PATCH 050/110] bump idom --- requirements/pkg-deps.txt | 2 +- src/django_idom/hooks.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index 08e42fad..41f0b6da 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,3 +1,3 @@ channels >=3.0.0 -idom >=0.39.0, <0.40.0 +idom >=0.40.0, <0.41.0 aiofile >=3.0 diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 79ce1d16..f938a5de 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -6,9 +6,7 @@ from django_idom.types import IdomWebsocket -WebsocketContext: Type[Context[Union[IdomWebsocket, None]]] = create_context( - None, "WebSocketContext" -) +WebsocketContext: Context[IdomWebsocket | None] = create_context(None) def use_location() -> Location: From 5d882e21f1e10839913e1cf49682d7eb8c851a10 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 14 Aug 2022 01:57:22 -0700 Subject: [PATCH 051/110] add fixme --- tests/test_app/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 87d54fe8..7bbbc2fa 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -154,7 +154,7 @@ def view_to_component_compatibility(): django_idom.utils.view_to_component( views.view_to_component_compatibility, compatibility=True ), - idom.html.hr(), + idom.html.hr(), # FIXME: Looks like elements can't follow an iframe ) From 848c51d7dd737532b6355d601e1c53e5612da1be Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 14 Aug 2022 01:59:54 -0700 Subject: [PATCH 052/110] simplify if expression --- src/django_idom/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 73abf979..caf615ed 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -65,7 +65,7 @@ def new_component(): } ) - # Hack for getting around some of Django's Async/Sync protections + # Hack for getting around some of Django's async/sync protections # Without this, we wouldn't be able to render async views within components async_view = False async_render, set_async_render = hooks.use_state(None) @@ -76,7 +76,7 @@ def new_component(): @hooks.use_effect(dependencies=[async_view]) async def async_renderer(): - if async_view is True and not async_render: + if async_view and not async_render: rendered_view = await _view_middleware(middleware, view)( request_obj, *args, **kwargs ) From 0796d4f08a9b3c62dae46c981c88abd065ffe63d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 14 Aug 2022 02:26:25 -0700 Subject: [PATCH 053/110] Py 3.8 compatibility --- src/django_idom/hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index f938a5de..51604e98 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -1,4 +1,4 @@ -from typing import Dict, Type, Union +from typing import Dict, Union from idom.backend.types import Location from idom.core.hooks import Context, create_context, use_context @@ -6,7 +6,7 @@ from django_idom.types import IdomWebsocket -WebsocketContext: Context[IdomWebsocket | None] = create_context(None) +WebsocketContext: Context[Union[IdomWebsocket, None]] = create_context(None) def use_location() -> Location: From 7be948f31a5ab6896e41cffc8b36526a927f2d0b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 14 Aug 2022 13:57:06 -0700 Subject: [PATCH 054/110] comments --- src/django_idom/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index caf615ed..c92ecb6b 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -84,16 +84,19 @@ async def async_renderer(): # Convert the view HTML to VDOM if getattr(view, "as_view", None): + # Django Class Based Views + # TODO: Support async views rendered_view = _view_middleware(middleware, view.as_view())( request_obj, *args, **kwargs ) rendered_view.render() elif iscoroutinefunction(view): - # Queue the view to be rendered within an UseEffect hook due to - # async/sync limitations + # Async Django Functions + # Queuer render within an async hook due to async/sync limitations async_view = True return None else: + # Django Function Based Views rendered_view = _view_middleware(middleware, view)( request_obj, *args, **kwargs ) From b249fe269a4c34fe75ff91f616adf939ecb33186 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 01:21:36 -0700 Subject: [PATCH 055/110] major view to component refactoring --- src/django_idom/http/views.py | 9 ++- src/django_idom/types.py | 1 - src/django_idom/utils.py | 118 +++++++++++++++++----------------- tests/test_app/components.py | 2 +- tests/test_app/settings.py | 1 - 5 files changed, 65 insertions(+), 66 deletions(-) diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 3e595e00..8a72e0d7 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -5,6 +5,7 @@ from channels.db import database_sync_to_async from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest, HttpResponse +from django.views.decorators.clickjacking import xframe_options_sameorigin from idom.config import IDOM_WED_MODULES_DIR from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES @@ -48,8 +49,10 @@ async def view_to_component_iframe( # Render the view if isclass(iframe): - return await database_sync_to_async(iframe.view.as_view())(request) + return await database_sync_to_async( + xframe_options_sameorigin(iframe.view.as_view)() + )(request) if iscoroutinefunction(iframe): - return await iframe.view(request) + return await xframe_options_sameorigin(iframe.view)(request) - return await database_sync_to_async(iframe.view)(request) + return await database_sync_to_async(xframe_options_sameorigin(iframe.view))(request) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index dd36e5c5..ad2a4145 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -17,6 +17,5 @@ class IdomWebsocket: class ViewComponentIframe: middleware: Union[List[Union[Callable, str]], None] view: Union[View, Callable] - component: Union[Component, object] args: Tuple kwargs: dict diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index c92ecb6b..c156c47b 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -7,14 +7,14 @@ from inspect import iscoroutinefunction from typing import Callable, List, Set, Union -import idom from django.http import HttpRequest from django.template import engines from django.urls import reverse from django.utils.encoding import smart_str from django.views import View -from idom import hooks, html, utils -from idom.types import ComponentType +from idom import component, hooks, html, utils +from idom.types import VdomDict +from channels.db import database_sync_to_async from django_idom.config import IDOM_REGISTERED_COMPONENTS, IDOM_VIEW_COMPONENT_IFRAMES from django_idom.types import ViewComponentIframe @@ -24,6 +24,7 @@ _logger = logging.getLogger(__name__) +@component def view_to_component( view: Union[Callable, View], middleware: Union[List[Union[Callable, str]], None] = None, @@ -31,85 +32,80 @@ def view_to_component( request: Union[HttpRequest, None] = None, *args, **kwargs, -) -> ComponentType: +) -> Union[VdomDict, None]: """Converts a Django view to an IDOM component. Args: middleware: The list of Django middleware to use when rendering the view. This can either be a list of middleware functions or string dotted paths. compatibility: If True, the component will be rendered in an iframe. - This requires X_FRAME_OPTIONS = 'SAMEORIGIN' in settings.py. request: Request object to provide to the view. *args: The positional arguments to pass to the view. Keyword Args: **kwargs: The keyword arguments to pass to the view. """ + # Return the view if it's been rendered via the async_renderer + rendered_view, set_rendered_view = hooks.use_state(None) + if rendered_view: + return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8").strip())) - dotted_path = f"{view.__module__}.{view.__name__}".replace("<", "").replace(">", "") - - @idom.component - def new_component(): - # Create a synthetic request object. - request_obj = request - if not request: - request_obj = HttpRequest() - request_obj.method = "GET" - - # Generate an iframe if compatibility mode is enabled. - if compatibility: - return html.iframe( - { - "src": reverse("idom:view_to_component", args=[dotted_path]), - "loading": "lazy", - } - ) + # Create a synthetic request object. + request_obj = request + if not request: + request_obj = HttpRequest() + # TODO: Figure out some intelligent way to set the method. + # Might need intercepting common things such as form submission. + request_obj.method = "GET" - # Hack for getting around some of Django's async/sync protections - # Without this, we wouldn't be able to render async views within components - async_view = False - async_render, set_async_render = hooks.use_state(None) - if async_render: - return html._( - utils.html_to_vdom(async_render.content.decode("utf-8").strip()) + # Render Check 1: Compatibility mode + if compatibility: + dotted_path = f"{view.__module__}.{view.__name__}" + dotted_path = dotted_path.replace("<", "").replace(">", "") + + # Register the iframe's URL if needed + if not IDOM_VIEW_COMPONENT_IFRAMES.get(dotted_path): + IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( + middleware, view, args, kwargs ) - @hooks.use_effect(dependencies=[async_view]) - async def async_renderer(): - if async_view and not async_render: - rendered_view = await _view_middleware(middleware, view)( - request_obj, *args, **kwargs - ) - set_async_render(rendered_view) + return html.iframe( + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } + ) - # Convert the view HTML to VDOM - if getattr(view, "as_view", None): - # Django Class Based Views - # TODO: Support async views - rendered_view = _view_middleware(middleware, view.as_view())( - request_obj, *args, **kwargs - ) - rendered_view.render() - elif iscoroutinefunction(view): - # Async Django Functions - # Queuer render within an async hook due to async/sync limitations - async_view = True - return None - else: - # Django Function Based Views - rendered_view = _view_middleware(middleware, view)( + # Render the view in an async hook to avoid blocking the main thread + @hooks.use_effect(dependencies=[rendered_view]) + async def async_renderer(): + if rendered_view: + return + + # Render Check 2: Async function view + if iscoroutinefunction(view): + render = await _view_middleware(middleware, view)( request_obj, *args, **kwargs ) - return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8").strip())) + # Render Check 3: Async class based view + # TODO: Support Django 4.1 async CBV + elif getattr(view, "view_is_async", False): + return - # Register an iframe if compatibility mode is enabled - if compatibility: - IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( - middleware, view, new_component, args, kwargs - ) + # Render Check 3: Sync class based view + # TODO: Make this async, maybe through database_sync_to_async on a helper function? + elif getattr(view, "as_view", None): + render = _view_middleware(middleware, view.as_view())( + request_obj, *args, **kwargs + ).render() + + # Render Check 4: Sync function based view + # TODO: Make this async, maybe through database_sync_to_async on a helper function? + else: + render = _view_middleware(middleware, view)(request_obj, *args, **kwargs) - return new_component() + set_rendered_view(render) def _view_middleware( @@ -124,6 +120,8 @@ def _wrapper(*args, **kwargs): for middleware_item in middleware: if isinstance(middleware_item, str): middleware_item = _import_dotted_path(middleware_item) + + # TODO: Consider doing this async if needed. Also consider class based middleware. new_view = middleware_item(new_view)(*args, **kwargs) return new_view diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 7bbbc2fa..87d54fe8 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -154,7 +154,7 @@ def view_to_component_compatibility(): django_idom.utils.view_to_component( views.view_to_component_compatibility, compatibility=True ), - idom.html.hr(), # FIXME: Looks like elements can't follow an iframe + idom.html.hr(), ) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index 5aa62076..fbacac81 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -27,7 +27,6 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ["*"] -X_FRAME_OPTIONS = "SAMEORIGIN" # Application definition INSTALLED_APPS = [ From bd5ae3fb633b67c107af5c53ee93d638cf7d9886 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 04:00:31 -0700 Subject: [PATCH 056/110] more refactoring --- mkdocs.yml | 2 +- src/django_idom/types.py | 1 - src/django_idom/utils.py | 66 +++++++++--------------------- tests/test_app/components.py | 31 ++++---------- tests/test_app/templates/base.html | 15 +++---- tests/test_app/views.py | 32 +++++++-------- 6 files changed, 49 insertions(+), 98 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 2fcf788d..ca9e8ba1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,7 @@ nav: - 3. Use the Template Tag: getting-started/reference-component.md - 4. Render Your View: getting-started/render-view.md - 5. Learn More: getting-started/learn-more.md - - Exclusive Features: + - Usage: - Components: features/components.md - Hooks: features/hooks.md - Decorators: features/decorators.md diff --git a/src/django_idom/types.py b/src/django_idom/types.py index ad2a4145..f8cc028c 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -15,7 +15,6 @@ class IdomWebsocket: @dataclass class ViewComponentIframe: - middleware: Union[List[Union[Callable, str]], None] view: Union[View, Callable] args: Tuple kwargs: dict diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index c156c47b..a325ec10 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -5,8 +5,9 @@ from fnmatch import fnmatch from importlib import import_module from inspect import iscoroutinefunction -from typing import Callable, List, Set, Union +from typing import Callable, Set, Union +from channels.db import database_sync_to_async from django.http import HttpRequest from django.template import engines from django.urls import reverse @@ -14,7 +15,6 @@ from django.views import View from idom import component, hooks, html, utils from idom.types import VdomDict -from channels.db import database_sync_to_async from django_idom.config import IDOM_REGISTERED_COMPONENTS, IDOM_VIEW_COMPONENT_IFRAMES from django_idom.types import ViewComponentIframe @@ -27,7 +27,6 @@ @component def view_to_component( view: Union[Callable, View], - middleware: Union[List[Union[Callable, str]], None] = None, compatibility: bool = False, request: Union[HttpRequest, None] = None, *args, @@ -36,8 +35,6 @@ def view_to_component( """Converts a Django view to an IDOM component. Args: - middleware: The list of Django middleware to use when rendering the view. - This can either be a list of middleware functions or string dotted paths. compatibility: If True, the component will be rendered in an iframe. request: Request object to provide to the view. *args: The positional arguments to pass to the view. @@ -55,7 +52,7 @@ def view_to_component( if not request: request_obj = HttpRequest() # TODO: Figure out some intelligent way to set the method. - # Might need intercepting common things such as form submission. + # Might need intercepting common things such as form submission? request_obj.method = "GET" # Render Check 1: Compatibility mode @@ -66,7 +63,7 @@ def view_to_component( # Register the iframe's URL if needed if not IDOM_VIEW_COMPONENT_IFRAMES.get(dotted_path): IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( - middleware, view, args, kwargs + view, args, kwargs ) return html.iframe( @@ -76,58 +73,38 @@ def view_to_component( } ) - # Render the view in an async hook to avoid blocking the main thread + # Asynchronous view rendering via hooks @hooks.use_effect(dependencies=[rendered_view]) async def async_renderer(): + """Render the view in an async hook to avoid blocking the main thread.""" if rendered_view: return # Render Check 2: Async function view if iscoroutinefunction(view): - render = await _view_middleware(middleware, view)( - request_obj, *args, **kwargs - ) + render = await view(request_obj, *args, **kwargs) - # Render Check 3: Async class based view + # Render Check 3: Async class view # TODO: Support Django 4.1 async CBV elif getattr(view, "view_is_async", False): - return + async_cbv = view.as_view() + async_view = await async_cbv(request_obj, *args, **kwargs) + render = await async_view.render() - # Render Check 3: Sync class based view - # TODO: Make this async, maybe through database_sync_to_async on a helper function? + # Render Check 3: Sync class view elif getattr(view, "as_view", None): - render = _view_middleware(middleware, view.as_view())( - request_obj, *args, **kwargs - ).render() + async_cbv = database_sync_to_async(view.as_view()) + async_view = await async_cbv(request_obj, *args, **kwargs) + render = await database_sync_to_async(async_view.render)() - # Render Check 4: Sync function based view - # TODO: Make this async, maybe through database_sync_to_async on a helper function? + # Render Check 4: Sync function view else: - render = _view_middleware(middleware, view)(request_obj, *args, **kwargs) + wrapped_view = database_sync_to_async(view) + render = await wrapped_view(request_obj, *args, **kwargs) set_rendered_view(render) -def _view_middleware( - middleware: Union[List[Union[Callable, str]], None], view: Union[Callable, View] -) -> Callable: - """Applies middleware to a view.""" - if not middleware: - return view - - def _wrapper(*args, **kwargs): - new_view = view - for middleware_item in middleware: - if isinstance(middleware_item, str): - middleware_item = _import_dotted_path(middleware_item) - - # TODO: Consider doing this async if needed. Also consider class based middleware. - new_view = middleware_item(new_view)(*args, **kwargs) - return new_view - - return _wrapper - - def _import_dotted_path(dotted_path: str) -> Callable: """Imports a dotted path and returns the callable.""" module_name, component_name = dotted_path.rsplit(".", 1) @@ -139,12 +116,7 @@ def _import_dotted_path(dotted_path: str) -> Callable: f"Failed to import {module_name!r} while loading {component_name!r}" ) from error - try: - return getattr(module, component_name) - except AttributeError as error: - raise RuntimeError( - f"Module {module_name!r} has no component named {component_name!r}" - ) from error + return getattr(module, component_name) def _register_component(dotted_path: str) -> None: diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 87d54fe8..875457ad 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -133,46 +133,31 @@ def authorized_user(): @idom.component -def view_to_component(): - return django_idom.utils.view_to_component(views.view_to_component) +def view_to_component_sync_func(): + return django_idom.utils.view_to_component(views.view_to_component_sync_func) @idom.component -def view_to_component_async(): - return django_idom.utils.view_to_component(views.view_to_component_async) +def view_to_component_async_func(): + return django_idom.utils.view_to_component(views.view_to_component_async_func) @idom.component -def view_to_component_class(): - return django_idom.utils.view_to_component(views.ViewToComponentClass) +def view_to_component_sync_class(): + return django_idom.utils.view_to_component(views.ViewToComponentSyncClass) @idom.component -def view_to_component_compatibility(): +def view_to_component_sync_func_compatibility(): return idom.html.div( {"id": "view_to_component_compatibility"}, django_idom.utils.view_to_component( - views.view_to_component_compatibility, compatibility=True + views.view_to_component_sync_func_compatibility, compatibility=True ), idom.html.hr(), ) -@idom.component -def view_to_component_middleware(): - def str_replace_middleware(view): - def middleware(request, *args, **kwargs): - render = view(request, *args, **kwargs) - render.content = render.content.decode("utf-8").replace("Fail", "Success") - return render - - return middleware - - return django_idom.utils.view_to_component( - views.view_to_component_middleware, middleware=[str_replace_middleware] - ) - - @idom.component def view_to_component_script(): return django_idom.utils.view_to_component(views.view_to_component_script) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index b9b8851b..d1625e68 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -23,16 +23,13 @@

IDOM Test Page

{% component "test_app.components.django_js" %}
{% component "test_app.components.unauthorized_user" %}
{% component "test_app.components.authorized_user" %}
-
{% component "test_app.components.view_to_component" %}
-
{% component "test_app.components.view_to_component_middleware" %}
-
{% component "test_app.components.view_to_component_async" %}
- -
{% component "test_app.components.view_to_component_class" %}
- -
{% component "test_app.components.view_to_component_compatibility" %}
- +
{% component "test_app.components.view_to_component_sync_func" %}
+
{% component "test_app.components.view_to_component_async_func" %}
+
{% component "test_app.components.view_to_component_sync_class" %}
+ +
{% component "test_app.components.view_to_component_sync_func_compatibility" %}
+
{% component "test_app.components.view_to_component_script" %}
- diff --git a/tests/test_app/views.py b/tests/test_app/views.py index edea425a..ceee4bef 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -7,36 +7,34 @@ def base_template(request): return render(request, "base.html", context) -def view_to_component(request): - return render(request, "view_to_component.html", {"test_name": "view_to_component"}) +def view_to_component_sync_func(request): + return render( + request, + "view_to_component.html", + {"test_name": view_to_component_sync_func.__name__}, + ) -async def view_to_component_async(request): +async def view_to_component_async_func(request): return render( - request, "view_to_component.html", {"test_name": "view_to_component_async"} + request, + "view_to_component.html", + {"test_name": view_to_component_async_func.__name__}, ) -class ViewToComponentClass(TemplateView): +class ViewToComponentSyncClass(TemplateView): template_name = "view_to_component.html" def get_context_data(self, **kwargs): - return {"test_name": "view_to_component_class"} - - -def view_to_component_compatibility(request): - return render( - request, - "view_to_component.html", - {"test_name": "view_to_component_compatibility"}, - ) + return {"test_name": ViewToComponentSyncClass.__name__} -def view_to_component_middleware(request): +def view_to_component_sync_func_compatibility(request): return render( request, "view_to_component.html", - {"test_name": "view_to_component_middleware", "status": "Fail"}, + {"test_name": view_to_component_sync_func_compatibility.__name__}, ) @@ -44,5 +42,5 @@ def view_to_component_script(request): return render( request, "view_to_component_script.html", - {"test_name": "view_to_component_script", "status": "Fail"}, + {"test_name": view_to_component_script.__name__, "status": "Fail"}, ) From 6631221701344d35312423efa29a7f5d9f430d08 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 04:40:16 -0700 Subject: [PATCH 057/110] support django 4.1 async CBC --- src/django_idom/utils.py | 11 ++++++++--- tests/test_app/components.py | 10 ++++++++++ tests/test_app/templates/base.html | 3 ++- tests/test_app/views.py | 25 ++++++++++++++++++++++--- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index a325ec10..b0e7c428 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -85,17 +85,22 @@ async def async_renderer(): render = await view(request_obj, *args, **kwargs) # Render Check 3: Async class view - # TODO: Support Django 4.1 async CBV elif getattr(view, "view_is_async", False): async_cbv = view.as_view() async_view = await async_cbv(request_obj, *args, **kwargs) - render = await async_view.render() + if getattr(async_view, "render", None): + render = await async_view.render() + else: + render = async_view # Render Check 3: Sync class view elif getattr(view, "as_view", None): async_cbv = database_sync_to_async(view.as_view()) async_view = await async_cbv(request_obj, *args, **kwargs) - render = await database_sync_to_async(async_view.render)() + if getattr(async_view, "render", None): + render = await database_sync_to_async(async_view.render)() + else: + render = async_view # Render Check 4: Sync function view else: diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 875457ad..3afcc424 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -147,6 +147,16 @@ def view_to_component_sync_class(): return django_idom.utils.view_to_component(views.ViewToComponentSyncClass) +@idom.component +def view_to_component_async_class(): + return django_idom.utils.view_to_component(views.ViewToComponentAsyncClass) + + +@idom.component +def view_to_component_template_view_class(): + return django_idom.utils.view_to_component(views.ViewToComponentTemplateViewClass) + + @idom.component def view_to_component_sync_func_compatibility(): return idom.html.div( diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index d1625e68..5ac22733 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -26,9 +26,10 @@

IDOM Test Page

{% component "test_app.components.view_to_component_sync_func" %}
{% component "test_app.components.view_to_component_async_func" %}
{% component "test_app.components.view_to_component_sync_class" %}
- +
{% component "test_app.components.view_to_component_async_class" %}
{% component "test_app.components.view_to_component_sync_func_compatibility" %}
+
{% component "test_app.components.view_to_component_template_view_class" %}
{% component "test_app.components.view_to_component_script" %}
diff --git a/tests/test_app/views.py b/tests/test_app/views.py index ceee4bef..e59a87be 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -1,5 +1,6 @@ +from channels.db import database_sync_to_async from django.shortcuts import render -from django.views.generic import TemplateView +from django.views.generic import TemplateView, View def base_template(request): @@ -23,11 +24,29 @@ async def view_to_component_async_func(request): ) -class ViewToComponentSyncClass(TemplateView): +class ViewToComponentSyncClass(View): + def get(self, request, *args, **kwargs): + return render( + request, + "view_to_component.html", + {"test_name": ViewToComponentSyncClass.__name__}, + ) + + +class ViewToComponentAsyncClass(View): + async def get(self, request, *args, **kwargs): + return await database_sync_to_async(render)( + request, + "view_to_component.html", + {"test_name": ViewToComponentAsyncClass.__name__}, + ) + + +class ViewToComponentTemplateViewClass(TemplateView): template_name = "view_to_component.html" def get_context_data(self, **kwargs): - return {"test_name": ViewToComponentSyncClass.__name__} + return {"test_name": ViewToComponentTemplateViewClass.__name__} def view_to_component_sync_func_compatibility(request): From 952f77204230090069a40f7fb4537880982e4773 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:42:24 -0700 Subject: [PATCH 058/110] fleshed out compatibility mode --- src/django_idom/http/views.py | 25 +++++++++++---- src/django_idom/utils.py | 17 +++++++++-- tests/test_app/components.py | 48 ++++++++++++++++++++++++++++- tests/test_app/templates/base.html | 7 +++-- tests/test_app/views.py | 49 +++++++++++++++++++++++++----- 5 files changed, 128 insertions(+), 18 deletions(-) diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 8a72e0d7..eff60293 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -1,14 +1,16 @@ import os -from inspect import isclass, iscoroutinefunction +from inspect import iscoroutinefunction from aiofile import async_open from channels.db import database_sync_to_async from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest, HttpResponse +from django.utils.decorators import method_decorator from django.views.decorators.clickjacking import xframe_options_sameorigin from idom.config import IDOM_WED_MODULES_DIR from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES +from django_idom.utils import async_xframe_options_sameorigin async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: @@ -47,12 +49,23 @@ async def view_to_component_iframe( if not iframe: raise ValueError(f"No view registered for component {view_path}.") - # Render the view - if isclass(iframe): + # Render Check 1: Async function view + if iscoroutinefunction(iframe.view): + return await async_xframe_options_sameorigin(iframe.view)(request) + + # Render Check 2: Async class view + if getattr(iframe.view, "view_is_async", False): + return await method_decorator(async_xframe_options_sameorigin, name="dispatch")( + iframe.view.as_view() + )(request) + + # Render Check 3: Sync class view + if getattr(iframe.view, "as_view", None): return await database_sync_to_async( - xframe_options_sameorigin(iframe.view.as_view)() + method_decorator(xframe_options_sameorigin, name="dispatch")( + iframe.view.as_view() + ) )(request) - if iscoroutinefunction(iframe): - return await xframe_options_sameorigin(iframe.view)(request) + # Render Check 4: Sync function view return await database_sync_to_async(xframe_options_sameorigin(iframe.view))(request) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index b0e7c428..3a77f61f 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -3,6 +3,7 @@ import os import re from fnmatch import fnmatch +from functools import wraps from importlib import import_module from inspect import iscoroutinefunction from typing import Callable, Set, Union @@ -93,7 +94,7 @@ async def async_renderer(): else: render = async_view - # Render Check 3: Sync class view + # Render Check 4: Sync class view elif getattr(view, "as_view", None): async_cbv = database_sync_to_async(view.as_view()) async_view = await async_cbv(request_obj, *args, **kwargs) @@ -102,7 +103,7 @@ async def async_renderer(): else: render = async_view - # Render Check 4: Sync function view + # Render Check 5: Sync function view else: wrapped_view = database_sync_to_async(view) render = await wrapped_view(request_obj, *args, **kwargs) @@ -225,3 +226,15 @@ def _register_components(self, components: Set) -> None: "\033[0m", component, ) + + +def async_xframe_options_sameorigin(view_func): + """Async version of `django.views.decorators.clickjacking.xframe_options_sameorigin`.""" + + async def wrapped_view(*args, **kwargs): + resp = await view_func(*args, **kwargs) + if resp.get("X-Frame-Options") is None: + resp["X-Frame-Options"] = "SAMEORIGIN" + return resp + + return wraps(view_func)(wrapped_view) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 3afcc424..1b909ded 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -1,3 +1,5 @@ +import inspect + import idom import django_idom @@ -160,7 +162,7 @@ def view_to_component_template_view_class(): @idom.component def view_to_component_sync_func_compatibility(): return idom.html.div( - {"id": "view_to_component_compatibility"}, + {"id": inspect.currentframe().f_code.co_name}, django_idom.utils.view_to_component( views.view_to_component_sync_func_compatibility, compatibility=True ), @@ -168,6 +170,50 @@ def view_to_component_sync_func_compatibility(): ) +@idom.component +def view_to_component_async_func_compatibility(): + return idom.html.div( + {"id": inspect.currentframe().f_code.co_name}, + django_idom.utils.view_to_component( + views.view_to_component_async_func_compatibility, compatibility=True + ), + idom.html.hr(), + ) + + +@idom.component +def view_to_component_sync_class_compatibility(): + return idom.html.div( + {"id": inspect.currentframe().f_code.co_name}, + django_idom.utils.view_to_component( + views.ViewToComponentSyncClassCompatibility, compatibility=True + ), + idom.html.hr(), + ) + + +@idom.component +def view_to_component_async_class_compatibility(): + return idom.html.div( + {"id": inspect.currentframe().f_code.co_name}, + django_idom.utils.view_to_component( + views.ViewToComponentAsyncClassCompatibility, compatibility=True + ), + idom.html.hr(), + ) + + +@idom.component +def view_to_component_template_view_class_compatibility(): + return idom.html.div( + {"id": inspect.currentframe().f_code.co_name}, + django_idom.utils.view_to_component( + views.ViewToComponentTemplateViewClassCompatibility, compatibility=True + ), + idom.html.hr(), + ) + + @idom.component def view_to_component_script(): return django_idom.utils.view_to_component(views.view_to_component_script) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 5ac22733..d49a02ef 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -28,9 +28,12 @@

IDOM Test Page

{% component "test_app.components.view_to_component_sync_class" %}
{% component "test_app.components.view_to_component_async_class" %}
{% component "test_app.components.view_to_component_sync_func_compatibility" %}
- +
{% component "test_app.components.view_to_component_async_func_compatibility" %}
+
{% component "test_app.components.view_to_component_sync_class_compatibility" %}
+
{% component "test_app.components.view_to_component_async_class_compatibility" %}
+
{% component "test_app.components.view_to_component_template_view_class_compatibility" %}
{% component "test_app.components.view_to_component_template_view_class" %}
{% component "test_app.components.view_to_component_script" %}
- + \ No newline at end of file diff --git a/tests/test_app/views.py b/tests/test_app/views.py index e59a87be..f2245840 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -1,3 +1,5 @@ +import inspect + from channels.db import database_sync_to_async from django.shortcuts import render from django.views.generic import TemplateView, View @@ -12,7 +14,7 @@ def view_to_component_sync_func(request): return render( request, "view_to_component.html", - {"test_name": view_to_component_sync_func.__name__}, + {"test_name": inspect.currentframe().f_code.co_name}, ) @@ -20,7 +22,7 @@ async def view_to_component_async_func(request): return render( request, "view_to_component.html", - {"test_name": view_to_component_async_func.__name__}, + {"test_name": inspect.currentframe().f_code.co_name}, ) @@ -29,7 +31,7 @@ def get(self, request, *args, **kwargs): return render( request, "view_to_component.html", - {"test_name": ViewToComponentSyncClass.__name__}, + {"test_name": self.__class__.__name__}, ) @@ -38,7 +40,7 @@ async def get(self, request, *args, **kwargs): return await database_sync_to_async(render)( request, "view_to_component.html", - {"test_name": ViewToComponentAsyncClass.__name__}, + {"test_name": self.__class__.__name__}, ) @@ -46,20 +48,53 @@ class ViewToComponentTemplateViewClass(TemplateView): template_name = "view_to_component.html" def get_context_data(self, **kwargs): - return {"test_name": ViewToComponentTemplateViewClass.__name__} + return {"test_name": self.__class__.__name__} def view_to_component_sync_func_compatibility(request): return render( request, "view_to_component.html", - {"test_name": view_to_component_sync_func_compatibility.__name__}, + {"test_name": inspect.currentframe().f_code.co_name}, ) +async def view_to_component_async_func_compatibility(request): + return await database_sync_to_async(render)( + request, + "view_to_component.html", + {"test_name": inspect.currentframe().f_code.co_name}, + ) + + +class ViewToComponentSyncClassCompatibility(View): + def get(self, request, *args, **kwargs): + return render( + request, + "view_to_component.html", + {"test_name": self.__class__.__name__}, + ) + + +class ViewToComponentAsyncClassCompatibility(View): + async def get(self, request, *args, **kwargs): + return await database_sync_to_async(render)( + request, + "view_to_component.html", + {"test_name": self.__class__.__name__}, + ) + + +class ViewToComponentTemplateViewClassCompatibility(TemplateView): + template_name = "view_to_component.html" + + def get_context_data(self, **kwargs): + return {"test_name": self.__class__.__name__} + + def view_to_component_script(request): return render( request, "view_to_component_script.html", - {"test_name": view_to_component_script.__name__, "status": "Fail"}, + {"test_name": inspect.currentframe().f_code.co_name, "status": "Fail"}, ) From 60684c0e60be3339cedd75354951752472493d49 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:00:34 -0700 Subject: [PATCH 059/110] move view_to_component() --- src/django_idom/components.py | 109 +++++++++++++++++++++++++++++++++- src/django_idom/utils.py | 98 +----------------------------- tests/test_app/components.py | 23 +++---- 3 files changed, 121 insertions(+), 109 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 653f0f76..abacf22a 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -1,9 +1,114 @@ import os +from inspect import iscoroutinefunction +from typing import Callable, Dict, Iterable, Union +from channels.db import database_sync_to_async from django.contrib.staticfiles.finders import find -from idom import component, html +from django.http import HttpRequest +from django.urls import reverse +from django.views import View +from idom import component, hooks, html, utils +from idom.types import VdomDict -from django_idom.config import IDOM_CACHE +from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES +from django_idom.types import ViewComponentIframe + + +@component +def view_to_component( + view: Union[Callable, View], + compatibility: bool = False, + strict_parsing: bool = True, + request: Union[HttpRequest, None] = None, + args: Union[Iterable, None] = None, + kwargs: Union[Dict, None] = None, +) -> Union[VdomDict, None]: + """Converts a Django view to an IDOM component. + + Args: + view: The view function or class to convert. + + Keyword Args: + compatibility: If True, the component will be rendered in an iframe. + strict_parsing: If True, an exception will be generated if the HTML does not + perfectly adhere to HTML5. + request: Request object to provide to the view. + *args: The positional arguments to pass to the view. + **kwargs: The keyword arguments to pass to the view. + """ + args = args or [] + kwargs = kwargs or {} + + # Return the view if it's been rendered via the `async_renderer` + rendered_view, set_rendered_view = hooks.use_state(None) + if rendered_view: + return html._( + utils.html_to_vdom( + rendered_view.content.decode("utf-8").strip(), strict=strict_parsing + ) + ) + + # Create a synthetic request object. + request_obj = request + if not request: + request_obj = HttpRequest() + # TODO: Figure out some intelligent way to set the method. + # Might need intercepting common things such as form submission? + request_obj.method = "GET" + + # Render Check 1: Compatibility mode + if compatibility: + dotted_path = f"{view.__module__}.{view.__name__}" + dotted_path = dotted_path.replace("<", "").replace(">", "") + + # Register the iframe's URL if needed + if not IDOM_VIEW_COMPONENT_IFRAMES.get(dotted_path): + IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( + view, args, kwargs + ) + + return html.iframe( + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } + ) + + # Asynchronous view rendering via hooks + @hooks.use_effect(dependencies=[rendered_view]) + async def async_renderer(): + """Render the view in an async hook to avoid blocking the main thread.""" + if rendered_view: + return + + # Render Check 2: Async function view + if iscoroutinefunction(view): + render = await view(request_obj, *args, **kwargs) + + # Render Check 3: Async class view + elif getattr(view, "view_is_async", False): + async_cbv = view.as_view() + async_view = await async_cbv(request_obj, *args, **kwargs) + if getattr(async_view, "render", None): + render = await async_view.render() + else: + render = async_view + + # Render Check 4: Sync class view + elif getattr(view, "as_view", None): + async_cbv = database_sync_to_async(view.as_view()) + async_view = await async_cbv(request_obj, *args, **kwargs) + if getattr(async_view, "render", None): + render = await database_sync_to_async(async_view.render)() + else: + render = async_view + + # Render Check 5: Sync function view + else: + wrapped_view = database_sync_to_async(view) + render = await wrapped_view(request_obj, *args, **kwargs) + + set_rendered_view(render) @component diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 3a77f61f..48c10ddc 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -5,112 +5,18 @@ from fnmatch import fnmatch from functools import wraps from importlib import import_module -from inspect import iscoroutinefunction -from typing import Callable, Set, Union +from typing import Callable, Set -from channels.db import database_sync_to_async -from django.http import HttpRequest from django.template import engines -from django.urls import reverse from django.utils.encoding import smart_str -from django.views import View -from idom import component, hooks, html, utils -from idom.types import VdomDict -from django_idom.config import IDOM_REGISTERED_COMPONENTS, IDOM_VIEW_COMPONENT_IFRAMES -from django_idom.types import ViewComponentIframe +from django_idom.config import IDOM_REGISTERED_COMPONENTS COMPONENT_REGEX = re.compile(r"{% *component +((\"[^\"']*\")|('[^\"']*'))(.*?)%}") _logger = logging.getLogger(__name__) -@component -def view_to_component( - view: Union[Callable, View], - compatibility: bool = False, - request: Union[HttpRequest, None] = None, - *args, - **kwargs, -) -> Union[VdomDict, None]: - """Converts a Django view to an IDOM component. - - Args: - compatibility: If True, the component will be rendered in an iframe. - request: Request object to provide to the view. - *args: The positional arguments to pass to the view. - - Keyword Args: - **kwargs: The keyword arguments to pass to the view. - """ - # Return the view if it's been rendered via the async_renderer - rendered_view, set_rendered_view = hooks.use_state(None) - if rendered_view: - return html._(utils.html_to_vdom(rendered_view.content.decode("utf-8").strip())) - - # Create a synthetic request object. - request_obj = request - if not request: - request_obj = HttpRequest() - # TODO: Figure out some intelligent way to set the method. - # Might need intercepting common things such as form submission? - request_obj.method = "GET" - - # Render Check 1: Compatibility mode - if compatibility: - dotted_path = f"{view.__module__}.{view.__name__}" - dotted_path = dotted_path.replace("<", "").replace(">", "") - - # Register the iframe's URL if needed - if not IDOM_VIEW_COMPONENT_IFRAMES.get(dotted_path): - IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( - view, args, kwargs - ) - - return html.iframe( - { - "src": reverse("idom:view_to_component", args=[dotted_path]), - "loading": "lazy", - } - ) - - # Asynchronous view rendering via hooks - @hooks.use_effect(dependencies=[rendered_view]) - async def async_renderer(): - """Render the view in an async hook to avoid blocking the main thread.""" - if rendered_view: - return - - # Render Check 2: Async function view - if iscoroutinefunction(view): - render = await view(request_obj, *args, **kwargs) - - # Render Check 3: Async class view - elif getattr(view, "view_is_async", False): - async_cbv = view.as_view() - async_view = await async_cbv(request_obj, *args, **kwargs) - if getattr(async_view, "render", None): - render = await async_view.render() - else: - render = async_view - - # Render Check 4: Sync class view - elif getattr(view, "as_view", None): - async_cbv = database_sync_to_async(view.as_view()) - async_view = await async_cbv(request_obj, *args, **kwargs) - if getattr(async_view, "render", None): - render = await database_sync_to_async(async_view.render)() - else: - render = async_view - - # Render Check 5: Sync function view - else: - wrapped_view = database_sync_to_async(view) - render = await wrapped_view(request_obj, *args, **kwargs) - - set_rendered_view(render) - - def _import_dotted_path(dotted_path: str) -> Callable: """Imports a dotted path and returns the callable.""" module_name, component_name = dotted_path.rsplit(".", 1) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 1b909ded..44d9907c 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -3,6 +3,7 @@ import idom import django_idom +from django_idom.components import view_to_component from . import views @@ -136,34 +137,34 @@ def authorized_user(): @idom.component def view_to_component_sync_func(): - return django_idom.utils.view_to_component(views.view_to_component_sync_func) + return view_to_component(views.view_to_component_sync_func) @idom.component def view_to_component_async_func(): - return django_idom.utils.view_to_component(views.view_to_component_async_func) + return view_to_component(views.view_to_component_async_func) @idom.component def view_to_component_sync_class(): - return django_idom.utils.view_to_component(views.ViewToComponentSyncClass) + return view_to_component(views.ViewToComponentSyncClass) @idom.component def view_to_component_async_class(): - return django_idom.utils.view_to_component(views.ViewToComponentAsyncClass) + return view_to_component(views.ViewToComponentAsyncClass) @idom.component def view_to_component_template_view_class(): - return django_idom.utils.view_to_component(views.ViewToComponentTemplateViewClass) + return view_to_component(views.ViewToComponentTemplateViewClass) @idom.component def view_to_component_sync_func_compatibility(): return idom.html.div( {"id": inspect.currentframe().f_code.co_name}, - django_idom.utils.view_to_component( + view_to_component( views.view_to_component_sync_func_compatibility, compatibility=True ), idom.html.hr(), @@ -174,7 +175,7 @@ def view_to_component_sync_func_compatibility(): def view_to_component_async_func_compatibility(): return idom.html.div( {"id": inspect.currentframe().f_code.co_name}, - django_idom.utils.view_to_component( + view_to_component( views.view_to_component_async_func_compatibility, compatibility=True ), idom.html.hr(), @@ -185,7 +186,7 @@ def view_to_component_async_func_compatibility(): def view_to_component_sync_class_compatibility(): return idom.html.div( {"id": inspect.currentframe().f_code.co_name}, - django_idom.utils.view_to_component( + view_to_component( views.ViewToComponentSyncClassCompatibility, compatibility=True ), idom.html.hr(), @@ -196,7 +197,7 @@ def view_to_component_sync_class_compatibility(): def view_to_component_async_class_compatibility(): return idom.html.div( {"id": inspect.currentframe().f_code.co_name}, - django_idom.utils.view_to_component( + view_to_component( views.ViewToComponentAsyncClassCompatibility, compatibility=True ), idom.html.hr(), @@ -207,7 +208,7 @@ def view_to_component_async_class_compatibility(): def view_to_component_template_view_class_compatibility(): return idom.html.div( {"id": inspect.currentframe().f_code.co_name}, - django_idom.utils.view_to_component( + view_to_component( views.ViewToComponentTemplateViewClassCompatibility, compatibility=True ), idom.html.hr(), @@ -216,4 +217,4 @@ def view_to_component_template_view_class_compatibility(): @idom.component def view_to_component_script(): - return django_idom.utils.view_to_component(views.view_to_component_script) + return view_to_component(views.view_to_component_script) From 446e65e4b4f50965db464a09ad72705c8756fa3f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:06:15 -0700 Subject: [PATCH 060/110] move docs to src folder --- docs/changelog/index.md | 11 -- docs/index.md | 17 --- docs/src/changelog/index.md | 11 ++ docs/{ => src}/contribute/django-idom.md | 0 docs/{ => src}/contribute/docs.md | 0 docs/{ => src}/contribute/running-tests.md | 0 docs/{ => src}/features/components.md | 101 ++++++++++++++++++ docs/{ => src}/features/decorators.md | 0 docs/{ => src}/features/hooks.md | 0 docs/{ => src}/features/orm.md | 0 docs/{ => src}/features/settings.md | 0 docs/{ => src}/features/templatetag.md | 4 +- .../getting-started/create-component.md | 4 +- .../getting-started/initial-steps.md | 0 docs/{ => src}/getting-started/learn-more.md | 0 .../getting-started/reference-component.md | 4 +- docs/{ => src}/getting-started/render-view.md | 0 docs/src/index.md | 17 +++ docs/{ => src}/installation/index.md | 0 docs/{ => src}/stylesheets/extra.css | 0 mkdocs.yml | 3 + 21 files changed, 138 insertions(+), 34 deletions(-) delete mode 100644 docs/changelog/index.md delete mode 100644 docs/index.md create mode 100644 docs/src/changelog/index.md rename docs/{ => src}/contribute/django-idom.md (100%) rename docs/{ => src}/contribute/docs.md (100%) rename docs/{ => src}/contribute/running-tests.md (100%) rename docs/{ => src}/features/components.md (57%) rename docs/{ => src}/features/decorators.md (100%) rename docs/{ => src}/features/hooks.md (100%) rename docs/{ => src}/features/orm.md (100%) rename docs/{ => src}/features/settings.md (100%) rename docs/{ => src}/features/templatetag.md (94%) rename docs/{ => src}/getting-started/create-component.md (71%) rename docs/{ => src}/getting-started/initial-steps.md (100%) rename docs/{ => src}/getting-started/learn-more.md (100%) rename docs/{ => src}/getting-started/reference-component.md (83%) rename docs/{ => src}/getting-started/render-view.md (100%) create mode 100644 docs/src/index.md rename docs/{ => src}/installation/index.md (100%) rename docs/{ => src}/stylesheets/extra.css (100%) diff --git a/docs/changelog/index.md b/docs/changelog/index.md deleted file mode 100644 index a6e2f878..00000000 --- a/docs/changelog/index.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -hide: - - navigation - - toc ---- - -!!! note "Attribution" - - {% include-markdown "../../CHANGELOG.md" start="" end="" %} - -{% include-markdown "../../CHANGELOG.md" start="" %} diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 9b11f2dc..00000000 --- a/docs/index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -hide: - - navigation - - toc ---- - -{% include-markdown "../README.md" start="" end="" %} - -## ReactJS for Django Developers. - ---- - -{% include-markdown "../README.md" start="" end="" %} - -## Resources - -{% include-markdown "../README.md" start="" end="" %} diff --git a/docs/src/changelog/index.md b/docs/src/changelog/index.md new file mode 100644 index 00000000..a4a0f241 --- /dev/null +++ b/docs/src/changelog/index.md @@ -0,0 +1,11 @@ +--- +hide: + - navigation + - toc +--- + +!!! note "Attribution" + + {% include-markdown "../../../CHANGELOG.md" start="" end="" %} + +{% include-markdown "../../../CHANGELOG.md" start="" %} diff --git a/docs/contribute/django-idom.md b/docs/src/contribute/django-idom.md similarity index 100% rename from docs/contribute/django-idom.md rename to docs/src/contribute/django-idom.md diff --git a/docs/contribute/docs.md b/docs/src/contribute/docs.md similarity index 100% rename from docs/contribute/docs.md rename to docs/src/contribute/docs.md diff --git a/docs/contribute/running-tests.md b/docs/src/contribute/running-tests.md similarity index 100% rename from docs/contribute/running-tests.md rename to docs/src/contribute/running-tests.md diff --git a/docs/features/components.md b/docs/src/features/components.md similarity index 57% rename from docs/features/components.md rename to docs/src/features/components.md index fa6b45f0..36ea700b 100644 --- a/docs/features/components.md +++ b/docs/src/features/components.md @@ -1,3 +1,104 @@ +## View To Component + +Convert any Django view into a IDOM component by usng this decorator. Compatible with sync/async [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/). + +=== "components.py" + + ```python + from idom import component, html + from django_idom.components import view_to_component + from .views import hello_world_view + + @component + def my_component(): + return html.div( + view_to_component(hello_world_view), + ) + ``` + +=== "views.py" + + ```python + from django.http import HttpResponse + + def hello_world_view(request): + return HttpResponse("Hello, World!") + ``` + +??? question "How do I use this for Class Based Views?" + + You can simply pass your Class Based View directly into this function. + + ```python title="components.py" + from idom import component, html + from django_idom.components import view_to_component + from .views import HelloWorldView + + @component + def my_component(): + return html.div( + view_to_component(HelloWorldView), + ) + ``` + +??? question "How do I pass arguments into the view?" + + You can use the `args` and `kwargs` parameters to pass arguments to the view. + + ```python title="components.py" + from idom import component, html + from django_idom.components import view_to_component + from .views import HelloWorldView + + @component + def my_component(): + return html.div( + view_to_component( + HelloWorldView, + args=["value_1", "value_2"], + kwargs={"key_1": "value_1", "key_2": "value_2"}, + ), + ) + ``` + +??? question "What is compatibility mode?" + + For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe. + + Any view can be rendered within compatibility mode. + + Please note that by default the iframe is unstyled, and thus won't look pretty until you add some CSS. + + ```python title="components.py" + from idom import component, html + from django_idom.components import view_to_component + from .views import hello_world_view + + @component + def my_component(): + return html.div( + view_to_component(hello_world_view, compatibility=True), + ) + ``` + +??? question "What is strict parsing?" + + By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5. + + You can rely on best-fit parsing by setting the `strict_parsing` parameter to `False`. + + ```python title="components.py" + from idom import component, html + from django_idom.components import view_to_component + from .views import hello_world_view + + @component + def my_component(): + return html.div( + view_to_component(hello_world_view, strict_parsing=False), + ) + ``` + ## Django CSS Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). diff --git a/docs/features/decorators.md b/docs/src/features/decorators.md similarity index 100% rename from docs/features/decorators.md rename to docs/src/features/decorators.md diff --git a/docs/features/hooks.md b/docs/src/features/hooks.md similarity index 100% rename from docs/features/hooks.md rename to docs/src/features/hooks.md diff --git a/docs/features/orm.md b/docs/src/features/orm.md similarity index 100% rename from docs/features/orm.md rename to docs/src/features/orm.md diff --git a/docs/features/settings.md b/docs/src/features/settings.md similarity index 100% rename from docs/features/settings.md rename to docs/src/features/settings.md diff --git a/docs/features/templatetag.md b/docs/src/features/templatetag.md similarity index 94% rename from docs/features/templatetag.md rename to docs/src/features/templatetag.md index 5f917f92..43910f4c 100644 --- a/docs/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -1,6 +1,6 @@ Integrated within Django IDOM, we bundle a template tag. Within this tag, you can pass in keyword arguments directly into your component. -{% include-markdown "../../README.md" start="" end="" %} +{% include-markdown "../../../README.md" start="" end="" %} @@ -81,6 +81,6 @@ Integrated within Django IDOM, we bundle a template tag. Within this tag, you ca Keep in mind, in order to use the `#!jinja {% component ... %}` tag, you'll need to first call `#!jinja {% load idom %}` to gain access to it. - {% include-markdown "../../README.md" start="" end="" %} + {% include-markdown "../../../README.md" start="" end="" %} diff --git a/docs/getting-started/create-component.md b/docs/src/getting-started/create-component.md similarity index 71% rename from docs/getting-started/create-component.md rename to docs/src/getting-started/create-component.md index ca6e2a18..29281051 100644 --- a/docs/getting-started/create-component.md +++ b/docs/src/getting-started/create-component.md @@ -4,9 +4,9 @@ --- -{% include-markdown "../../README.md" start="" end="" %} +{% include-markdown "../../../README.md" start="" end="" %} -{% include-markdown "../../README.md" start="" end="" %} +{% include-markdown "../../../README.md" start="" end="" %} ??? question "What should I name my IDOM files and functions?" diff --git a/docs/getting-started/initial-steps.md b/docs/src/getting-started/initial-steps.md similarity index 100% rename from docs/getting-started/initial-steps.md rename to docs/src/getting-started/initial-steps.md diff --git a/docs/getting-started/learn-more.md b/docs/src/getting-started/learn-more.md similarity index 100% rename from docs/getting-started/learn-more.md rename to docs/src/getting-started/learn-more.md diff --git a/docs/getting-started/reference-component.md b/docs/src/getting-started/reference-component.md similarity index 83% rename from docs/getting-started/reference-component.md rename to docs/src/getting-started/reference-component.md index af7dd77c..db5c4a5e 100644 --- a/docs/getting-started/reference-component.md +++ b/docs/src/getting-started/reference-component.md @@ -4,9 +4,9 @@ --- -{% include-markdown "../../README.md" start="" end="" %} +{% include-markdown "../../../README.md" start="" end="" %} -{% include-markdown "../../README.md" start="" end="" %} +{% include-markdown "../../../README.md" start="" end="" %} {% include-markdown "../features/templatetag.md" start="" end="" %} diff --git a/docs/getting-started/render-view.md b/docs/src/getting-started/render-view.md similarity index 100% rename from docs/getting-started/render-view.md rename to docs/src/getting-started/render-view.md diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 00000000..07ac4bb5 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,17 @@ +--- +hide: + - navigation + - toc +--- + +{% include-markdown "../../README.md" start="" end="" %} + +## ReactJS for Django Developers. + +--- + +{% include-markdown "../../README.md" start="" end="" %} + +## Resources + +{% include-markdown "../../README.md" start="" end="" %} diff --git a/docs/installation/index.md b/docs/src/installation/index.md similarity index 100% rename from docs/installation/index.md rename to docs/src/installation/index.md diff --git a/docs/stylesheets/extra.css b/docs/src/stylesheets/extra.css similarity index 100% rename from docs/stylesheets/extra.css rename to docs/src/stylesheets/extra.css diff --git a/mkdocs.yml b/mkdocs.yml index ca9e8ba1..b72bfcff 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,6 +52,8 @@ markdown_extensions: - pymdownx.emoji: emoji_index: !!python/name:materialx.emoji.twemoji emoji_generator: !!python/name:materialx.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true - pymdownx.highlight - pymdownx.superfences - pymdownx.details @@ -85,3 +87,4 @@ repo_url: https://github.com/idom-team/django-idom site_url: https://idom-team.github.io/django-idom repo_name: idom-team/django-idom edit_uri: edit/docs +docs_dir: docs/src From 97503ca60359a9f2113df62bad9f4be04ca91612 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:31:09 -0700 Subject: [PATCH 061/110] first cut at docs --- docs/includes/examples.md | 23 ++++++ docs/src/features/components.md | 125 +++++++++++++++++++------------- 2 files changed, 97 insertions(+), 51 deletions(-) create mode 100644 docs/includes/examples.md diff --git a/docs/includes/examples.md b/docs/includes/examples.md new file mode 100644 index 00000000..623e60c3 --- /dev/null +++ b/docs/includes/examples.md @@ -0,0 +1,23 @@ + + +```python +from django.http import HttpResponse + +def hello_world_view(request, *args, **kwargs): + return HttpResponse("Hello World!") +``` + + + + + +```python +from django.http import HttpResponse +from django.views import View + +class HelloWorldView(View): + def get(self, request, *args, **kwargs): + return HttpResponse("Hello World!") +``` + + \ No newline at end of file diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 36ea700b..5ae4cd60 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -18,48 +18,55 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible === "views.py" - ```python - from django.http import HttpResponse - - def hello_world_view(request): - return HttpResponse("Hello, World!") - ``` + {% include-markdown "../../includes/examples.md" start="" end="" %} ??? question "How do I use this for Class Based Views?" You can simply pass your Class Based View directly into this function. - ```python title="components.py" - from idom import component, html - from django_idom.components import view_to_component - from .views import HelloWorldView + === "components.py" - @component - def my_component(): - return html.div( - view_to_component(HelloWorldView), - ) - ``` + ```python + from idom import component, html + from django_idom.components import view_to_component + from .views import HelloWorldView + + @component + def my_component(): + return html.div( + view_to_component(HelloWorldView), + ) + ``` + + === "views.py" + + {% include-markdown "../../includes/examples.md" start="" end="" %} ??? question "How do I pass arguments into the view?" You can use the `args` and `kwargs` parameters to pass arguments to the view. - ```python title="components.py" - from idom import component, html - from django_idom.components import view_to_component - from .views import HelloWorldView + === "components.py" - @component - def my_component(): - return html.div( - view_to_component( - HelloWorldView, - args=["value_1", "value_2"], - kwargs={"key_1": "value_1", "key_2": "value_2"}, - ), - ) - ``` + ```python + from idom import component, html + from django_idom.components import view_to_component + from .views import hello_world_view + + @component + def my_component(): + return html.div( + view_to_component( + hello_world_view, + args=["value_1", "value_2"], + kwargs={"key_1": "value_1", "key_2": "value_2"}, + ), + ) + ``` + + === "views.py" + + {% include-markdown "../../includes/examples.md" start="" end="" %} ??? question "What is compatibility mode?" @@ -69,35 +76,51 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible Please note that by default the iframe is unstyled, and thus won't look pretty until you add some CSS. - ```python title="components.py" - from idom import component, html - from django_idom.components import view_to_component - from .views import hello_world_view + === "components.py" - @component - def my_component(): - return html.div( - view_to_component(hello_world_view, compatibility=True), - ) - ``` + ```python + from idom import component, html + from django_idom.components import view_to_component + from .views import hello_world_view + + @component + def my_component(): + return html.div( + view_to_component(hello_world_view, compatibility=True), + ) + ``` + + === "views.py" + + {% include-markdown "../../includes/examples.md" start="" end="" %} ??? question "What is strict parsing?" By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5. - You can rely on best-fit parsing by setting the `strict_parsing` parameter to `False`. + However, there are some circumstances where you may not have control over the original HTML, so you may be unable to fix it. Or you may be relying on non-standard HTML tags such as `#!html Hello World `. - ```python title="components.py" - from idom import component, html - from django_idom.components import view_to_component - from .views import hello_world_view + In these scenarios, you may want to rely on best-fit parsing by setting the `strict_parsing` parameter to `False`. - @component - def my_component(): - return html.div( - view_to_component(hello_world_view, strict_parsing=False), - ) - ``` + === "components.py" + + ```python + from idom import component, html + from django_idom.components import view_to_component + from .views import hello_world_view + + @component + def my_component(): + return html.div( + view_to_component(hello_world_view, strict_parsing=False), + ) + ``` + + === "views.py" + + {% include-markdown "../../includes/examples.md" start="" end="" %} + + Note that best-fit parsing is very similar to how web browsers will handle broken HTML. ## Django CSS From eb1ef6fa71fd401dbdbf7b32a81ddee7deba57e0 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:45:35 -0700 Subject: [PATCH 062/110] always re-register iframe, incase args/kwargs change --- src/django_idom/components.py | 3 +-- src/django_idom/types.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index abacf22a..0662d8e1 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -62,8 +62,7 @@ def view_to_component( dotted_path = dotted_path.replace("<", "").replace(">", "") # Register the iframe's URL if needed - if not IDOM_VIEW_COMPONENT_IFRAMES.get(dotted_path): - IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( + IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( view, args, kwargs ) diff --git a/src/django_idom/types.py b/src/django_idom/types.py index f8cc028c..1cdeef7f 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -1,8 +1,7 @@ from dataclasses import dataclass -from typing import Awaitable, Callable, List, Optional, Tuple, Union +from typing import Awaitable, Callable, Iterable, Optional, Union from django.views.generic import View -from idom.core.component import Component @dataclass @@ -16,5 +15,5 @@ class IdomWebsocket: @dataclass class ViewComponentIframe: view: Union[View, Callable] - args: Tuple + args: Iterable kwargs: dict From 22a16fef6f93356289fd6976bf698b3f2f478baf Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:13:28 -0700 Subject: [PATCH 063/110] clean up Render Check 5: Sync function view --- src/django_idom/components.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 0662d8e1..dc2a83ac 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -104,8 +104,7 @@ async def async_renderer(): # Render Check 5: Sync function view else: - wrapped_view = database_sync_to_async(view) - render = await wrapped_view(request_obj, *args, **kwargs) + render = await database_sync_to_async(view)(request_obj, *args, **kwargs) set_rendered_view(render) From 7b915df7c1ff0b8eab1d2eca5875df14b824bc58 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:13:43 -0700 Subject: [PATCH 064/110] submit args/kwargs to compatibility view --- src/django_idom/http/views.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index eff60293..2f390c34 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -51,13 +51,15 @@ async def view_to_component_iframe( # Render Check 1: Async function view if iscoroutinefunction(iframe.view): - return await async_xframe_options_sameorigin(iframe.view)(request) + return await async_xframe_options_sameorigin(iframe.view)( + request, *iframe.args, **iframe.kwargs + ) # Render Check 2: Async class view if getattr(iframe.view, "view_is_async", False): return await method_decorator(async_xframe_options_sameorigin, name="dispatch")( iframe.view.as_view() - )(request) + )(request, *iframe.args, **iframe.kwargs) # Render Check 3: Sync class view if getattr(iframe.view, "as_view", None): @@ -65,7 +67,9 @@ async def view_to_component_iframe( method_decorator(xframe_options_sameorigin, name="dispatch")( iframe.view.as_view() ) - )(request) + )(request, *iframe.args, **iframe.kwargs) # Render Check 4: Sync function view - return await database_sync_to_async(xframe_options_sameorigin(iframe.view))(request) + return await database_sync_to_async(xframe_options_sameorigin(iframe.view))( + request, *iframe.args, **iframe.kwargs + ) From 956f3908446db9004a051ca6941f7b4939d58fd7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 15 Aug 2022 19:50:27 -0700 Subject: [PATCH 065/110] docstring cleanup --- src/django_idom/components.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index dc2a83ac..658f665b 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -33,8 +33,8 @@ def view_to_component( strict_parsing: If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. request: Request object to provide to the view. - *args: The positional arguments to pass to the view. - **kwargs: The keyword arguments to pass to the view. + args: The positional arguments to pass to the view. + kwargs: The keyword arguments to pass to the view. """ args = args or [] kwargs = kwargs or {} From 4747d7a46373e1fad4309ff094a4a5873311dafd Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 17 Aug 2022 23:44:17 -0700 Subject: [PATCH 066/110] remove x frame options decorator --- docs/src/features/components.md | 2 +- src/django_idom/components.py | 1 + src/django_idom/http/views.py | 34 +++++++++++++----------------- src/django_idom/utils.py | 13 ------------ tests/test_app/templates/base.html | 9 ++++++-- 5 files changed, 24 insertions(+), 35 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 5ae4cd60..465580e7 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -72,7 +72,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe. - Any view can be rendered within compatibility mode. + Any view can be rendered within compatibility mode. However, the `strict_parsing` argument does not apply to compatibility mode. Please note that by default the iframe is unstyled, and thus won't look pretty until you add some CSS. diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 658f665b..10fead51 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -30,6 +30,7 @@ def view_to_component( Keyword Args: compatibility: If True, the component will be rendered in an iframe. + Strict parsing does not apply to compatibility mode. strict_parsing: If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. request: Request object to provide to the view. diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 2f390c34..72aef4b6 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -5,12 +5,9 @@ from channels.db import database_sync_to_async from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest, HttpResponse -from django.utils.decorators import method_decorator -from django.views.decorators.clickjacking import xframe_options_sameorigin from idom.config import IDOM_WED_MODULES_DIR from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES -from django_idom.utils import async_xframe_options_sameorigin async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: @@ -51,25 +48,24 @@ async def view_to_component_iframe( # Render Check 1: Async function view if iscoroutinefunction(iframe.view): - return await async_xframe_options_sameorigin(iframe.view)( - request, *iframe.args, **iframe.kwargs - ) + response = await iframe.view(request, *iframe.args, **iframe.kwargs) # Render Check 2: Async class view - if getattr(iframe.view, "view_is_async", False): - return await method_decorator(async_xframe_options_sameorigin, name="dispatch")( - iframe.view.as_view() - )(request, *iframe.args, **iframe.kwargs) + elif getattr(iframe.view, "view_is_async", False): + response = await iframe.view.as_view()(request, *iframe.args, **iframe.kwargs) # Render Check 3: Sync class view - if getattr(iframe.view, "as_view", None): - return await database_sync_to_async( - method_decorator(xframe_options_sameorigin, name="dispatch")( - iframe.view.as_view() - ) - )(request, *iframe.args, **iframe.kwargs) + elif getattr(iframe.view, "as_view", None): + response = await database_sync_to_async(iframe.view.as_view())( + request, *iframe.args, **iframe.kwargs + ) # Render Check 4: Sync function view - return await database_sync_to_async(xframe_options_sameorigin(iframe.view))( - request, *iframe.args, **iframe.kwargs - ) + else: + response = await database_sync_to_async(iframe.view)( + request, *iframe.args, **iframe.kwargs + ) + + # Ensure page can be rendered as an iframe + response["X-Frame-Options"] = "SAMEORIGIN" + return response diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 48c10ddc..48f75f3d 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -3,7 +3,6 @@ import os import re from fnmatch import fnmatch -from functools import wraps from importlib import import_module from typing import Callable, Set @@ -132,15 +131,3 @@ def _register_components(self, components: Set) -> None: "\033[0m", component, ) - - -def async_xframe_options_sameorigin(view_func): - """Async version of `django.views.decorators.clickjacking.xframe_options_sameorigin`.""" - - async def wrapped_view(*args, **kwargs): - resp = await view_func(*args, **kwargs) - if resp.get("X-Frame-Options") is None: - resp["X-Frame-Options"] = "SAMEORIGIN" - return resp - - return wraps(view_func)(wrapped_view) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index d49a02ef..a0ecf2a5 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -12,6 +12,11 @@

IDOM Test Page

+
{% component "test_app.components.hello_world" class="hello-world" %}
{% component "test_app.components.button" class="button" %}
{% component "test_app.components.parameterized_component" class="parametarized-component" x=123 y=456 %}
@@ -27,13 +32,13 @@

IDOM Test Page

{% component "test_app.components.view_to_component_async_func" %}
{% component "test_app.components.view_to_component_sync_class" %}
{% component "test_app.components.view_to_component_async_class" %}
+
{% component "test_app.components.view_to_component_template_view_class" %}
+
{% component "test_app.components.view_to_component_script" %}
{% component "test_app.components.view_to_component_sync_func_compatibility" %}
{% component "test_app.components.view_to_component_async_func_compatibility" %}
{% component "test_app.components.view_to_component_sync_class_compatibility" %}
{% component "test_app.components.view_to_component_async_class_compatibility" %}
{% component "test_app.components.view_to_component_template_view_class_compatibility" %}
-
{% component "test_app.components.view_to_component_template_view_class" %}
-
{% component "test_app.components.view_to_component_script" %}
\ No newline at end of file From 60aaafe1df484563bd9fb2084102c9624e534d41 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 30 Aug 2022 02:48:42 -0700 Subject: [PATCH 067/110] cleanup old code --- src/django_idom/components.py | 12 ++++++------ src/django_idom/templatetags/idom.py | 8 ++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 10fead51..55343995 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -14,6 +14,8 @@ from django_idom.types import ViewComponentIframe +# TODO: Might want to intercept href clicks and form submit events. +# Form events will probably be accomplished through the upcoming DjangoForm. @component def view_to_component( view: Union[Callable, View], @@ -43,10 +45,8 @@ def view_to_component( # Return the view if it's been rendered via the `async_renderer` rendered_view, set_rendered_view = hooks.use_state(None) if rendered_view: - return html._( - utils.html_to_vdom( - rendered_view.content.decode("utf-8").strip(), strict=strict_parsing - ) + return utils.html_to_vdom( + rendered_view.content.decode("utf-8").strip(), strict=strict_parsing ) # Create a synthetic request object. @@ -64,8 +64,8 @@ def view_to_component( # Register the iframe's URL if needed IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( - view, args, kwargs - ) + view, args, kwargs + ) return html.iframe( { diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index 5e6b7ced..5da0e343 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -33,7 +33,11 @@ def component(_component_id_, **kwargs): _register_component(_component_id_) class_ = kwargs.pop("class", "") - json_kwargs = json.dumps(kwargs, separators=(",", ":")) + component_params = ( + urlencode({"kwargs": json.dumps(kwargs, separators=(",", ":"))}) + if kwargs + else "" + ) return { "class": class_, @@ -42,5 +46,5 @@ def component(_component_id_, **kwargs): "idom_ws_max_reconnect_timeout": IDOM_WS_MAX_RECONNECT_TIMEOUT, "idom_mount_uuid": uuid4().hex, "idom_component_id": _component_id_, - "idom_component_params": urlencode({"kwargs": json_kwargs}), + "idom_component_params": component_params, } From c894d341180af130789a54ccbb87687b55b42c61 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 11 Sep 2022 23:47:09 -0700 Subject: [PATCH 068/110] bump idom version --- requirements/pkg-deps.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index 41f0b6da..322fd017 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,3 +1,3 @@ channels >=3.0.0 -idom >=0.40.0, <0.41.0 +idom >=0.40.1, <0.41.0 aiofile >=3.0 From 95872ace6049787cfc48c64dbfb9d4cfa34ae2e8 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 00:41:35 -0700 Subject: [PATCH 069/110] clean up tests --- tests/test_app/components.py | 146 +++++++++++++++-------------- tests/test_app/templates/base.html | 1 + 2 files changed, 77 insertions(+), 70 deletions(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 44d9907c..9cc2615a 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -1,6 +1,6 @@ import inspect -import idom +from idom import component, hooks, html, web import django_idom from django_idom.components import view_to_component @@ -8,213 +8,219 @@ from . import views -@idom.component +@component def hello_world(): - return idom.html.h1({"id": "hello-world"}, "Hello World!") + return html._(html.h1({"id": "hello-world"}, "Hello World!"), html.hr()) -@idom.component +@component def button(): - count, set_count = idom.hooks.use_state(0) - return idom.html.div( - idom.html.button( - {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)}, - "Click me!", - ), - idom.html.p( - {"id": "counter-num", "data-count": count}, - f"Current count is: {count}", + count, set_count = hooks.use_state(0) + return html._( + html.div( + html.button( + {"id": "counter-inc", "onClick": lambda event: set_count(count + 1)}, + "Click me!", + ), + html.p( + {"id": "counter-num", "data-count": count}, + f"Current count is: {count}", + ), ), + html.hr(), ) -@idom.component +@component def parameterized_component(x, y): total = x + y - return idom.html.h1({"id": "parametrized-component", "data-value": total}, total) + return html._( + html.h1({"id": "parametrized-component", "data-value": total}, total), + html.hr(), + ) -victory = idom.web.module_from_template("react", "victory-bar", fallback="...") -VictoryBar = idom.web.export(victory, "VictoryBar") +victory = web.module_from_template("react", "victory-bar", fallback="...") +VictoryBar = web.export(victory, "VictoryBar") -@idom.component +@component def simple_bar_chart(): - return VictoryBar() + return html._(VictoryBar(), html.hr()) -@idom.component +@component def use_websocket(): ws = django_idom.hooks.use_websocket() - ws.scope = "..." + ws.__delattr__("scope") success = bool(ws.scope and ws.close and ws.disconnect and ws.view_id) - return idom.html.div( + return html.div( {"id": "use-websocket", "data-success": success}, - idom.html.hr(), + html.hr(), f"use_websocket: {ws}", - idom.html.hr(), + html.hr(), ) -@idom.component +@component def use_scope(): scope = django_idom.hooks.use_scope() success = len(scope) >= 10 and scope["type"] == "websocket" - return idom.html.div( + return html.div( {"id": "use-scope", "data-success": success}, f"use_scope: {scope}", - idom.html.hr(), + html.hr(), ) -@idom.component +@component def use_location(): location = django_idom.hooks.use_location() success = bool(location) - return idom.html.div( + return html.div( {"id": "use-location", "data-success": success}, f"use_location: {location}", - idom.html.hr(), + html.hr(), ) -@idom.component +@component def django_css(): - return idom.html.div( + return html.div( {"id": "django-css"}, django_idom.components.django_css("django-css-test.css"), - idom.html.div({"style": {"display": "inline"}}, "django_css: "), - idom.html.button("This text should be blue."), - idom.html.hr(), + html.div({"style": {"display": "inline"}}, "django_css: "), + html.button("This text should be blue."), + html.hr(), ) -@idom.component +@component def django_js(): success = False - return idom.html._( - idom.html.div( + return html._( + html.div( {"id": "django-js", "data-success": success}, f"django_js: {success}", django_idom.components.django_js("django-js-test.js"), ), - idom.html.hr(), + html.hr(), ) -@idom.component +@component @django_idom.decorators.auth_required( - fallback=idom.html.div( + fallback=html.div( {"id": "unauthorized-user-fallback"}, "unauthorized_user: Success", - idom.html.hr(), + html.hr(), ) ) def unauthorized_user(): - return idom.html.div( + return html.div( {"id": "unauthorized-user"}, "unauthorized_user: Fail", - idom.html.hr(), + html.hr(), ) -@idom.component +@component @django_idom.decorators.auth_required( auth_attribute="is_anonymous", - fallback=idom.html.div( + fallback=html.div( {"id": "authorized-user-fallback"}, "authorized_user: Fail", - idom.html.hr(), + html.hr(), ), ) def authorized_user(): - return idom.html.div( + return html.div( {"id": "authorized-user"}, "authorized_user: Success", - idom.html.hr(), + html.hr(), ) -@idom.component +@component def view_to_component_sync_func(): return view_to_component(views.view_to_component_sync_func) -@idom.component +@component def view_to_component_async_func(): return view_to_component(views.view_to_component_async_func) -@idom.component +@component def view_to_component_sync_class(): return view_to_component(views.ViewToComponentSyncClass) -@idom.component +@component def view_to_component_async_class(): return view_to_component(views.ViewToComponentAsyncClass) -@idom.component +@component def view_to_component_template_view_class(): return view_to_component(views.ViewToComponentTemplateViewClass) -@idom.component +@component def view_to_component_sync_func_compatibility(): - return idom.html.div( + return html.div( {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.view_to_component_sync_func_compatibility, compatibility=True ), - idom.html.hr(), + html.hr(), ) -@idom.component +@component def view_to_component_async_func_compatibility(): - return idom.html.div( + return html.div( {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.view_to_component_async_func_compatibility, compatibility=True ), - idom.html.hr(), + html.hr(), ) -@idom.component +@component def view_to_component_sync_class_compatibility(): - return idom.html.div( + return html.div( {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.ViewToComponentSyncClassCompatibility, compatibility=True ), - idom.html.hr(), + html.hr(), ) -@idom.component +@component def view_to_component_async_class_compatibility(): - return idom.html.div( + return html.div( {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.ViewToComponentAsyncClassCompatibility, compatibility=True ), - idom.html.hr(), + html.hr(), ) -@idom.component +@component def view_to_component_template_view_class_compatibility(): - return idom.html.div( + return html.div( {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.ViewToComponentTemplateViewClassCompatibility, compatibility=True ), - idom.html.hr(), + html.hr(), ) -@idom.component +@component def view_to_component_script(): return view_to_component(views.view_to_component_script) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index a0ecf2a5..c4150d0c 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -17,6 +17,7 @@

IDOM Test Page

width: 100%; } +
{% component "test_app.components.hello_world" class="hello-world" %}
{% component "test_app.components.button" class="button" %}
{% component "test_app.components.parameterized_component" class="parametarized-component" x=123 y=456 %}
From 4531a5a4913f1464c17b99fa27a430b373edf4da Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 01:15:18 -0700 Subject: [PATCH 070/110] fix cache type hints --- src/django_idom/config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/django_idom/config.py b/src/django_idom/config.py index 17c0bdb7..2bf4d4f9 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -1,7 +1,7 @@ from typing import Dict from django.conf import settings -from django.core.cache import DEFAULT_CACHE_ALIAS, caches +from django.core.cache import DEFAULT_CACHE_ALIAS, BaseCache, caches from idom.core.types import ComponentConstructor from django_idom.types import ViewComponentIframe @@ -17,6 +17,6 @@ # Determine if using Django caching or LRU cache if "idom" in getattr(settings, "CACHES", {}): - IDOM_CACHE = caches["idom"] + IDOM_CACHE: BaseCache = caches["idom"] else: - IDOM_CACHE = caches[DEFAULT_CACHE_ALIAS] + IDOM_CACHE: BaseCache = caches[DEFAULT_CACHE_ALIAS] From 941b5089978743eaf3d7aaef487c62444bd8186c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 01:20:34 -0700 Subject: [PATCH 071/110] add type: ignore for some instances --- src/django_idom/components.py | 6 +++--- tests/test_app/components.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 55343995..d0fb770e 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -59,7 +59,7 @@ def view_to_component( # Render Check 1: Compatibility mode if compatibility: - dotted_path = f"{view.__module__}.{view.__name__}" + dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore dotted_path = dotted_path.replace("<", "").replace(">", "") # Register the iframe's URL if needed @@ -88,7 +88,7 @@ async def async_renderer(): # Render Check 3: Async class view elif getattr(view, "view_is_async", False): async_cbv = view.as_view() - async_view = await async_cbv(request_obj, *args, **kwargs) + async_view = await async_cbv(request_obj, *args, **kwargs) # type: ignore if getattr(async_view, "render", None): render = await async_view.render() else: @@ -105,7 +105,7 @@ async def async_renderer(): # Render Check 5: Sync function view else: - render = await database_sync_to_async(view)(request_obj, *args, **kwargs) + render = await database_sync_to_async(view)(request_obj, *args, **kwargs) # type: ignore set_rendered_view(render) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 9cc2615a..4c609f8e 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -169,7 +169,7 @@ def view_to_component_template_view_class(): @component def view_to_component_sync_func_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, + {"id": inspect.currentframe().f_code.co_name}, # type: ignore view_to_component( views.view_to_component_sync_func_compatibility, compatibility=True ), @@ -180,7 +180,7 @@ def view_to_component_sync_func_compatibility(): @component def view_to_component_async_func_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, + {"id": inspect.currentframe().f_code.co_name}, # type: ignore view_to_component( views.view_to_component_async_func_compatibility, compatibility=True ), @@ -191,7 +191,7 @@ def view_to_component_async_func_compatibility(): @component def view_to_component_sync_class_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, + {"id": inspect.currentframe().f_code.co_name}, # type: ignore view_to_component( views.ViewToComponentSyncClassCompatibility, compatibility=True ), @@ -202,7 +202,7 @@ def view_to_component_sync_class_compatibility(): @component def view_to_component_async_class_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, + {"id": inspect.currentframe().f_code.co_name}, # type: ignore view_to_component( views.ViewToComponentAsyncClassCompatibility, compatibility=True ), @@ -213,7 +213,7 @@ def view_to_component_async_class_compatibility(): @component def view_to_component_template_view_class_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, + {"id": inspect.currentframe().f_code.co_name}, # type: ignore view_to_component( views.ViewToComponentTemplateViewClassCompatibility, compatibility=True ), From 728bcc49018cd941ee1d5326bec92fb99c71613a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:57:08 -0700 Subject: [PATCH 072/110] reorganize utils.py to reduce deltas --- src/django_idom/utils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 48f75f3d..744f70a8 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -16,6 +16,14 @@ _logger = logging.getLogger(__name__) +def _register_component(dotted_path: str) -> None: + if dotted_path in IDOM_REGISTERED_COMPONENTS: + return + + IDOM_REGISTERED_COMPONENTS[dotted_path] = _import_dotted_path(dotted_path) + _logger.debug("IDOM has registered component %s", dotted_path) + + def _import_dotted_path(dotted_path: str) -> Callable: """Imports a dotted path and returns the callable.""" module_name, component_name = dotted_path.rsplit(".", 1) @@ -30,14 +38,6 @@ def _import_dotted_path(dotted_path: str) -> Callable: return getattr(module, component_name) -def _register_component(dotted_path: str) -> None: - if dotted_path in IDOM_REGISTERED_COMPONENTS: - return - - IDOM_REGISTERED_COMPONENTS[dotted_path] = _import_dotted_path(dotted_path) - _logger.debug("IDOM has registered component %s", dotted_path) - - class ComponentPreloader: def register_all(self): """Registers all IDOM components found within Django templates.""" From 664f79eb36b8c88c80e0e3126f784c46dff08a2a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 16:15:34 -0700 Subject: [PATCH 073/110] remove delete scope --- tests/test_app/components.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 4c609f8e..451aa1b9 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -52,7 +52,6 @@ def simple_bar_chart(): @component def use_websocket(): ws = django_idom.hooks.use_websocket() - ws.__delattr__("scope") success = bool(ws.scope and ws.close and ws.disconnect and ws.view_id) return html.div( {"id": "use-websocket", "data-success": success}, From f9117cd2ccc56ffcaed042f75dd68890bdef7693 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 17:28:28 -0700 Subject: [PATCH 074/110] use playwright for tests --- noxfile.py | 17 +-- requirements/test-env.txt | 2 +- src/django_idom/websocket/consumer.py | 6 +- src/js/package-lock.json | 14 +- src/js/package.json | 2 +- tests/test_app/tests/test_components.py | 173 +++++++++++------------- 6 files changed, 99 insertions(+), 115 deletions(-) diff --git a/noxfile.py b/noxfile.py index b28665df..afb8b77f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,10 +1,7 @@ from __future__ import annotations -import os import re -import subprocess from pathlib import Path -from typing import List, Tuple import nox from nox.sessions import Session @@ -50,13 +47,14 @@ def test_suite(session: Session) -> None: session.env["IDOM_DEBUG_MODE"] = "1" posargs = session.posargs[:] - if "--headless" in posargs: - posargs.remove("--headless") - session.env["SELENIUM_HEADLESS"] = "1" + if "--headed" in posargs: + posargs.remove("--headed") + session.env["PLAYWRIGHT_HEADED"] = "1" if "--no-debug-mode" not in posargs: posargs.append("--debug-mode") + session.run("playwright", "install", "chromium") session.run("python", "manage.py", "test", *posargs) @@ -65,18 +63,17 @@ def test_style(session: Session) -> None: """Check that style guidelines are being followed""" install_requirements_file(session, "check-style") session.run("flake8", "src/django_idom", "tests") - black_default_exclude = r"\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist" session.run( "black", ".", "--check", - "--exclude", - rf"/({black_default_exclude}|venv|node_modules)/", + "--extend-exclude", + "/migrations/", ) session.run("isort", ".", "--check-only") def install_requirements_file(session: Session, name: str) -> None: - file_path = HERE / "requirements" / (name + ".txt") + file_path = HERE / "requirements" / f"{name}.txt" assert file_path.exists(), f"requirements file {file_path} does not exist" session.install("-r", str(file_path)) diff --git a/requirements/test-env.txt b/requirements/test-env.txt index 61ee65ee..32187f96 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -1,3 +1,3 @@ django -selenium <= 4.2.0 +playwright twisted diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index cdc3eb9f..a41e6d50 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -5,7 +5,6 @@ from typing import Any from urllib.parse import parse_qsl -from channels.auth import login from channels.db import database_sync_to_async as convert_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from idom.core.layout import Layout, LayoutEvent @@ -23,6 +22,9 @@ class IdomAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer): """Communicates with the browser to perform actions on-demand.""" async def connect(self) -> None: + # this triggers AppRegistryNotReady exception in manage.py if at root level + from channels.auth import login + await super().connect() user = self.scope.get("user") @@ -74,7 +76,7 @@ async def _run_dispatch_loop(self): self._idom_recv_queue = recv_queue = asyncio.Queue() try: await serve_json_patch( - Layout(WebsocketContext(component_instance, value=socket)), + Layout(WebsocketContext(component_instance, value=socket)), # type: ignore self.send_json, recv_queue.get, ) diff --git a/src/js/package-lock.json b/src/js/package-lock.json index 54aa3359..989959a7 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "idom-client-react": "^0.37.2", + "idom-client-react": "^0.40.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, @@ -99,9 +99,9 @@ "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" }, "node_modules/idom-client-react": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.37.2.tgz", - "integrity": "sha512-9yUp39Ah57EXmdzfRF9yL9aXk3MnQgK9S+i01dTbZIfMaTdDtgfjj9sdQKmM+lEFKY6nSgkGenohuB4h1ZOy7Q==", + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.40.1.tgz", + "integrity": "sha512-vqeVIWSwLRoPUet88Kek664Q2W/+9JJy6f6oxjN1tW+j5cq6eVMrgbsmvPsvhbkLKynXwaHaaaTRiZ+Eprktzg==", "dependencies": { "fast-json-patch": "^3.0.0-1", "htm": "^3.0.3" @@ -379,9 +379,9 @@ "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" }, "idom-client-react": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.37.2.tgz", - "integrity": "sha512-9yUp39Ah57EXmdzfRF9yL9aXk3MnQgK9S+i01dTbZIfMaTdDtgfjj9sdQKmM+lEFKY6nSgkGenohuB4h1ZOy7Q==", + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/idom-client-react/-/idom-client-react-0.40.1.tgz", + "integrity": "sha512-vqeVIWSwLRoPUet88Kek664Q2W/+9JJy6f6oxjN1tW+j5cq6eVMrgbsmvPsvhbkLKynXwaHaaaTRiZ+Eprktzg==", "requires": { "fast-json-patch": "^3.0.0-1", "htm": "^3.0.3" diff --git a/src/js/package.json b/src/js/package.json index 67bfb2a4..1f17dfd6 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -16,7 +16,7 @@ "rollup-plugin-replace": "^2.2.0" }, "dependencies": { - "idom-client-react": "^0.37.2", + "idom-client-react": "^0.40.1", "react": "^17.0.2", "react-dom": "^17.0.2" } diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 7b1f0518..fb4418fa 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -1,98 +1,83 @@ import os import sys +from unittest import SkipTest from channels.testing import ChannelsLiveServerTestCase -from selenium import webdriver -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.wait import WebDriverWait - - -# These tests are broken on Windows due to Selenium -if sys.platform != "win32": - - class TestIdomCapabilities(ChannelsLiveServerTestCase): - def setUp(self): - self.driver = make_driver(5, 5) - self.driver.get(self.live_server_url) - - def tearDown(self) -> None: - self.driver.quit() - - def wait(self, timeout=10): - return WebDriverWait(self.driver, timeout) - - def wait_until(self, condition, timeout=10): - return self.wait(timeout).until(lambda driver: condition()) - - def test_hello_world(self): - self.driver.find_element_by_id("hello-world") - - def test_counter(self): - button = self.driver.find_element_by_id("counter-inc") - count = self.driver.find_element_by_id("counter-num") - - for i in range(5): - self.wait_until(lambda: count.get_attribute("data-count") == str(i)) - button.click() - - def test_parametrized_component(self): - element = self.driver.find_element_by_id("parametrized-component") - self.assertEqual(element.get_attribute("data-value"), "579") - - def test_component_from_web_module(self): - self.wait(20).until( - expected_conditions.visibility_of_element_located( - (By.CLASS_NAME, "VictoryContainer") - ) - ) - - def test_use_websocket(self): - element = self.driver.find_element_by_id("use-websocket") - self.assertEqual(element.get_attribute("data-success"), "true") - - def test_use_scope(self): - element = self.driver.find_element_by_id("use-scope") - self.assertEqual(element.get_attribute("data-success"), "true") - - def test_use_location(self): - element = self.driver.find_element_by_id("use-location") - self.assertEqual(element.get_attribute("data-success"), "true") - - def test_static_css(self): - element = self.driver.find_element_by_css_selector("#django-css button") - self.assertEqual( - element.value_of_css_property("color"), "rgba(0, 0, 255, 1)" - ) - - def test_static_js(self): - element = self.driver.find_element_by_id("django-js") - self.assertEqual(element.get_attribute("data-success"), "true") - - def test_unauthorized_user(self): - self.assertRaises( - NoSuchElementException, - self.driver.find_element_by_id, - "unauthorized-user", - ) - element = self.driver.find_element_by_id("unauthorized-user-fallback") - self.assertIsNotNone(element) - - def test_authorized_user(self): - self.assertRaises( - NoSuchElementException, - self.driver.find_element_by_id, - "authorized-user-fallback", - ) - element = self.driver.find_element_by_id("authorized-user") - self.assertIsNotNone(element) - - -def make_driver(page_load_timeout, implicit_wait_timeout): - options = webdriver.ChromeOptions() - options.headless = bool(int(os.environ.get("SELENIUM_HEADLESS", 0))) - driver = webdriver.Chrome(options=options) - driver.set_page_load_timeout(page_load_timeout) - driver.implicitly_wait(implicit_wait_timeout) - return driver +from django.test import TransactionTestCase +from playwright.sync_api import TimeoutError, sync_playwright + + +class TestIdomCapabilities(ChannelsLiveServerTestCase, TransactionTestCase): + @classmethod + def setUpClass(cls): + if sys.platform == "win32": + raise SkipTest("These tests are broken on Windows due to Selenium") + super().setUpClass() + cls.playwright = sync_playwright().start() + headed = bool(int(os.environ.get("PLAYWRIGHT_HEADED", 0))) + cls.browser = cls.playwright.chromium.launch(headless=not headed) + cls.page = cls.browser.new_page() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.page.close() + cls.browser.close() + cls.playwright.stop() + + def setUp(self): + super().setUp() + self.page.goto(self.live_server_url) + + def test_hello_world(self): + self.page.wait_for_selector("#hello-world") + + def test_counter(self): + for i in range(5): + self.page.locator(f"#counter-num[data-count={i}]") + self.page.locator("#counter-inc").click() + + def test_parametrized_component(self): + self.page.locator("#parametrized-component[data-value='579']").wait_for() + + def test_component_from_web_module(self): + self.page.wait_for_selector(".VictoryContainer") + + def test_use_websocket(self): + self.page.locator("#use-websocket[data-success=true]").wait_for() + + def test_use_scope(self): + self.page.locator("#use-fuck").wait_for() + self.page.locator("#use-scope[data-success=true]").wait_for() + + def test_use_location(self): + self.page.locator("#use-location[data-success=true]").wait_for() + + def test_static_css(self): + self.assertEqual( + self.page.wait_for_selector("#django-css button").evaluate( + "e => window.getComputedStyle(e).getPropertyValue('color')" + ), + "rgb(0, 0, 255)", + ) + + def test_static_js(self): + self.page.locator("#django-js[data-success=true]").wait_for() + + def test_unauthorized_user(self): + self.assertRaises( + TimeoutError, + self.page.wait_for_selector, + "#unauthorized-user", + timeout=1, + ) + self.page.wait_for_selector("#unauthorized-user-fallback") + + def test_authorized_user(self): + self.assertRaises( + TimeoutError, + self.page.wait_for_selector, + "#authorized-user-fallback", + timeout=1, + ) + self.page.wait_for_selector("#authorized-user") From cd744039dd826d96d3157adfcac5ae657f318321 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 17:46:07 -0700 Subject: [PATCH 075/110] revert changes to idom.py --- src/django_idom/templatetags/idom.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index 5da0e343..5e6b7ced 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -33,11 +33,7 @@ def component(_component_id_, **kwargs): _register_component(_component_id_) class_ = kwargs.pop("class", "") - component_params = ( - urlencode({"kwargs": json.dumps(kwargs, separators=(",", ":"))}) - if kwargs - else "" - ) + json_kwargs = json.dumps(kwargs, separators=(",", ":")) return { "class": class_, @@ -46,5 +42,5 @@ def component(_component_id_, **kwargs): "idom_ws_max_reconnect_timeout": IDOM_WS_MAX_RECONNECT_TIMEOUT, "idom_mount_uuid": uuid4().hex, "idom_component_id": _component_id_, - "idom_component_params": component_params, + "idom_component_params": urlencode({"kwargs": json_kwargs}), } From 89786f730511b0c25e10b16e0b0682119dd5a342 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 17:59:59 -0700 Subject: [PATCH 076/110] fix tests --- .github/workflows/test-src.yml | 2 +- docs/src/contribute/running-tests.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml index f5320f35..f469f888 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -32,4 +32,4 @@ jobs: run: | npm install -g npm@latest npm --version - nox -s test -- --headless + nox -s test diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md index 287b9301..91d1c85b 100644 --- a/docs/src/contribute/running-tests.md +++ b/docs/src/contribute/running-tests.md @@ -4,8 +4,8 @@ This repo uses [Nox](https://nox.thea.codes/en/stable/) to run scripts which can nox -s test ``` -If you want to run the tests in the background (headless): +If you do not want to run the tests in the background: ``` -nox -s test -- --headless +nox -s test -- --headed ``` From c4e63f1328009ed6828a663ec22f83210079cdf9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:05:19 -0700 Subject: [PATCH 077/110] try removing transaction testcase --- tests/test_app/tests/test_components.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index fb4418fa..e9a9fffb 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -3,11 +3,10 @@ from unittest import SkipTest from channels.testing import ChannelsLiveServerTestCase -from django.test import TransactionTestCase from playwright.sync_api import TimeoutError, sync_playwright -class TestIdomCapabilities(ChannelsLiveServerTestCase, TransactionTestCase): +class TestIdomCapabilities(ChannelsLiveServerTestCase): @classmethod def setUpClass(cls): if sys.platform == "win32": From 44815c1c779e7beed32e902a4f5803b8196a3ada Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 00:21:44 -0700 Subject: [PATCH 078/110] clean up --- tests/test_app/components.py | 1 - tests/test_app/views.py | 13 ++++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 451aa1b9..2459d35d 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -55,7 +55,6 @@ def use_websocket(): success = bool(ws.scope and ws.close and ws.disconnect and ws.view_id) return html.div( {"id": "use-websocket", "data-success": success}, - html.hr(), f"use_websocket: {ws}", html.hr(), ) diff --git a/tests/test_app/views.py b/tests/test_app/views.py index f2245840..885293ab 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -14,7 +14,7 @@ def view_to_component_sync_func(request): return render( request, "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, + {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore ) @@ -22,7 +22,7 @@ async def view_to_component_async_func(request): return render( request, "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, + {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore ) @@ -55,7 +55,7 @@ def view_to_component_sync_func_compatibility(request): return render( request, "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, + {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore ) @@ -63,7 +63,7 @@ async def view_to_component_async_func_compatibility(request): return await database_sync_to_async(render)( request, "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, + {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore ) @@ -96,5 +96,8 @@ def view_to_component_script(request): return render( request, "view_to_component_script.html", - {"test_name": inspect.currentframe().f_code.co_name, "status": "Fail"}, + { + "test_name": inspect.currentframe().f_code.co_name, # type: ignore + "status": "false", + }, ) From 998e6328dd15676591f418fcdf5224102d6db6a2 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 00:35:12 -0700 Subject: [PATCH 079/110] tests for view_to_component --- docs/src/contribute/running-tests.md | 4 ++ .../test_app/templates/view_to_component.html | 3 +- tests/test_app/tests/test_components.py | 71 ++++++++++++++++++- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/docs/src/contribute/running-tests.md b/docs/src/contribute/running-tests.md index 91d1c85b..ece8d1a7 100644 --- a/docs/src/contribute/running-tests.md +++ b/docs/src/contribute/running-tests.md @@ -9,3 +9,7 @@ If you do not want to run the tests in the background: ``` nox -s test -- --headed ``` + +!!! warning "Most tests will not run on Windows" + + Due to [bugs within Django Channels](https://github.com/django/channels/issues/1207), functional tests are not run on Windows. In order for Windows users to test Django-IDOM functionality, you will need to run tests via [Windows Subsystem for Linux](https://code.visualstudio.com/docs/remote/wsl). diff --git a/tests/test_app/templates/view_to_component.html b/tests/test_app/templates/view_to_component.html index b0d02da6..e2de762a 100644 --- a/tests/test_app/templates/view_to_component.html +++ b/tests/test_app/templates/view_to_component.html @@ -1,2 +1,3 @@ -
{{ test_name }}: {% firstof status "Success" %}
+
{{ test_name }}: {% firstof status "Success" %} +

{% block bottom %}{% endblock %} \ No newline at end of file diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index e9a9fffb..72023962 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -10,7 +10,18 @@ class TestIdomCapabilities(ChannelsLiveServerTestCase): @classmethod def setUpClass(cls): if sys.platform == "win32": - raise SkipTest("These tests are broken on Windows due to Selenium") + raise SkipTest("These tests are broken on Windows.") + + # FIXME: The following lines will be needed once Django channels fixes Windows tests + # See: https://github.com/django/channels/issues/1207 + + # import asyncio + # asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + + # FIXME: This is required otherwise the tests will throw a `SynchronousOnlyOperation` + # error when deleting the test datatabase. Potentially a Django bug. + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + super().setUpClass() cls.playwright = sync_playwright().start() headed = bool(int(os.environ.get("PLAYWRIGHT_HEADED", 0))) @@ -46,7 +57,6 @@ def test_use_websocket(self): self.page.locator("#use-websocket[data-success=true]").wait_for() def test_use_scope(self): - self.page.locator("#use-fuck").wait_for() self.page.locator("#use-scope[data-success=true]").wait_for() def test_use_location(self): @@ -54,7 +64,7 @@ def test_use_location(self): def test_static_css(self): self.assertEqual( - self.page.wait_for_selector("#django-css button").evaluate( + self.page.wait_for_selector("#django-css button").evaluate( # type: ignore "e => window.getComputedStyle(e).getPropertyValue('color')" ), "rgb(0, 0, 255)", @@ -80,3 +90,58 @@ def test_authorized_user(self): timeout=1, ) self.page.wait_for_selector("#authorized-user") + + def test_view_to_component_sync_func(self): + self.page.locator("#view_to_component_sync_func[data-success=true]").wait_for() + + def test_view_to_component_async_func(self): + self.page.locator("#view_to_component_async_func[data-success=true]").wait_for() + + def test_view_to_component_sync_class(self): + self.page.locator("#ViewToComponentSyncClass[data-success=true]").wait_for() + + def test_view_to_component_async_class(self): + self.page.locator("#ViewToComponentAsyncClass[data-success=true]").wait_for() + + def test_view_to_component_template_view_class(self): + self.page.locator( + "#ViewToComponentTemplateViewClass[data-success=true]" + ).wait_for() + + def test_view_to_component_script(self): + self.page.locator("#view_to_component_script[data-success=true]").wait_for() + + def test_view_to_component_sync_func_compatibility(self): + self.page.frame_locator( + "#view_to_component_sync_func_compatibility > iframe" + ).locator( + "#view_to_component_sync_func_compatibility[data-success=true]" + ).wait_for() + + def test_view_to_component_async_func_compatibility(self): + self.page.frame_locator( + "#view_to_component_async_func_compatibility > iframe" + ).locator( + "#view_to_component_async_func_compatibility[data-success=true]" + ).wait_for() + + def test_view_to_component_sync_class_compatibility(self): + self.page.frame_locator( + "#view_to_component_sync_class_compatibility > iframe" + ).locator( + "#ViewToComponentSyncClassCompatibility[data-success=true]" + ).wait_for() + + def test_view_to_component_async_class_compatibility(self): + self.page.frame_locator( + "#view_to_component_async_class_compatibility > iframe" + ).locator( + "#ViewToComponentAsyncClassCompatibility[data-success=true]" + ).wait_for() + + def test_view_to_component_template_view_class_compatibility(self): + self.page.frame_locator( + "#view_to_component_template_view_class_compatibility > iframe" + ).locator( + "#ViewToComponentTemplateViewClassCompatibility[data-success=true]" + ).wait_for() From 5d039adebaf44f2f813f2827e8b27ccd8d4d5c0f Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 00:41:29 -0700 Subject: [PATCH 080/110] remove unneeded movement of import --- src/django_idom/websocket/consumer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index a41e6d50..4f9a0051 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -5,6 +5,7 @@ from typing import Any from urllib.parse import parse_qsl +from channels.auth import login from channels.db import database_sync_to_async as convert_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from idom.core.layout import Layout, LayoutEvent @@ -22,9 +23,6 @@ class IdomAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer): """Communicates with the browser to perform actions on-demand.""" async def connect(self) -> None: - # this triggers AppRegistryNotReady exception in manage.py if at root level - from channels.auth import login - await super().connect() user = self.scope.get("user") From 6833e9157990efc6827f607c92246cdd3fbdafb4 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 01:16:30 -0700 Subject: [PATCH 081/110] fix mypy warnings at the cost of adding more pylance warnings --- src/django_idom/components.py | 10 ++++++---- src/django_idom/config.py | 9 +++++---- src/django_idom/http/urls.py | 2 +- src/django_idom/http/views.py | 12 ++++++------ src/django_idom/utils.py | 1 + src/django_idom/websocket/consumer.py | 2 +- tests/test_app/components.py | 10 +++++----- tests/test_app/tests/test_components.py | 2 +- tests/test_app/views.py | 10 +++++----- 9 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index d0fb770e..4b0b5513 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -88,7 +88,7 @@ async def async_renderer(): # Render Check 3: Async class view elif getattr(view, "view_is_async", False): async_cbv = view.as_view() - async_view = await async_cbv(request_obj, *args, **kwargs) # type: ignore + async_view = await async_cbv(request_obj, *args, **kwargs) if getattr(async_view, "render", None): render = await async_view.render() else: @@ -105,10 +105,12 @@ async def async_renderer(): # Render Check 5: Sync function view else: - render = await database_sync_to_async(view)(request_obj, *args, **kwargs) # type: ignore + render = await database_sync_to_async(view)(request_obj, *args, **kwargs) set_rendered_view(render) + return None + @component def django_css(static_path: str): @@ -144,13 +146,13 @@ 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=last_modified_time) + file_contents = 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( - cache_key, file_contents, timeout=None, version=last_modified_time + cache_key, file_contents, timeout=None, version=int(last_modified_time) ) return file_contents diff --git a/src/django_idom/config.py b/src/django_idom/config.py index 2bf4d4f9..b87fb7e9 100644 --- a/src/django_idom/config.py +++ b/src/django_idom/config.py @@ -16,7 +16,8 @@ ) # Determine if using Django caching or LRU cache -if "idom" in getattr(settings, "CACHES", {}): - IDOM_CACHE: BaseCache = caches["idom"] -else: - IDOM_CACHE: BaseCache = caches[DEFAULT_CACHE_ALIAS] +IDOM_CACHE: BaseCache = ( + caches["idom"] + if "idom" in getattr(settings, "CACHES", {}) + else caches[DEFAULT_CACHE_ALIAS] +) diff --git a/src/django_idom/http/urls.py b/src/django_idom/http/urls.py index 8d4f25a0..50b0c4ec 100644 --- a/src/django_idom/http/urls.py +++ b/src/django_idom/http/urls.py @@ -13,7 +13,7 @@ ), path( "iframe/", - views.view_to_component_iframe, + views.view_to_component_iframe, # type: ignore[arg-type] name="view_to_component", ), ] diff --git a/src/django_idom/http/views.py b/src/django_idom/http/views.py index 72aef4b6..033e9ffd 100644 --- a/src/django_idom/http/views.py +++ b/src/django_idom/http/views.py @@ -25,12 +25,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 = f"django_idom:web_module:{str(path).lstrip(str(web_modules_dir))}" - response = await IDOM_CACHE.aget(cache_key, version=last_modified_time) + response = await IDOM_CACHE.aget(cache_key, version=last_modified_time) # type: ignore[attr-defined] 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 IDOM_CACHE.adelete(cache_key) # type: ignore[attr-defined] + await IDOM_CACHE.aset( # type: ignore[attr-defined] cache_key, response, timeout=None, version=last_modified_time ) return response @@ -48,15 +48,15 @@ async def view_to_component_iframe( # Render Check 1: Async function view if iscoroutinefunction(iframe.view): - response = await iframe.view(request, *iframe.args, **iframe.kwargs) + response = await iframe.view(request, *iframe.args, **iframe.kwargs) # type: ignore[operator] # Render Check 2: Async class view elif getattr(iframe.view, "view_is_async", False): - response = await iframe.view.as_view()(request, *iframe.args, **iframe.kwargs) + response = await iframe.view.as_view()(request, *iframe.args, **iframe.kwargs) # type: ignore[misc, union-attr] # Render Check 3: Sync class view elif getattr(iframe.view, "as_view", None): - response = await database_sync_to_async(iframe.view.as_view())( + response = await database_sync_to_async(iframe.view.as_view())( # type: ignore[union-attr] request, *iframe.args, **iframe.kwargs ) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 08c015f0..d1dba264 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -6,6 +6,7 @@ import re from fnmatch import fnmatch from importlib import import_module +from typing import Callable from django.template import engines from django.utils.encoding import smart_str diff --git a/src/django_idom/websocket/consumer.py b/src/django_idom/websocket/consumer.py index 4f9a0051..cdc3eb9f 100644 --- a/src/django_idom/websocket/consumer.py +++ b/src/django_idom/websocket/consumer.py @@ -74,7 +74,7 @@ async def _run_dispatch_loop(self): self._idom_recv_queue = recv_queue = asyncio.Queue() try: await serve_json_patch( - Layout(WebsocketContext(component_instance, value=socket)), # type: ignore + Layout(WebsocketContext(component_instance, value=socket)), self.send_json, recv_queue.get, ) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 2459d35d..8a881f6d 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -167,7 +167,7 @@ def view_to_component_template_view_class(): @component def view_to_component_sync_func_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.view_to_component_sync_func_compatibility, compatibility=True ), @@ -178,7 +178,7 @@ def view_to_component_sync_func_compatibility(): @component def view_to_component_async_func_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.view_to_component_async_func_compatibility, compatibility=True ), @@ -189,7 +189,7 @@ def view_to_component_async_func_compatibility(): @component def view_to_component_sync_class_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.ViewToComponentSyncClassCompatibility, compatibility=True ), @@ -200,7 +200,7 @@ def view_to_component_sync_class_compatibility(): @component def view_to_component_async_class_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.ViewToComponentAsyncClassCompatibility, compatibility=True ), @@ -211,7 +211,7 @@ def view_to_component_async_class_compatibility(): @component def view_to_component_template_view_class_compatibility(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, view_to_component( views.ViewToComponentTemplateViewClassCompatibility, compatibility=True ), diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 72023962..95539f36 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -64,7 +64,7 @@ def test_use_location(self): def test_static_css(self): self.assertEqual( - self.page.wait_for_selector("#django-css button").evaluate( # type: ignore + self.page.wait_for_selector("#django-css button").evaluate( "e => window.getComputedStyle(e).getPropertyValue('color')" ), "rgb(0, 0, 255)", diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 885293ab..8e14eb69 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -14,7 +14,7 @@ def view_to_component_sync_func(request): return render( request, "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore + {"test_name": inspect.currentframe().f_code.co_name}, ) @@ -22,7 +22,7 @@ async def view_to_component_async_func(request): return render( request, "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore + {"test_name": inspect.currentframe().f_code.co_name}, ) @@ -55,7 +55,7 @@ def view_to_component_sync_func_compatibility(request): return render( request, "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore + {"test_name": inspect.currentframe().f_code.co_name}, ) @@ -63,7 +63,7 @@ async def view_to_component_async_func_compatibility(request): return await database_sync_to_async(render)( request, "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore + {"test_name": inspect.currentframe().f_code.co_name}, ) @@ -97,7 +97,7 @@ def view_to_component_script(request): request, "view_to_component_script.html", { - "test_name": inspect.currentframe().f_code.co_name, # type: ignore + "test_name": inspect.currentframe().f_code.co_name, "status": "false", }, ) From 7c388802244cbca3d4e04436f455ba0a722cbed7 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 03:07:05 -0700 Subject: [PATCH 082/110] remove duplicate todo --- src/django_idom/components.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 4b0b5513..bb2c5747 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -53,8 +53,6 @@ def view_to_component( request_obj = request if not request: request_obj = HttpRequest() - # TODO: Figure out some intelligent way to set the method. - # Might need intercepting common things such as form submission? request_obj.method = "GET" # Render Check 1: Compatibility mode From 213eb722c8959db1bf11ff4514570f8141879665 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 14:41:35 -0700 Subject: [PATCH 083/110] Add interface docs for View to Component --- docs/src/features/components.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 465580e7..d91ae57f 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -20,6 +20,26 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible {% include-markdown "../../includes/examples.md" start="" end="" %} +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | view | `Union[Callable, View]` | The view function or class to convert. | N/A | + | compatibility | `bool` | If True, the component will be rendered in an iframe. Strict parsing does not apply to compatibility mode. | `False` | + | strict_parsing | `bool` | If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `True` | + | request | `Union[HttpRequest, None]` | Request object to provide to the view. | `None` | + | args | `Union[Iterable, None]` | The positional arguments to pass to the view. | `None` | + | kwargs | `Union[Dict, None]` | The keyword arguments to pass to the view. | `None` | + + **Returns** + + | Type | Description | + | --- | --- | + | `Component` | An IDOM component. | + | `None` | No component render. | + ??? question "How do I use this for Class Based Views?" You can simply pass your Class Based View directly into this function. From d58be7ccb79bc57e06d1ed7a3df81495e828f3ec Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 15:23:04 -0700 Subject: [PATCH 084/110] add transforms variable --- docs/src/features/components.md | 1 + src/django_idom/components.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index d91ae57f..f7437f8a 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -28,6 +28,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible | --- | --- | --- | --- | | view | `Union[Callable, View]` | The view function or class to convert. | N/A | | compatibility | `bool` | If True, the component will be rendered in an iframe. Strict parsing does not apply to compatibility mode. | `False` | + | transforms | `Iterable[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `tuple` | | strict_parsing | `bool` | If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `True` | | request | `Union[HttpRequest, None]` | Request object to provide to the view. | `None` | | args | `Union[Iterable, None]` | The positional arguments to pass to the view. | `None` | diff --git a/src/django_idom/components.py b/src/django_idom/components.py index bb2c5747..261904ed 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -1,6 +1,6 @@ import os from inspect import iscoroutinefunction -from typing import Callable, Dict, Iterable, Union +from typing import Any, Callable, Dict, Iterable, Union from channels.db import database_sync_to_async from django.contrib.staticfiles.finders import find @@ -20,6 +20,7 @@ def view_to_component( view: Union[Callable, View], compatibility: bool = False, + transforms: Iterable[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, request: Union[HttpRequest, None] = None, args: Union[Iterable, None] = None, @@ -33,6 +34,8 @@ def view_to_component( Keyword Args: compatibility: If True, the component will be rendered in an iframe. Strict parsing does not apply to compatibility mode. + transforms: A list of functions that transforms the newly generated VDOM. + The functions will be called on each VDOM node. strict_parsing: If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. request: Request object to provide to the view. @@ -46,7 +49,9 @@ def view_to_component( rendered_view, set_rendered_view = hooks.use_state(None) if rendered_view: return utils.html_to_vdom( - rendered_view.content.decode("utf-8").strip(), strict=strict_parsing + rendered_view.content.decode("utf-8").strip(), + *transforms, + strict=strict_parsing, ) # Create a synthetic request object. From edc1839f0472ef07e017b709b4961c105b16b946 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 15:51:42 -0700 Subject: [PATCH 085/110] transforms arg docs --- docs/src/features/components.md | 44 ++++++++++++++++++++++++++++++--- src/django_idom/components.py | 3 +-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index f7437f8a..d919e607 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -31,7 +31,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible | transforms | `Iterable[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `tuple` | | strict_parsing | `bool` | If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `True` | | request | `Union[HttpRequest, None]` | Request object to provide to the view. | `None` | - | args | `Union[Iterable, None]` | The positional arguments to pass to the view. | `None` | + | args | `Iterable` | The positional arguments to pass to the view. | `tuple` | | kwargs | `Union[Dict, None]` | The keyword arguments to pass to the view. | `None` | **Returns** @@ -89,7 +89,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible {% include-markdown "../../includes/examples.md" start="" end="" %} -??? question "What is compatibility mode?" +??? question "What is `compatibility` mode?" For views that rely on HTTP responses other than `GET` (such as `PUT`, `POST`, `PATCH`, etc), you should consider using compatibility mode to render your view within an iframe. @@ -115,7 +115,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible {% include-markdown "../../includes/examples.md" start="" end="" %} -??? question "What is strict parsing?" +??? question "What is `strict_parsing`?" By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5. @@ -143,6 +143,44 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible Note that best-fit parsing is very similar to how web browsers will handle broken HTML. +??? question "What is `transforms`?" + + After your view has been turned into [VDOM](https://idom-docs.herokuapp.com/docs/reference/specifications.html#vdom) (python dictionaries), `view_to_component` will call your `transforms` functions on every VDOM node. + + This allows you to modify your view prior to rendering. + + For example, if you are trying to modify the text of a node with a certain `id`, you can create a transform like such: + + === "components.py" + + ```python + from idom import component, html + from django_idom.components import view_to_component + from .views import hello_world_view + + def example_transform(vdom): + attributes = vdom.get("attributes") + + if attributes and attributes.get("id") == "hello-world": + vdom["children"][0] = "Good Bye World!" + + @component + def my_component(): + return view_to_component( + hello_world_view, + transforms=[example_transform], + ) + ``` + + === "views.py" + + ```python + from django.http import HttpResponse + + def hello_world_view(request, *args, **kwargs): + return HttpResponse("
Hello World!
") + ``` + ## Django CSS Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 261904ed..b6a5874f 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -23,7 +23,7 @@ def view_to_component( transforms: Iterable[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, request: Union[HttpRequest, None] = None, - args: Union[Iterable, None] = None, + args: Iterable = (), kwargs: Union[Dict, None] = None, ) -> Union[VdomDict, None]: """Converts a Django view to an IDOM component. @@ -42,7 +42,6 @@ def view_to_component( args: The positional arguments to pass to the view. kwargs: The keyword arguments to pass to the view. """ - args = args or [] kwargs = kwargs or {} # Return the view if it's been rendered via the `async_renderer` From 95065ecfc7e2a10cbc35fc4012539447c07396ea Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 16:58:37 -0700 Subject: [PATCH 086/110] remove tests from mypy --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 1e55dabf..c0c63855 100644 --- a/noxfile.py +++ b/noxfile.py @@ -63,7 +63,7 @@ def test_suite(session: Session) -> None: def test_types(session: Session) -> None: install_requirements_file(session, "check-types") install_requirements_file(session, "pkg-deps") - session.run("mypy", "--show-error-codes", "src/django_idom", "tests/test_app") + session.run("mypy", "--show-error-codes", "src/django_idom") @nox.session From 9597d94b9d77ce3c615bf8c4538ecbf3edd134c3 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Tue, 13 Sep 2022 17:40:30 -0700 Subject: [PATCH 087/110] bump idom version --- requirements/pkg-deps.txt | 2 +- src/js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index 322fd017..a581336f 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,3 +1,3 @@ channels >=3.0.0 -idom >=0.40.1, <0.41.0 +idom >=0.40.2, <0.41.0 aiofile >=3.0 diff --git a/src/js/package.json b/src/js/package.json index 1f17dfd6..4d4112a2 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -16,7 +16,7 @@ "rollup-plugin-replace": "^2.2.0" }, "dependencies": { - "idom-client-react": "^0.40.1", + "idom-client-react": "^0.40.2", "react": "^17.0.2", "react-dom": "^17.0.2" } From 4bbdf64aa551a8e12fa1ac900acf79f4bb8abfce Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 03:24:26 -0700 Subject: [PATCH 088/110] fix hooks link --- docs/src/features/hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index ad4afb34..ef89c3bf 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -1,6 +1,6 @@ ???+ tip "Looking for more hooks?" - Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html?highlight=hooks) on hooks! + Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html#basic-hooks) on hooks! ## Use Websocket From 65a53105b687a1c1268ff284b39b3eb4b7c4bf2a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:22:49 -0700 Subject: [PATCH 089/110] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a4ca3b8..904b27c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Using the following categories, list your changes in this order: - `auth_required` decorator to prevent your components from rendered to unauthenticated users. - `use_query` hook for fetching database values. - `use_mutation` hook for modifying database values. +- `view_to_component` utility to convert legacy Django views to IDOM components. ## [1.1.0] - 2022-07-01 From 11afed21674665fffb6652bdd86d79516b50c11d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:29:08 -0700 Subject: [PATCH 090/110] use __future__ annotations --- docs/src/features/components.md | 6 +++--- src/django_idom/components.py | 12 +++++++----- src/django_idom/decorators.py | 8 +++++--- src/django_idom/types.py | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index d919e607..c9708d9d 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -26,13 +26,13 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible | Name | Type | Description | Default | | --- | --- | --- | --- | - | view | `Union[Callable, View]` | The view function or class to convert. | N/A | + | view | `Callable | View` | The view function or class to convert. | N/A | | compatibility | `bool` | If True, the component will be rendered in an iframe. Strict parsing does not apply to compatibility mode. | `False` | | transforms | `Iterable[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `tuple` | | strict_parsing | `bool` | If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `True` | - | request | `Union[HttpRequest, None]` | Request object to provide to the view. | `None` | + | request | `HttpRequest | None` | Request object to provide to the view. | `None` | | args | `Iterable` | The positional arguments to pass to the view. | `tuple` | - | kwargs | `Union[Dict, None]` | The keyword arguments to pass to the view. | `None` | + | kwargs | `Dict | None` | The keyword arguments to pass to the view. | `None` | **Returns** diff --git a/src/django_idom/components.py b/src/django_idom/components.py index b6a5874f..1c02a35b 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import os from inspect import iscoroutinefunction -from typing import Any, Callable, Dict, Iterable, Union +from typing import Any, Callable, Dict, Iterable from channels.db import database_sync_to_async from django.contrib.staticfiles.finders import find @@ -18,14 +20,14 @@ # Form events will probably be accomplished through the upcoming DjangoForm. @component def view_to_component( - view: Union[Callable, View], + view: Callable | View, compatibility: bool = False, transforms: Iterable[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, - request: Union[HttpRequest, None] = None, + request: HttpRequest | None = None, args: Iterable = (), - kwargs: Union[Dict, None] = None, -) -> Union[VdomDict, None]: + kwargs: Dict | None = None, +) -> VdomDict | None : """Converts a Django view to an IDOM component. Args: diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py index 0659938a..038e070d 100644 --- a/src/django_idom/decorators.py +++ b/src/django_idom/decorators.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from functools import wraps -from typing import Callable, Union +from typing import Callable from idom.core.types import ComponentType, VdomDict @@ -7,9 +9,9 @@ def auth_required( - component: Union[Callable, None] = None, + component: Callable | None = None, auth_attribute: str = "is_active", - fallback: Union[ComponentType, VdomDict, None] = None, + fallback: ComponentType | VdomDict | None = None, ) -> Callable: """If the user passes authentication criteria, the decorated component will be rendered. Otherwise, the fallback component will be rendered. diff --git a/src/django_idom/types.py b/src/django_idom/types.py index 9696284a..1f4bd406 100644 --- a/src/django_idom/types.py +++ b/src/django_idom/types.py @@ -48,6 +48,6 @@ class Mutation(Generic[_Params]): @dataclass class ViewComponentIframe: - view: Union[View, Callable] + view: View | Callable args: Iterable kwargs: dict From 76348c4d8f61b1fe2cfdbe65af08c95769eeaf35 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:34:32 -0700 Subject: [PATCH 091/110] sourcery refactoring --- src/django_idom/decorators.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/django_idom/decorators.py b/src/django_idom/decorators.py index 038e070d..5800d220 100644 --- a/src/django_idom/decorators.py +++ b/src/django_idom/decorators.py @@ -31,16 +31,9 @@ def _wrapped_func(*args, **kwargs): if getattr(websocket.scope["user"], auth_attribute): return component(*args, **kwargs) - - if callable(fallback): - return fallback(*args, **kwargs) - return fallback + return fallback(*args, **kwargs) if callable(fallback) else fallback return _wrapped_func - # Return for @authenticated(...) - if component is None: - return decorator - - # Return for @authenticated - return decorator(component) + # Return for @authenticated(...) and @authenticated respectively + return decorator if component is None else decorator(component) From fe4c5971905e71c401f80d13f42b25a1d6a1f9ab Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 18:34:53 -0700 Subject: [PATCH 092/110] formatting --- src/django_idom/components.py | 2 +- src/django_idom/templates/idom/component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 1c02a35b..a3682b7e 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -27,7 +27,7 @@ def view_to_component( request: HttpRequest | None = None, args: Iterable = (), kwargs: Dict | None = None, -) -> VdomDict | None : +) -> VdomDict | None: """Converts a Django view to an IDOM component. Args: diff --git a/src/django_idom/templates/idom/component.html b/src/django_idom/templates/idom/component.html index fc8ba6f8..501793b1 100644 --- a/src/django_idom/templates/idom/component.html +++ b/src/django_idom/templates/idom/component.html @@ -11,4 +11,4 @@ "{{ idom_component_id }}", "{{ idom_component_params }}" ); - + \ No newline at end of file From 649e7b70594eea68e0fdd794bbe76557e9f02de5 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 21:43:55 -0700 Subject: [PATCH 093/110] reduce LOC changes --- tests/test_app/components.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index bfc547ad..58de6b15 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -141,6 +141,27 @@ def authorized_user(): ) +def get_items_query(): + return TodoItem.objects.all() + + +def add_item_mutation(text: str): + existing = TodoItem.objects.filter(text=text).first() + if existing: + if existing.done: + existing.done = False + existing.save() + else: + return False + else: + TodoItem(text=text, done=False).save() + + +def toggle_item_mutation(item: TodoItem): + item.done = not item.done + item.save() + + @component def todo_list(): input_value, set_input_value = hooks.use_state("") @@ -214,27 +235,6 @@ def _render_items(items, toggle_item): ) -def get_items_query(): - return TodoItem.objects.all() - - -def add_item_mutation(text: str): - existing = TodoItem.objects.filter(text=text).first() - if existing: - if existing.done: - existing.done = False - existing.save() - else: - return False - else: - TodoItem(text=text, done=False).save() - - -def toggle_item_mutation(item: TodoItem): - item.done = not item.done - item.save() - - @component def view_to_component_sync_func(): return view_to_component(views.view_to_component_sync_func) From 80cbcb90af7e73c091f0b7bf409c6f34e8a5f234 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 21:46:10 -0700 Subject: [PATCH 094/110] add changelog entry for bumping IDOM version --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 904b27c2..e2d821bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ Using the following categories, list your changes in this order: - `use_mutation` hook for modifying database values. - `view_to_component` utility to convert legacy Django views to IDOM components. +### Changed + +- Bumped the minimum IDOM version to 0.40.2 + ## [1.1.0] - 2022-07-01 ### Added From 5a4a5f4994ada9ab9870eec3fa0ff733e5b93ee1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 23:08:36 -0700 Subject: [PATCH 095/110] address review comments on `view_to_component` --- src/django_idom/components.py | 76 ++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index a3682b7e..c17fcb67 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -45,51 +45,27 @@ def view_to_component( kwargs: The keyword arguments to pass to the view. """ kwargs = kwargs or {} - - # Return the view if it's been rendered via the `async_renderer` rendered_view, set_rendered_view = hooks.use_state(None) - if rendered_view: - return utils.html_to_vdom( - rendered_view.content.decode("utf-8").strip(), - *transforms, - strict=strict_parsing, - ) - - # Create a synthetic request object. - request_obj = request - if not request: - request_obj = HttpRequest() - request_obj.method = "GET" - - # Render Check 1: Compatibility mode - if compatibility: - dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore - dotted_path = dotted_path.replace("<", "").replace(">", "") - - # Register the iframe's URL if needed - IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( - view, args, kwargs - ) - - return html.iframe( - { - "src": reverse("idom:view_to_component", args=[dotted_path]), - "loading": "lazy", - } - ) # Asynchronous view rendering via hooks - @hooks.use_effect(dependencies=[rendered_view]) + @hooks.use_effect async def async_renderer(): """Render the view in an async hook to avoid blocking the main thread.""" - if rendered_view: + # Avoid re-rendering the view + if rendered_view or compatibility: return - # Render Check 2: Async function view + # Create a synthetic request object. + request_obj = request + if not request: + request_obj = HttpRequest() + request_obj.method = "GET" + + # Render Check 1: Async function view if iscoroutinefunction(view): render = await view(request_obj, *args, **kwargs) - # Render Check 3: Async class view + # Render Check 2: Async class view elif getattr(view, "view_is_async", False): async_cbv = view.as_view() async_view = await async_cbv(request_obj, *args, **kwargs) @@ -98,7 +74,7 @@ async def async_renderer(): else: render = async_view - # Render Check 4: Sync class view + # Render Check 3: Sync class view elif getattr(view, "as_view", None): async_cbv = database_sync_to_async(view.as_view()) async_view = await async_cbv(request_obj, *args, **kwargs) @@ -107,12 +83,38 @@ async def async_renderer(): else: render = async_view - # Render Check 5: Sync function view + # Render Check 4: Sync function view else: render = await database_sync_to_async(view)(request_obj, *args, **kwargs) set_rendered_view(render) + # Render Check 5: Compatibility mode + if compatibility: + dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore + dotted_path = dotted_path.replace("<", "").replace(">", "") + + # Register the iframe's URL if needed + IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( + view, args, kwargs + ) + + return html.iframe( + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } + ) + + # Return the view if it's been rendered via the `async_renderer` hook + if rendered_view: + return utils.html_to_vdom( + rendered_view.content.decode("utf-8").strip(), + *transforms, + strict=strict_parsing, + ) + + # No view has been rendered by the `async_renderer` hook yet return None From 6eb9ae7bb313ee9869de5e763c65fb1392e7cbec Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Wed, 14 Sep 2022 23:59:31 -0700 Subject: [PATCH 096/110] insert file newline --- src/django_idom/templates/idom/component.html | 2 +- tests/test_app/static/django-css-test.css | 2 +- tests/test_app/templates/base.html | 3 ++- tests/test_app/templates/view_to_component.html | 2 +- tests/test_app/templates/view_to_component_script.html | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/django_idom/templates/idom/component.html b/src/django_idom/templates/idom/component.html index 501793b1..fc8ba6f8 100644 --- a/src/django_idom/templates/idom/component.html +++ b/src/django_idom/templates/idom/component.html @@ -11,4 +11,4 @@ "{{ idom_component_id }}", "{{ idom_component_params }}" ); - \ No newline at end of file + diff --git a/tests/test_app/static/django-css-test.css b/tests/test_app/static/django-css-test.css index 41f98461..40266ebb 100644 --- a/tests/test_app/static/django-css-test.css +++ b/tests/test_app/static/django-css-test.css @@ -1,3 +1,3 @@ #django-css button { - color: rgb(0, 0, 255); + color: rgb(0, 0, 255); } diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 94e33bf2..181fa23c 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -16,6 +16,7 @@

IDOM Test Page

iframe { width: 100%; } +
{% component "test_app.components.hello_world" class="hello-world" %}
@@ -43,4 +44,4 @@

IDOM Test Page

{% component "test_app.components.view_to_component_template_view_class_compatibility" %}
- \ No newline at end of file + diff --git a/tests/test_app/templates/view_to_component.html b/tests/test_app/templates/view_to_component.html index e2de762a..1bedc70e 100644 --- a/tests/test_app/templates/view_to_component.html +++ b/tests/test_app/templates/view_to_component.html @@ -1,3 +1,3 @@
{{ test_name }}: {% firstof status "Success" %}
-
{% block bottom %}{% endblock %} \ No newline at end of file +
{% block bottom %}{% endblock %} diff --git a/tests/test_app/templates/view_to_component_script.html b/tests/test_app/templates/view_to_component_script.html index f63a0a56..a417007f 100644 --- a/tests/test_app/templates/view_to_component_script.html +++ b/tests/test_app/templates/view_to_component_script.html @@ -5,4 +5,4 @@ el.textContent = "view_to_component_script: Success"; el.dataset.success = "true"; -{% endblock %} \ No newline at end of file +{% endblock %} From 711b0fdb59da3ca9cdc80b86ce58984281f1d405 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:17:41 -0700 Subject: [PATCH 097/110] address more comments --- src/django_idom/components.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index c17fcb67..ddeda6a8 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -48,7 +48,7 @@ def view_to_component( rendered_view, set_rendered_view = hooks.use_state(None) # Asynchronous view rendering via hooks - @hooks.use_effect + @hooks.use_effect(dependencies=[request, args, kwargs]) async def async_renderer(): """Render the view in an async hook to avoid blocking the main thread.""" # Avoid re-rendering the view @@ -87,7 +87,14 @@ async def async_renderer(): else: render = await database_sync_to_async(view)(request_obj, *args, **kwargs) - set_rendered_view(render) + # Signal that the view has been rendered + set_rendered_view( + utils.html_to_vdom( + render.content.decode("utf-8").strip(), + *transforms, + strict=strict_parsing, + ) + ) # Render Check 5: Compatibility mode if compatibility: @@ -107,15 +114,7 @@ async def async_renderer(): ) # Return the view if it's been rendered via the `async_renderer` hook - if rendered_view: - return utils.html_to_vdom( - rendered_view.content.decode("utf-8").strip(), - *transforms, - strict=strict_parsing, - ) - - # No view has been rendered by the `async_renderer` hook yet - return None + return rendered_view or None @component From f336819533b7b595e8ae04d934ad4047f91933c6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:19:13 -0700 Subject: [PATCH 098/110] no need for `or None` --- src/django_idom/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index ddeda6a8..f8327439 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -114,7 +114,7 @@ async def async_renderer(): ) # Return the view if it's been rendered via the `async_renderer` hook - return rendered_view or None + return rendered_view @component From 453626299a9801d69047fd0d7662959db39cfb83 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:59:26 -0700 Subject: [PATCH 099/110] use serialized request/args/kwargs as async_renderer dependencies --- src/django_idom/components.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index f8327439..4fb3ce38 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import os from inspect import iscoroutinefunction from typing import Any, Callable, Dict, Iterable @@ -46,21 +47,24 @@ def view_to_component( """ kwargs = kwargs or {} rendered_view, set_rendered_view = hooks.use_state(None) - - # Asynchronous view rendering via hooks - @hooks.use_effect(dependencies=[request, args, kwargs]) + request_obj = request + if not request: + request_obj = HttpRequest() + request_obj.method = "GET" + + # Render the view render within a hook + @hooks.use_effect( + dependencies=[ + json.dumps(vars(request_obj), default=lambda _: None), + json.dumps([args, kwargs], default=lambda _: None), + ] + ) async def async_renderer(): """Render the view in an async hook to avoid blocking the main thread.""" # Avoid re-rendering the view if rendered_view or compatibility: return - # Create a synthetic request object. - request_obj = request - if not request: - request_obj = HttpRequest() - request_obj.method = "GET" - # Render Check 1: Async function view if iscoroutinefunction(view): render = await view(request_obj, *args, **kwargs) From 52cc4c82d50fe66b85110f926c0cd85fad4f961d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 15 Sep 2022 15:14:05 -0700 Subject: [PATCH 100/110] Makes a best effort to create a name for an object --- src/django_idom/components.py | 5 +++-- src/django_idom/utils.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 4fb3ce38..51e2bee2 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -15,6 +15,7 @@ from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES from django_idom.types import ViewComponentIframe +from django_idom.utils import _generate_obj_name # TODO: Might want to intercept href clicks and form submit events. @@ -55,8 +56,8 @@ def view_to_component( # Render the view render within a hook @hooks.use_effect( dependencies=[ - json.dumps(vars(request_obj), default=lambda _: None), - json.dumps([args, kwargs], default=lambda _: None), + json.dumps(vars(request_obj), default=lambda x: _generate_obj_name(x)), + json.dumps([args, kwargs], default=lambda x: _generate_obj_name(x)), ] ) async def async_renderer(): diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index d1dba264..634d14ed 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -6,7 +6,7 @@ import re from fnmatch import fnmatch from importlib import import_module -from typing import Callable +from typing import Any, Callable from django.template import engines from django.utils.encoding import smart_str @@ -133,3 +133,13 @@ def _register_components(self, components: set[str]) -> None: "\033[0m", component, ) + + +def _generate_obj_name(object: Any) -> str | None: + """Makes a best effort to create a name for an object.""" + if hasattr(object, "__module__"): + if hasattr(object, "__name__"): + return f"{object.__module__}.{object.__name__}" + if hasattr(object, "__class__"): + return f"{object.__module__}.{object.__class__.__name__}" + return None From f30f891076b03b7ea7f8a8a4d8ca60a1c2f439ca Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 15 Sep 2022 15:34:58 -0700 Subject: [PATCH 101/110] move compatibility render into async_renderer hook --- docs/src/features/components.md | 2 +- src/django_idom/components.py | 47 ++++++++++++++++----------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index c9708d9d..cc8d9bf4 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -30,7 +30,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible | compatibility | `bool` | If True, the component will be rendered in an iframe. Strict parsing does not apply to compatibility mode. | `False` | | transforms | `Iterable[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `tuple` | | strict_parsing | `bool` | If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `True` | - | request | `HttpRequest | None` | Request object to provide to the view. | `None` | + | request | `HttpRequest | None` | Request object to provide to the view. Custom request objects cannot be used in compatibility mode. | `None` | | args | `Iterable` | The positional arguments to pass to the view. | `tuple` | | kwargs | `Dict | None` | The keyword arguments to pass to the view. | `None` | diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 51e2bee2..8b080b0f 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -43,6 +43,7 @@ def view_to_component( strict_parsing: If True, an exception will be generated if the HTML does not perfectly adhere to HTML5. request: Request object to provide to the view. + Custom request objects cannot be used in compatibility mode. args: The positional arguments to pass to the view. kwargs: The keyword arguments to pass to the view. """ @@ -62,15 +63,30 @@ def view_to_component( ) async def async_renderer(): """Render the view in an async hook to avoid blocking the main thread.""" - # Avoid re-rendering the view - if rendered_view or compatibility: + # Render Check 1: Compatibility mode + if compatibility: + dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore + dotted_path = dotted_path.replace("<", "").replace(">", "") + IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( + view, args, kwargs + ) + + # Signal that the view has been rendered + set_rendered_view( + html.iframe( + { + "src": reverse("idom:view_to_component", args=[dotted_path]), + "loading": "lazy", + } + ) + ) return - # Render Check 1: Async function view - if iscoroutinefunction(view): + # Render Check 2: Async function view + elif iscoroutinefunction(view): render = await view(request_obj, *args, **kwargs) - # Render Check 2: Async class view + # Render Check 3: Async class view elif getattr(view, "view_is_async", False): async_cbv = view.as_view() async_view = await async_cbv(request_obj, *args, **kwargs) @@ -79,7 +95,7 @@ async def async_renderer(): else: render = async_view - # Render Check 3: Sync class view + # Render Check 4: Sync class view elif getattr(view, "as_view", None): async_cbv = database_sync_to_async(view.as_view()) async_view = await async_cbv(request_obj, *args, **kwargs) @@ -88,7 +104,7 @@ async def async_renderer(): else: render = async_view - # Render Check 4: Sync function view + # Render Check 5: Sync function view else: render = await database_sync_to_async(view)(request_obj, *args, **kwargs) @@ -101,23 +117,6 @@ async def async_renderer(): ) ) - # Render Check 5: Compatibility mode - if compatibility: - dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore - dotted_path = dotted_path.replace("<", "").replace(">", "") - - # Register the iframe's URL if needed - IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( - view, args, kwargs - ) - - return html.iframe( - { - "src": reverse("idom:view_to_component", args=[dotted_path]), - "loading": "lazy", - } - ) - # Return the view if it's been rendered via the `async_renderer` hook return rendered_view From 4a433265b830df6f95284268549f4beef6f5b330 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 15 Sep 2022 15:38:44 -0700 Subject: [PATCH 102/110] fix mypy warning --- src/django_idom/components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 8b080b0f..31aa1d40 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -65,7 +65,7 @@ async def async_renderer(): """Render the view in an async hook to avoid blocking the main thread.""" # Render Check 1: Compatibility mode if compatibility: - dotted_path = f"{view.__module__}.{view.__name__}" # type: ignore + dotted_path = f"{view.__module__}.{view.__name__}" dotted_path = dotted_path.replace("<", "").replace(">", "") IDOM_VIEW_COMPONENT_IFRAMES[dotted_path] = ViewComponentIframe( view, args, kwargs From 5f89a125f5a9fcef2611172d1490c32e2efeaa42 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 15 Sep 2022 15:45:54 -0700 Subject: [PATCH 103/110] variable renaming --- src/django_idom/components.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 31aa1d40..f966522f 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -88,21 +88,20 @@ async def async_renderer(): # Render Check 3: Async class view elif getattr(view, "view_is_async", False): - async_cbv = view.as_view() - async_view = await async_cbv(request_obj, *args, **kwargs) - if getattr(async_view, "render", None): - render = await async_view.render() - else: - render = async_view + view_or_template_view = await view.as_view()(request_obj, *args, **kwargs) + if getattr(view_or_template_view, "render", None): # TemplateView + render = await view_or_template_view.render() + else: # View + render = view_or_template_view # Render Check 4: Sync class view elif getattr(view, "as_view", None): async_cbv = database_sync_to_async(view.as_view()) - async_view = await async_cbv(request_obj, *args, **kwargs) - if getattr(async_view, "render", None): - render = await database_sync_to_async(async_view.render)() - else: - render = async_view + view_or_template_view = await async_cbv(request_obj, *args, **kwargs) + if getattr(view_or_template_view, "render", None): # TemplateView + render = await database_sync_to_async(view_or_template_view.render)() + else: # View + render = view_or_template_view # Render Check 5: Sync function view else: From 3b2205277129eaffba3cb8e10f22012933713dcc Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Sep 2022 01:11:46 -0700 Subject: [PATCH 104/110] move _generate_obj_name to components.py --- src/django_idom/components.py | 12 +++++++++++- src/django_idom/utils.py | 10 ---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index f966522f..9c5aae6c 100644 --- a/src/django_idom/components.py +++ b/src/django_idom/components.py @@ -15,7 +15,6 @@ from django_idom.config import IDOM_CACHE, IDOM_VIEW_COMPONENT_IFRAMES from django_idom.types import ViewComponentIframe -from django_idom.utils import _generate_obj_name # TODO: Might want to intercept href clicks and form submit events. @@ -164,3 +163,14 @@ def _cached_static_contents(static_path: str): ) return file_contents + + +def _generate_obj_name(object: Any) -> str | None: + """Makes a best effort to create a name for an object. + Useful for JSON serialization of Python objects.""" + if hasattr(object, "__module__"): + if hasattr(object, "__name__"): + return f"{object.__module__}.{object.__name__}" + if hasattr(object, "__class__"): + return f"{object.__module__}.{object.__class__.__name__}" + return None diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 634d14ed..e1f8389d 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -133,13 +133,3 @@ def _register_components(self, components: set[str]) -> None: "\033[0m", component, ) - - -def _generate_obj_name(object: Any) -> str | None: - """Makes a best effort to create a name for an object.""" - if hasattr(object, "__module__"): - if hasattr(object, "__name__"): - return f"{object.__module__}.{object.__name__}" - if hasattr(object, "__class__"): - return f"{object.__module__}.{object.__class__.__name__}" - return None From 83527d21939867763740f110580fd6a64ea1ea8c Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Sep 2022 03:21:13 -0700 Subject: [PATCH 105/110] formatting --- src/django_idom/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index e1f8389d..d1dba264 100644 --- a/src/django_idom/utils.py +++ b/src/django_idom/utils.py @@ -6,7 +6,7 @@ import re from fnmatch import fnmatch from importlib import import_module -from typing import Any, Callable +from typing import Callable from django.template import engines from django.utils.encoding import smart_str From 56d501ac697bcffb72ef0bcb8eea54735c8f7e80 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 16 Sep 2022 15:21:23 -0700 Subject: [PATCH 106/110] add tests for request, args, and kwargs --- tests/test_app/components.py | 40 +++++++++++++++++++++++++ tests/test_app/templates/base.html | 4 +++ tests/test_app/tests/test_components.py | 9 ++++++ tests/test_app/views.py | 35 ++++++++++++++++++++++ 4 files changed, 88 insertions(+) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 58de6b15..7a06d0fd 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -1,5 +1,7 @@ +import asyncio import inspect +from django.http import HttpRequest from idom import component, hooks, html, web from test_app.models import TodoItem @@ -318,3 +320,41 @@ def view_to_component_template_view_class_compatibility(): @component def view_to_component_script(): return view_to_component(views.view_to_component_script) + + +@component +def view_to_component_request(): + request, set_request = hooks.use_state(None) + + @hooks.use_effect + async def change_request(): + await asyncio.sleep(3) + post_request = HttpRequest() + post_request.method = "POST" + set_request(post_request) + + return view_to_component(views.view_to_component_request, request=request) + + +@component +def view_to_component_args(): + params, set_params = hooks.use_state("false") + + @hooks.use_effect + async def change_args(): + await asyncio.sleep(3) + set_params("") + + return view_to_component(views.view_to_component_args, args=[params]) + + +@component +def view_to_component_kwargs(): + params, set_params = hooks.use_state("false") + + @hooks.use_effect + async def change_kwargs(): + await asyncio.sleep(3) + set_params("") + + return view_to_component(views.view_to_component_kwargs, kwargs={"success": params}) diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index 181fa23c..bea9893a 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -15,6 +15,7 @@

IDOM Test Page

@@ -37,6 +38,9 @@

IDOM Test Page

{% component "test_app.components.view_to_component_async_class" %}
{% component "test_app.components.view_to_component_template_view_class" %}
{% component "test_app.components.view_to_component_script" %}
+
{% component "test_app.components.view_to_component_request" %}
+
{% component "test_app.components.view_to_component_args" %}
+
{% component "test_app.components.view_to_component_kwargs" %}
{% component "test_app.components.view_to_component_sync_func_compatibility" %}
{% component "test_app.components.view_to_component_async_func_compatibility" %}
{% component "test_app.components.view_to_component_sync_class_compatibility" %}
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index e2c372c6..d5baccaa 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -128,6 +128,15 @@ def test_view_to_component_template_view_class(self): def test_view_to_component_script(self): self.page.locator("#view_to_component_script[data-success=true]").wait_for() + def test_view_to_component_request(self): + self.page.locator("#view_to_component_request[data-success=true]").wait_for() + + def test_view_to_component_args(self): + self.page.locator("#view_to_component_args[data-success=true]").wait_for() + + def test_view_to_component_kwargs(self): + self.page.locator("#view_to_component_kwargs[data-success=true]").wait_for() + def test_view_to_component_sync_func_compatibility(self): self.page.frame_locator( "#view_to_component_sync_func_compatibility > iframe" diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 8e14eb69..cec726f5 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -101,3 +101,38 @@ def view_to_component_script(request): "status": "false", }, ) + + +def view_to_component_request(request): + if request.method == "POST": + return render( + request, + "view_to_component.html", + {"test_name": inspect.currentframe().f_code.co_name}, + ) + + return render( + request, + "view_to_component.html", + { + "test_name": inspect.currentframe().f_code.co_name, + "status": "false", + "success": "false", + }, + ) + + +def view_to_component_args(request, success): + return render( + request, + "view_to_component.html", + {"test_name": inspect.currentframe().f_code.co_name, "status": success}, + ) + + +def view_to_component_kwargs(request, success="false"): + return render( + request, + "view_to_component.html", + {"test_name": inspect.currentframe().f_code.co_name, "status": success}, + ) From 5d56bd42f97602707ed979ef7c09a70d9e57f4d4 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 19 Sep 2022 14:52:42 -0700 Subject: [PATCH 107/110] fix changelog grammer --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 491acfc7..471f872e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Using the following categories, list your changes in this order: ### Added -- `auth_required` decorator to prevent your components from rendered to unauthenticated users. +- `auth_required` decorator to prevent your components from rendering to unauthenticated users. - `use_query` hook for fetching database values. - `use_mutation` hook for modifying database values. - `view_to_component` utility to convert legacy Django views to IDOM components. From a2ac0254ce38d0d8d7dbe6607ba4b1421cccc860 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:26:25 -0700 Subject: [PATCH 108/110] deterministic buttons for some tests --- tests/test_app/components.py | 36 ++++++++++++------- .../test_app/templates/view_to_component.html | 1 + .../templates/view_to_component_script.html | 14 ++++++-- tests/test_app/tests/test_components.py | 14 +++++--- 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 7a06d0fd..38f0ac5f 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -326,35 +326,47 @@ def view_to_component_script(): def view_to_component_request(): request, set_request = hooks.use_state(None) - @hooks.use_effect - async def change_request(): - await asyncio.sleep(3) + def on_click(_): post_request = HttpRequest() post_request.method = "POST" set_request(post_request) - return view_to_component(views.view_to_component_request, request=request) + return html._( + html.button( + {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, + "Click me", + ), + view_to_component(views.view_to_component_request, request=request), + ) @component def view_to_component_args(): params, set_params = hooks.use_state("false") - @hooks.use_effect - async def change_args(): - await asyncio.sleep(3) + def on_click(_): set_params("") - return view_to_component(views.view_to_component_args, args=[params]) + return html._( + html.button( + {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, + "Click me", + ), + view_to_component(views.view_to_component_args, args=[params]), + ) @component def view_to_component_kwargs(): params, set_params = hooks.use_state("false") - @hooks.use_effect - async def change_kwargs(): - await asyncio.sleep(3) + def on_click(_): set_params("") - return view_to_component(views.view_to_component_kwargs, kwargs={"success": params}) + return html._( + html.button( + {"id": f"{inspect.currentframe().f_code.co_name}_btn", "onClick": on_click}, + "Click me", + ), + view_to_component(views.view_to_component_kwargs, kwargs={"success": params}), + ) diff --git a/tests/test_app/templates/view_to_component.html b/tests/test_app/templates/view_to_component.html index 1bedc70e..7dbe51de 100644 --- a/tests/test_app/templates/view_to_component.html +++ b/tests/test_app/templates/view_to_component.html @@ -1,3 +1,4 @@ +{% block top %}{% endblock %}
{{ test_name }}: {% firstof status "Success" %}

{% block bottom %}{% endblock %} diff --git a/tests/test_app/templates/view_to_component_script.html b/tests/test_app/templates/view_to_component_script.html index a417007f..c9c5d263 100644 --- a/tests/test_app/templates/view_to_component_script.html +++ b/tests/test_app/templates/view_to_component_script.html @@ -1,8 +1,16 @@ {% extends "view_to_component.html" %} + +{% block top %} + +{% endblock %} + {% block bottom %} {% endblock %} diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index d5baccaa..7d1b1b0f 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -125,17 +125,23 @@ def test_view_to_component_template_view_class(self): "#ViewToComponentTemplateViewClass[data-success=true]" ).wait_for() + def _click_btn_and_check_success(self, name): + self.page.locator(f"#{name}:not([data-success=true])").wait_for() + btn = self.page.locator(f"#{name}_btn").wait_for() + btn.click() + self.page.locator(f"#{name}[data-success=true]").wait_for() + def test_view_to_component_script(self): - self.page.locator("#view_to_component_script[data-success=true]").wait_for() + self._click_btn_and_check_success("view_to_component_script") def test_view_to_component_request(self): - self.page.locator("#view_to_component_request[data-success=true]").wait_for() + self._click_btn_and_check_success("view_to_component_request") def test_view_to_component_args(self): - self.page.locator("#view_to_component_args[data-success=true]").wait_for() + self._click_btn_and_check_success("view_to_component_args") def test_view_to_component_kwargs(self): - self.page.locator("#view_to_component_kwargs[data-success=true]").wait_for() + self._click_btn_and_check_success("view_to_component_kwargs") def test_view_to_component_sync_func_compatibility(self): self.page.frame_locator( From 85eb88887074454708aa166a64cbd8fe847918f9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:33:57 -0700 Subject: [PATCH 109/110] fix tests --- tests/test_app/tests/test_components.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 7d1b1b0f..cf6b153e 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -127,8 +127,7 @@ def test_view_to_component_template_view_class(self): def _click_btn_and_check_success(self, name): self.page.locator(f"#{name}:not([data-success=true])").wait_for() - btn = self.page.locator(f"#{name}_btn").wait_for() - btn.click() + self.page.wait_for_selector(f"#{name}_btn").click() self.page.locator(f"#{name}[data-success=true]").wait_for() def test_view_to_component_script(self): From ed104c4f3de2a6d3ac1d6889f2887447753c90a9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:37:42 -0700 Subject: [PATCH 110/110] remove unused import --- tests/test_app/components.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 38f0ac5f..c103e01c 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -1,4 +1,3 @@ -import asyncio import inspect from django.http import HttpRequest