From c59975d93f5cbb7be8fba7c67c086997ac52b457 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 01:59:34 -0700 Subject: [PATCH 01/23] Allow refetch to be none --- src/django_idom/hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 49b2f606..457e0bc3 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -97,7 +97,7 @@ def execute_query() -> None: def use_mutation( mutate: Callable[_Params, bool | None], - refetch: Callable[..., Any] | Sequence[Callable[..., Any]], + refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None, ) -> Mutation[_Params]: loading, set_loading = use_state(False) error, set_error = use_state(cast(Union[Exception, None], None)) @@ -117,6 +117,8 @@ def execute_mutation() -> None: set_loading(False) set_error(None) if should_refetch is not False: + if not refetch: + return for query in (refetch,) if callable(refetch) else refetch: for callback in _REFETCH_CALLBACKS.get(query) or (): callback() From 142d9a5e36eac7565bc6c71705a6a47e5bb32a2d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 02:00:03 -0700 Subject: [PATCH 02/23] docs on `use_mutation.reset()` --- docs/includes/examples.md | 13 +++- docs/src/features/hooks.md | 136 ++++++++++++++++++++++++------------- 2 files changed, 102 insertions(+), 47 deletions(-) diff --git a/docs/includes/examples.md b/docs/includes/examples.md index 623e60c3..3ba0f385 100644 --- a/docs/includes/examples.md +++ b/docs/includes/examples.md @@ -20,4 +20,15 @@ class HelloWorldView(View): return HttpResponse("Hello World!") ``` - \ No newline at end of file + + + + +```python +from django.db import models + +class TodoItem(models.Model): + text = models.CharField(max_length=255) +``` + + diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index 09c8daa9..256cba92 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -67,8 +67,11 @@ The `use_mutation` hook is used to modify Django ORM objects. @component def todo_list(): - item_mutation = use_mutation(add_item) + def submit_event(event): + if event["key"] == "Enter": + item_mutation.execute(text=event["target"]["value"]) + item_mutation = use_mutation(add_item) if item_mutation.loading: mutation_status = html.h2("Adding...") elif item_mutation.error: @@ -76,10 +79,6 @@ The `use_mutation` hook is used to modify Django ORM objects. else: mutation_status = "" - def submit_event(event): - if event["key"] == "Enter": - item_mutation.execute(text=event["target"]["value"]) - return html.div( html.label("Add an item:"), html.input({"type": "text", "onKeyDown": submit_event}), @@ -89,61 +88,106 @@ The `use_mutation` hook is used to modify Django ORM objects. === "models.py" - ```python - from django.db import models - - class TodoItem(models.Model): - text = models.CharField(max_length=255) - ``` + {% include-markdown "../../includes/examples.md" start="" end="" %} ??? question "Can `use_mutation` trigger a refetch of `use_query`?" Yes, `use_mutation` can queue a refetch of a `use_query` via the `refetch=...` argument. The example below is a merge of the `use_query` and `use_mutation` examples above with the addition of a `refetch` argument on `use_mutation`. - + Please note that any `use_query` hooks that use `get_items` will be refetched upon a successful mutation. - ```python title="components.py" - from example_project.my_app.models import TodoItem - from idom import component, html - from django_idom.hooks import use_mutation + === "components.py" - def get_items(): - return TodoItem.objects.all() + ```python + from example_project.my_app.models import TodoItem + from idom import component, html + from django_idom.hooks import use_mutation - def add_item(text: str): - TodoItem(text=text).save() + def get_items(): + return TodoItem.objects.all() - @component - def todo_list(): - item_query = use_query(get_items) - if item_query.loading: - rendered_items = html.h2("Loading...") - elif item_query.error: - rendered_items = html.h2("Error when loading!") - else: - rendered_items = html.ul(html.li(item, key=item) for item in item_query.data) + def add_item(text: str): + TodoItem(text=text).save() - item_mutation = use_mutation(add_item, refetch=get_items) - if item_mutation.loading: - mutation_status = html.h2("Adding...") - elif item_mutation.error: - mutation_status = html.h2("Error when adding!") - else: - mutation_status = "" + @component + def todo_list(): + def submit_event(event): + if event["key"] == "Enter": + item_mutation.execute(text=event["target"]["value"]) - def submit_event(event): - if event["key"] == "Enter": - item_mutation.execute(text=event["target"]["value"]) + item_query = use_query(get_items) + if item_query.loading: + rendered_items = html.h2("Loading...") + elif item_query.error: + rendered_items = html.h2("Error when loading!") + else: + rendered_items = html.ul(html.li(item, key=item) for item in item_query.data) - return html.div( - html.label("Add an item:"), - html.input({"type": "text", "onKeyDown": submit_event}), - mutation_status, - rendered_items, - ) - ``` + item_mutation = use_mutation(add_item, refetch=get_items) + if item_mutation.loading: + mutation_status = html.h2("Adding...") + elif item_mutation.error: + mutation_status = html.h2("Error when adding!") + else: + mutation_status = "" + + return html.div( + html.label("Add an item:"), + html.input({"type": "text", "onKeyDown": submit_event}), + mutation_status, + rendered_items, + ) + ``` + + === "models.py" + + {% include-markdown "../../includes/examples.md" start="" end="" %} + +??? question "Can I make a failed `use_mutation` try again?" + + Yes, a `use_mutation` can be re-performed by calling `reset()` on your `use_mutation` instance. + + For example, take a look at `reset_event` below. + + === "components.py" + + ```python + from example_project.my_app.models import TodoItem + from idom import component, html + from django_idom.hooks import use_mutation + + def add_item(text: str): + TodoItem(text=text).save() + + @component + def todo_list(): + def reset_event(event): + item_mutation.reset() + + def submit_event(event): + if event["key"] == "Enter": + item_mutation.execute(text=event["target"]["value"]) + + item_mutation = use_mutation(add_item) + if item_mutation.loading: + mutation_status = html.h2("Adding...") + elif item_mutation.error: + mutation_status = html.button({"onClick": reset_event}, "Error: Try again!") + else: + mutation_status = "" + + return html.div( + html.label("Add an item:"), + html.input({"type": "text", "onKeyDown": submit_event}), + mutation_status, + ) + ``` + + === "models.py" + + {% include-markdown "../../includes/examples.md" start="" end="" %} ??? question "Can I make ORM calls without hooks?" From 3a7b93c7b79660371c625c9dceea1bced5c97b40 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 02:20:59 -0700 Subject: [PATCH 03/23] `use_origin` hook --- docs/src/features/hooks.md | 28 ++++++++++++++++++++++------ src/django_idom/hooks.py | 15 ++++++++++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index 256cba92..143e161d 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -1,6 +1,8 @@ ???+ tip "Looking for more hooks?" - Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html#basic-hooks) on hooks! + Standard ReactJS hooks are contained within [`idom-team/idom`](https://github.com/idom-team/idom). Please note that `idom` is installed by default alongside `django-idom`. + + Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html#basic-hooks) to see these hooks! ## Use Query @@ -205,7 +207,7 @@ The `use_mutation` hook is used to modify Django ORM objects. ## Use Websocket -You can fetch the Django Channels websocket at any time by using `use_websocket`. +You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_websocket`. ```python title="components.py" from idom import component, html @@ -219,7 +221,7 @@ def my_component(): ## Use Scope -This is a shortcut that returns the Websocket's `scope`. +This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). ```python title="components.py" from idom import component, html @@ -235,11 +237,11 @@ def my_component(): ??? info "This hook's behavior will be changed in a future update" - This hook will be updated to return the browser's current URL. This change will come in alongside IDOM URL routing support. + This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support. - Check out [idom-team/idom#569](https://github.com/idom-team/idom/issues/569) for more information. + Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information. -This is a shortcut that returns the Websocket's `path`. +This is a shortcut that returns the Websocket's `path`. You can expect this hook to provide strings such as `/idom/my_path`. ```python title="components.py" from idom import component, html @@ -250,3 +252,17 @@ def my_component(): my_location = use_location() return html.div(my_location) ``` + +## Use Origin + +This is a shortcut that returns the Websocket's `origin`. You can expect this hook to provide strings such as `http://example.com`. + +```python title="components.py" +from idom import component, html +from django_idom.hooks import use_origin + +@component +def my_component(): + my_origin = use_origin() + return html.div(my_origin) +``` diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 457e0bc3..e6a6f578 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -24,13 +24,26 @@ def use_location() -> Location: - """Get the current route as a string""" + """Get the current route as a `Location` object""" # TODO: Use the browser's current page, rather than the WS route scope = use_scope() search = scope["query_string"].decode() return Location(scope["path"], f"?{search}" if search else "") +def use_origin() -> str: + """Get the current origin as a string""" + scope = use_scope() + return next( + ( + header[1].decode("utf-8") + for header in scope["headers"] + if header[0] == b"origin" + ), + "", + ) + + def use_scope() -> dict[str, Any]: """Get the current ASGI scope dictionary""" return use_websocket().scope From 9d11f598c24f40ac5d30da77e6a3e715c528b6b9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 02:34:42 -0700 Subject: [PATCH 04/23] fix contrib template name --- docs/src/contribute/{django-idom.md => code.md} | 0 mkdocs.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/src/contribute/{django-idom.md => code.md} (100%) diff --git a/docs/src/contribute/django-idom.md b/docs/src/contribute/code.md similarity index 100% rename from docs/src/contribute/django-idom.md rename to docs/src/contribute/code.md diff --git a/mkdocs.yml b/mkdocs.yml index 3d7287b9..c3c339ec 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,7 +15,7 @@ nav: - Template Tag: features/templatetag.md - Settings: features/settings.md - Contribute: - - Code: contribute/django-idom.md + - Code: contribute/code.md - Docs: contribute/docs.md - Running Tests: contribute/running-tests.md - Changelog: changelog/index.md From 5557a24b6e009bc8fb522caa5b21207a28edce83 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 02:38:00 -0700 Subject: [PATCH 05/23] random cleanup --- docs/src/features/hooks.md | 11 +++++------ docs/src/features/templatetag.md | 16 ++++++++++------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index 143e161d..ed93d1a8 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -235,12 +235,6 @@ def my_component(): ## Use Location -??? info "This hook's behavior will be changed in a future update" - - This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support. - - Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information. - This is a shortcut that returns the Websocket's `path`. You can expect this hook to provide strings such as `/idom/my_path`. ```python title="components.py" @@ -253,6 +247,11 @@ def my_component(): return html.div(my_location) ``` +??? info "This hook's behavior will be changed in a future update" + + This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support. + + Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information. ## Use Origin This is a shortcut that returns the Websocket's `origin`. You can expect this hook to provide strings such as `http://example.com`. diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md index 43910f4c..36046bc3 100644 --- a/docs/src/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -8,15 +8,11 @@ Integrated within Django IDOM, we bundle a template tag. Within this tag, you ca Our pre-processor relies on the template tag containing a string. - **Do not** use a Django context variable for the path string. Failure to follow this warning will result in a performance penalty and also jankiness when using the Django autoreloader. + **Do not** use a Django context variable for the path string. Failure to follow this warning will result in unexpected behavior. For example, **do not** do the following: - ```python title="views.py" - def example_view(): - context_vars = {"dont_do_this": "example_project.my_app.components.hello_world"} - return render(request, "my-template.html", context_vars) - ``` + === "my-template.html" ```jinja title="my-template.html" @@ -26,6 +22,14 @@ Integrated within Django IDOM, we bundle a template tag. Within this tag, you ca {% component "example_project.my_app.components.hello_world" recipient="World" %} ``` + === "views.py" + + ```python + def example_view(): + context_vars = {"dont_do_this": "example_project.my_app.components.hello_world"} + return render(request, "my-template.html", context_vars) + ``` + From 951b1eeb8c47669c6cab02e7a4512d949daa263b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 10:24:04 -0700 Subject: [PATCH 06/23] Explictly use `None` if origin is unavailable. --- src/django_idom/hooks.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index e6a6f578..174e1c55 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -31,17 +31,21 @@ def use_location() -> Location: return Location(scope["path"], f"?{search}" if search else "") -def use_origin() -> str: - """Get the current origin as a string""" +def use_origin() -> str | None: + """Get the current origin as a string. If the browser did not send an origin header, + this will be None.""" scope = use_scope() - return next( - ( - header[1].decode("utf-8") - for header in scope["headers"] - if header[0] == b"origin" - ), - "", - ) + try: + return next( + ( + header[1].decode("utf-8") + for header in scope["headers"] + if header[0] == b"origin" + ), + None, + ) + except Exception: + return None def use_scope() -> dict[str, Any]: From 15bf21f1efda1f311b138d6dfb6391af2066950e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 11:02:00 -0700 Subject: [PATCH 07/23] hook description cleanup --- docs/src/features/hooks.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index ed93d1a8..c5c02571 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -8,6 +8,8 @@ The `use_query` hook is used fetch Django ORM queries. +The function you provide into this hook must return either a `Model` or `QuerySet`. + === "components.py" ```python @@ -47,6 +49,12 @@ The `use_query` hook is used fetch Django ORM queries. This may be resolved in a future version of Django with a natively asynchronous ORM. +??? question "Why does the example `get_items` function return a `Model` or `QuerySet`?" + + This was a technical design decision to [based on Apollo](https://www.apollographql.com/docs/react/data/mutations/#usemutation-api), but ultimately helps avoid Django's `SynchronousOnlyOperation` exceptions. + + The `use_query` hook ensures the provided `Model` or `QuerySet` executes all [deferred](https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_deferred_fields)/[lazy queries](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) safely prior to reaching your components. + ??? question "What is an "ORM"?" A Python **Object Relational Mapper** is an API for your code to access a database. @@ -55,7 +63,9 @@ The `use_query` hook is used fetch Django ORM queries. ## Use Mutation -The `use_mutation` hook is used to modify Django ORM objects. +The `use_mutation` hook is used to create, update, or delete Django ORM objects. + +The function you provide into this hook will have no return value. === "components.py" @@ -235,7 +245,9 @@ def my_component(): ## Use Location -This is a shortcut that returns the Websocket's `path`. You can expect this hook to provide strings such as `/idom/my_path`. +This is a shortcut that returns the Websocket's `path`. + +You can expect this hook to provide strings such as `/idom/my_path`. ```python title="components.py" from idom import component, html @@ -254,7 +266,9 @@ def my_component(): Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information. ## Use Origin -This is a shortcut that returns the Websocket's `origin`. You can expect this hook to provide strings such as `http://example.com`. +This is a shortcut that returns the Websocket's `origin`. + +You can expect this hook to provide strings such as `http://example.com`. ```python title="components.py" from idom import component, html From 94c8311b815115dd002659871d5d6ccc26a46cf5 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:19:00 -0700 Subject: [PATCH 08/23] clean up code examples --- README.md | 4 +- docs/includes/examples.md | 6 +- docs/src/features/components.md | 66 +++++----- docs/src/features/decorators.md | 120 ++++++++++-------- docs/src/features/hooks.md | 83 ++++++------ docs/src/features/settings.md | 2 +- docs/src/features/templatetag.md | 58 +++++---- docs/src/getting-started/create-component.md | 4 +- .../getting-started/reference-component.md | 4 +- docs/src/getting-started/render-view.md | 28 ++-- docs/src/installation/index.md | 92 ++++++++------ docs/src/stylesheets/extra.css | 5 + 12 files changed, 263 insertions(+), 209 deletions(-) diff --git a/README.md b/README.md index dfee8134..1c7e4fbc 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ You'll need a file to define your [IDOM](https://github.com/idom-team/idom) comp -```python title="components.py" +```python linenums="1" from idom import component, html @component @@ -51,7 +51,7 @@ Additonally, you can pass in keyword arguments into your component function. For -```jinja title="my-template.html" +```jinja linenums="1" {% load idom %} diff --git a/docs/includes/examples.md b/docs/includes/examples.md index 3ba0f385..3ed273fd 100644 --- a/docs/includes/examples.md +++ b/docs/includes/examples.md @@ -1,6 +1,6 @@ -```python +```python linenums="1" from django.http import HttpResponse def hello_world_view(request, *args, **kwargs): @@ -11,7 +11,7 @@ def hello_world_view(request, *args, **kwargs): -```python +```python linenums="1" from django.http import HttpResponse from django.views import View @@ -24,7 +24,7 @@ class HelloWorldView(View): -```python +```python linenums="1" from django.db import models class TodoItem(models.Model): diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 8d95c062..54b038ff 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -4,7 +4,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible === "components.py" - ```python + ```python linenums="1" from idom import component, html from django_idom.components import view_to_component from .views import hello_world_view @@ -47,7 +47,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible === "components.py" - ```python + ```python linenums="1" from idom import component, html from django_idom.components import view_to_component from .views import HelloWorldView @@ -69,7 +69,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible === "components.py" - ```python + ```python linenums="1" from idom import component, html from django_idom.components import view_to_component from .views import hello_world_view @@ -99,7 +99,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible === "components.py" - ```python + ```python linenums="1" from idom import component, html from django_idom.components import view_to_component from .views import hello_world_view @@ -125,7 +125,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible === "components.py" - ```python + ```python linenums="1" from idom import component, html from django_idom.components import view_to_component from .views import hello_world_view @@ -153,7 +153,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible === "components.py" - ```python + ```python linenums="1" from idom import component, html from django_idom.components import view_to_component from .views import hello_world_view @@ -174,7 +174,7 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible === "views.py" - ```python + ```python linenums="1" from django.http import HttpResponse def hello_world_view(request, *args, **kwargs): @@ -185,17 +185,19 @@ Convert any Django view into a IDOM component by usng this decorator. Compatible 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 django_css +=== "components.py" + + ```python linenums="1" + from idom import component, html + from django_idom.components import django_css -@component -def my_component(): - return html.div( - django_css("css/buttons.css"), - html.button("My Button!"), - ) -``` + @component + def my_component(): + return html.div( + django_css("css/buttons.css"), + html.button("My Button!"), + ) + ``` ??? question "Should I put `django_css` at the top of my component?" @@ -207,7 +209,7 @@ def my_component(): Here's an example on what you should avoid doing for Django static files: - ```python + ```python linenums="1" from idom import component, html from django.templatetags.static import static @@ -225,7 +227,7 @@ def my_component(): For external CSS, substitute `django_css` with `html.link`. - ```python + ```python linenums="1" from idom import component, html @component @@ -246,17 +248,19 @@ def my_component(): 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 django_js +=== "components.py" + + ```python linenums="1" + from idom import component, html + from django_idom.components import django_js -@component -def my_component(): - return html.div( - html.button("My Button!"), - django_js("js/scripts.js"), - ) -``` + @component + def my_component(): + return html.div( + html.button("My Button!"), + django_js("js/scripts.js"), + ) + ``` ??? question "Should I put `django_js` at the bottom of my component?" @@ -268,7 +272,7 @@ def my_component(): Here's an example on what you should avoid doing for Django static files: - ```python + ```python linenums="1" from idom import component, html from django.templatetags.static import static @@ -286,7 +290,7 @@ def my_component(): For external JavaScript, substitute `django_js` with `html.script`. - ```python + ```python linenums="1" from idom import component, html @component diff --git a/docs/src/features/decorators.md b/docs/src/features/decorators.md index 29a32d96..994834ec 100644 --- a/docs/src/features/decorators.md +++ b/docs/src/features/decorators.md @@ -8,16 +8,18 @@ Common uses of this decorator are to hide components from [`AnonymousUser`](http This decorator can be used with or without parentheses. -```python title="components.py" -from django_idom.decorators import auth_required -from django_idom.hooks import use_websocket -from idom import component, html +=== "components.py" -@component -@auth_required -def my_component(): - return html.div("I am logged in!") -``` + ```python linenums="1" + from django_idom.decorators import auth_required + from django_idom.hooks import use_websocket + from idom import component, html + + @component + @auth_required + def my_component(): + return html.div("I am logged in!") + ``` ??? example "See Interface" @@ -40,50 +42,56 @@ def my_component(): You can use a component with the `fallback` argument, as seen below. - ```python title="components.py" - from django_idom.decorators import auth_required - from idom import component, html + === "components.py" - @component - def my_component_fallback(): - return html.div("I am NOT logged in!") + ```python linenums="1" + from django_idom.decorators import auth_required + from idom import component, html - @component - @auth_required(fallback=my_component_fallback) - def my_component(): - return html.div("I am logged in!") - ``` + @component + def my_component_fallback(): + return html.div("I am NOT logged in!") + + @component + @auth_required(fallback=my_component_fallback) + def my_component(): + return html.div("I am logged in!") + ``` ??? question "How do I render a simple `idom.html` snippet if authentication fails?" You can use a `idom.html` snippet with the `fallback` argument, as seen below. - ```python title="components.py" - from django_idom.decorators import auth_required - from django_idom.hooks import use_websocket - from idom import component, html + === "components.py" - @component - @auth_required(fallback=html.div("I am NOT logged in!")) - def my_component(): - return html.div("I am logged in!") - ``` + ```python linenums="1" + from django_idom.decorators import auth_required + from django_idom.hooks import use_websocket + from idom import component, html + + @component + @auth_required(fallback=html.div("I am NOT logged in!")) + def my_component(): + return html.div("I am logged in!") + ``` ??? question "How can I check if a user `is_staff`?" You can set the `auth_attribute` to `is_staff`, as seen blow. - ```python title="components.py" - from django_idom.decorators import auth_required - from django_idom.hooks import use_websocket - from idom import component, html + === "components.py" + ```python linenums="1" + from django_idom.decorators import auth_required + from django_idom.hooks import use_websocket + from idom import component, html - @component - @auth_required(auth_attribute="is_staff") - def my_component(): - return html.div("I am logged in!") - ``` + + @component + @auth_required(auth_attribute="is_staff") + def my_component(): + return html.div("I am logged in!") + ``` ??? question "How can I check for a custom attribute?" @@ -91,24 +99,28 @@ def my_component(): For example, if your user model has the field `is_really_cool` ... - ```python - from django.contrib.auth.models import AbstractBaseUser + === "models.py" - class CustomUserModel(AbstractBaseUser): - @property - def is_really_cool(self): - return True - ``` + ```python linenums="1" + from django.contrib.auth.models import AbstractBaseUser + + class CustomUserModel(AbstractBaseUser): + @property + def is_really_cool(self): + return True + ``` ... then you would do the following within your decorator: - ```python title="components.py" - from django_idom.decorators import auth_required - from django_idom.hooks import use_websocket - from idom import component, html + === "components.py" - @component - @auth_required(auth_attribute="is_really_cool") - def my_component(): - return html.div("I am logged in!") - ``` + ```python linenums="1" + from django_idom.decorators import auth_required + from django_idom.hooks import use_websocket + from idom import component, html + + @component + @auth_required(auth_attribute="is_really_cool") + def my_component(): + return html.div("I am logged in!") + ``` diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index c5c02571..31b3215c 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -12,7 +12,7 @@ The function you provide into this hook must return either a `Model` or `QuerySe === "components.py" - ```python + ```python linenums="1" from example_project.my_app.models import TodoItem from idom import component, html from django_idom.hooks import use_query @@ -36,7 +36,7 @@ The function you provide into this hook must return either a `Model` or `QuerySe === "models.py" - ```python + ```python linenums="1" from django.db import models class TodoItem(models.Model): @@ -69,7 +69,7 @@ The function you provide into this hook will have no return value. === "components.py" - ```python + ```python linenums="1" from example_project.my_app.models import TodoItem from idom import component, html from django_idom.hooks import use_mutation @@ -112,7 +112,7 @@ The function you provide into this hook will have no return value. === "components.py" - ```python + ```python linenums="1" from example_project.my_app.models import TodoItem from idom import component, html from django_idom.hooks import use_mutation @@ -165,7 +165,7 @@ The function you provide into this hook will have no return value. === "components.py" - ```python + ```python linenums="1" from example_project.my_app.models import TodoItem from idom import component, html from django_idom.hooks import use_mutation @@ -219,29 +219,33 @@ The function you provide into this hook will have no return value. You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_websocket`. -```python title="components.py" -from idom import component, html -from django_idom.hooks import use_websocket +=== "components.py" + + ```python linenums="1" + from idom import component, html + from django_idom.hooks import use_websocket -@component -def my_component(): - my_websocket = use_websocket() - return html.div(my_websocket) -``` + @component + def my_component(): + my_websocket = use_websocket() + return html.div(my_websocket) + ``` ## Use Scope This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). -```python title="components.py" -from idom import component, html -from django_idom.hooks import use_scope +=== "components.py" -@component -def my_component(): - my_scope = use_scope() - return html.div(my_scope) -``` + ```python linenums="1" + from idom import component, html + from django_idom.hooks import use_scope + + @component + def my_component(): + my_scope = use_scope() + return html.div(my_scope) + ``` ## Use Location @@ -249,33 +253,38 @@ This is a shortcut that returns the Websocket's `path`. You can expect this hook to provide strings such as `/idom/my_path`. -```python title="components.py" -from idom import component, html -from django_idom.hooks import use_location +=== "components.py" -@component -def my_component(): - my_location = use_location() - return html.div(my_location) -``` + ```python linenums="1" + from idom import component, html + from django_idom.hooks import use_location + + @component + def my_component(): + my_location = use_location() + return html.div(my_location) + ``` ??? info "This hook's behavior will be changed in a future update" This hook will be updated to return the browser's currently active path. This change will come in alongside IDOM URL routing support. Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information. + ## Use Origin This is a shortcut that returns the Websocket's `origin`. You can expect this hook to provide strings such as `http://example.com`. -```python title="components.py" -from idom import component, html -from django_idom.hooks import use_origin +=== "components.py" + + ```python linenums="1" + from idom import component, html + from django_idom.hooks import use_origin -@component -def my_component(): - my_origin = use_origin() - return html.div(my_origin) -``` + @component + def my_component(): + my_origin = use_origin() + return html.div(my_origin) + ``` diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md index b003ae3c..8663f59a 100644 --- a/docs/src/features/settings.md +++ b/docs/src/features/settings.md @@ -4,7 +4,7 @@ Here are the configurable variables that are available. -```python title="settings.py" +```python linenums="1" # If "idom" cache is not configured, then we'll use "default" instead CACHES = { "idom": {"BACKEND": ...}, diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md index 36046bc3..6e01ba1f 100644 --- a/docs/src/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -1,6 +1,8 @@ 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="" %} +=== "my-template.html" + + {% include-markdown "../../../README.md" start="" end="" %} @@ -14,17 +16,17 @@ Integrated within Django IDOM, we bundle a template tag. Within this tag, you ca === "my-template.html" - ```jinja title="my-template.html" - - {% component dont_do_this recipient="World" %} + ```jinja linenums="1" + + {% component dont_do_this recipient="World" %} - - {% component "example_project.my_app.components.hello_world" recipient="World" %} - ``` + + {% component "example_project.my_app.components.hello_world" recipient="World" %} + ``` === "views.py" - ```python + ```python linenums="1" def example_view(): context_vars = {"dont_do_this": "example_project.my_app.components.hello_world"} return render(request, "my-template.html", context_vars) @@ -40,11 +42,13 @@ Integrated within Django IDOM, we bundle a template tag. Within this tag, you ca - `class` allows you to apply a HTML class to the top-level component div. This is useful for styling purposes. - `key` allows you to force the component to use a [specific key value](https://idom-docs.herokuapp.com/docs/guides/understanding-idom/why-idom-needs-keys.html?highlight=key). You typically won't need to set this. - ```jinja title="my-template.html" - ... - {% component "example.components.my_component" class="my-html-class" key=123 %} - ... - ``` + === "my-template.html" + + ```jinja linenums="1" + ... + {% component "example.components.my_component" class="my-html-class" key=123 %} + ... + ``` @@ -53,17 +57,19 @@ Integrated within Django IDOM, we bundle a template tag. Within this tag, you ca You can add as many components to a webpage as needed by using the template tag multiple times. Retrofitting legacy sites to use IDOM will typically involve many components on one page. - ```jinja - {% load idom %} - - - - {% component "example_project.my_app.components.hello_world" recipient="World" %} - {% component "example_project.my_app_2.components.class_component" class="bold small-font" %} -
{% component "example_project.my_app_3.components.simple_component" %}
- - - ``` + === "my-template.html" + + ```jinja linenums="1" + {% load idom %} + + + + {% component "example_project.my_app.components.hello_world" recipient="World" %} + {% component "example_project.my_app_2.components.class_component" class="bold small-font" %} +
{% component "example_project.my_app_3.components.simple_component" %}
+ + + ``` But keep in mind, in scenarios where you are trying to create a Single Page Application (SPA) within Django, you will only have one central component within your `#!html ` tag. @@ -85,6 +91,8 @@ 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="" %} + === "my-template.html" + + {% include-markdown "../../../README.md" start="" end="" %} diff --git a/docs/src/getting-started/create-component.md b/docs/src/getting-started/create-component.md index 29281051..8481cf87 100644 --- a/docs/src/getting-started/create-component.md +++ b/docs/src/getting-started/create-component.md @@ -6,7 +6,9 @@ {% include-markdown "../../../README.md" start="" end="" %} -{% include-markdown "../../../README.md" start="" end="" %} +=== "components.py" + + {% include-markdown "../../../README.md" start="" end="" %} ??? question "What should I name my IDOM files and functions?" diff --git a/docs/src/getting-started/reference-component.md b/docs/src/getting-started/reference-component.md index db5c4a5e..f7c03ecb 100644 --- a/docs/src/getting-started/reference-component.md +++ b/docs/src/getting-started/reference-component.md @@ -6,7 +6,9 @@ {% include-markdown "../../../README.md" start="" end="" %} -{% include-markdown "../../../README.md" start="" end="" %} +=== "my-template.html" + + {% include-markdown "../../../README.md" start="" end="" %} {% include-markdown "../features/templatetag.md" start="" end="" %} diff --git a/docs/src/getting-started/render-view.md b/docs/src/getting-started/render-view.md index d264517b..b1055d9b 100644 --- a/docs/src/getting-started/render-view.md +++ b/docs/src/getting-started/render-view.md @@ -10,23 +10,27 @@ Within your **Django app**'s `views.py` file, you'll need to create a function t In this example, we will create a view that renders `my-template.html` (_from the previous step_). -```python title="views.py" -from django.shortcuts import render +=== "views.py" -def index(request): - return render(request, "my-template.html") -``` + ```python linenums="1" + from django.shortcuts import render + + def index(request): + return render(request, "my-template.html") + ``` We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view). -```python title="urls.py" -from django.urls import path -from example_project.my_app import views +=== "urls.py" + + ```python linenums="1" + from django.urls import path + from example_project.my_app import views -urlpatterns = [ - path("example/", views.index), -] -``` + urlpatterns = [ + path("example/", views.index), + ] + ``` Now, navigate to `http://127.0.0.1:8000/example/`. If you copy-pasted the component from the previous example, you will now see your component display "Hello World". diff --git a/docs/src/installation/index.md b/docs/src/installation/index.md index e0cdfc1e..61782278 100644 --- a/docs/src/installation/index.md +++ b/docs/src/installation/index.md @@ -16,12 +16,14 @@ You'll also need to modify a few files in your **Django project**... In your settings you'll need to add `django_idom` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS). -```python title="settings.py" -INSTALLED_APPS = [ - "django_idom", - ... -] -``` +=== "settings.py" + + ```python linenums="1" + INSTALLED_APPS = [ + "django_idom", + ... + ] + ``` ??? warning "Enable Django ASGI (Required)" @@ -31,13 +33,15 @@ INSTALLED_APPS = [ Read the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info. - ```python title="settings.py" - INSTALLED_APPS = [ - "channels", - ... - ] - ASGI_APPLICATION = "example_project.asgi.application" - ``` + === "settings.py" + + ```python linenums="1" + INSTALLED_APPS = [ + "channels", + ... + ] + ASGI_APPLICATION = "example_project.asgi.application" + ``` ??? note "Configure IDOM settings (Optional)" @@ -51,14 +55,16 @@ INSTALLED_APPS = [ Add IDOM HTTP paths to your `urlpatterns`. -```python title="urls.py" -from django.urls import include, path +=== "urls.py" -urlpatterns = [ - path("idom/", include("django_idom.http.urls")), - ... -] -``` + ```python linenums="1" + from django.urls import include, path + + urlpatterns = [ + path("idom/", include("django_idom.http.urls")), + ... + ] + ``` --- @@ -66,28 +72,30 @@ urlpatterns = [ Register IDOM's Websocket using `IDOM_WEBSOCKET_PATH`. -```python title="asgi.py" -import os -from django.core.asgi import get_asgi_application - -# Ensure DJANGO_SETTINGS_MODULE is set properly based on your project name! -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") -django_asgi_app = get_asgi_application() - -from channels.auth import AuthMiddlewareStack -from channels.routing import ProtocolTypeRouter, URLRouter -from channels.sessions import SessionMiddlewareStack -from django_idom import IDOM_WEBSOCKET_PATH - -application = ProtocolTypeRouter( - { - "http": django_asgi_app, - "websocket": SessionMiddlewareStack( - AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH])) - ), - } -) -``` +=== "asgi.py" + + ```python linenums="1" + import os + from django.core.asgi import get_asgi_application + + # Ensure DJANGO_SETTINGS_MODULE is set properly based on your project name! + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") + django_asgi_app = get_asgi_application() + + from channels.auth import AuthMiddlewareStack + from channels.routing import ProtocolTypeRouter, URLRouter + from channels.sessions import SessionMiddlewareStack + from django_idom import IDOM_WEBSOCKET_PATH + + application = ProtocolTypeRouter( + { + "http": django_asgi_app, + "websocket": SessionMiddlewareStack( + AuthMiddlewareStack(URLRouter([IDOM_WEBSOCKET_PATH])) + ), + } + ) + ``` ??? question "Where is my asgi.py?" diff --git a/docs/src/stylesheets/extra.css b/docs/src/stylesheets/extra.css index 6b30153f..79881677 100644 --- a/docs/src/stylesheets/extra.css +++ b/docs/src/stylesheets/extra.css @@ -10,3 +10,8 @@ .md-typeset :is(.admonition, details) { margin: 0.55em 0; } + +.md-typeset .tabbed-labels > label { + padding-top: 0; + padding-bottom: 0.35em; +} From a2dd3c645ff22117386b9eae5465aaa784e66af2 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:30:12 -0700 Subject: [PATCH 09/23] add logging to query and mutation hooks --- src/django_idom/hooks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 174e1c55..55c6d916 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import logging from typing import Any, Awaitable, Callable, DefaultDict, Sequence, Union, cast from channels.db import database_sync_to_async as _database_sync_to_async @@ -13,6 +14,7 @@ from django_idom.types import IdomWebsocket, Mutation, Query, _Params, _Result +_logger = logging.getLogger(__name__) database_sync_to_async = cast( Callable[..., Callable[..., Awaitable[Any]]], _database_sync_to_async, @@ -101,6 +103,7 @@ def execute_query() -> None: set_data(None) set_loading(False) set_error(e) + _logger.exception("Error executing query", exc_info=True, stack_info=True) return finally: set_should_execute(False) @@ -130,6 +133,9 @@ def execute_mutation() -> None: except Exception as e: set_loading(False) set_error(e) + _logger.exception( + "Error executing mutation", exc_info=True, stack_info=True + ) else: set_loading(False) set_error(None) From 606019add1d1c6a6377e9fb239b47b94b10cefb9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:33:43 -0700 Subject: [PATCH 10/23] add mutation/query name to logger --- src/django_idom/hooks.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 55c6d916..bba1ecb7 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -103,7 +103,9 @@ def execute_query() -> None: set_data(None) set_loading(False) set_error(e) - _logger.exception("Error executing query", exc_info=True, stack_info=True) + _logger.exception( + f"Error executing query: {query}", exc_info=True, stack_info=True + ) return finally: set_should_execute(False) @@ -134,7 +136,9 @@ def execute_mutation() -> None: set_loading(False) set_error(e) _logger.exception( - "Error executing mutation", exc_info=True, stack_info=True + f"Error executing mutation: {mutate}", + exc_info=True, + stack_info=True, ) else: set_loading(False) From ba9bebec4772c413f60d742843e224dedaaee37b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 15:37:54 -0700 Subject: [PATCH 11/23] try removing delay from typing --- tests/test_app/tests/test_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index cf6b153e..fee79d3f 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -97,7 +97,7 @@ def test_use_query_and_mutation(self): item_ids = list(range(5)) for i in item_ids: - todo_input.type(f"sample-{i}", delay=10) + todo_input.type(f"sample-{i}") todo_input.press("Enter") self.page.wait_for_selector(f"#todo-item-sample-{i}") self.page.wait_for_selector(f"#todo-item-sample-{i}-checkbox").click() From c8389972937d5f7d340078bea272122ecc078a66 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 15:43:36 -0700 Subject: [PATCH 12/23] increase click delay --- tests/test_app/tests/test_components.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index fee79d3f..ebaf9b10 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -6,6 +6,9 @@ from playwright.sync_api import TimeoutError, sync_playwright +CLICK_DELAY = 250 # Delay in miliseconds. Needed for GitHub Actions. + + class TestIdomCapabilities(ChannelsLiveServerTestCase): @classmethod def setUpClass(cls): @@ -97,8 +100,8 @@ def test_use_query_and_mutation(self): item_ids = list(range(5)) for i in item_ids: - todo_input.type(f"sample-{i}") - todo_input.press("Enter") + todo_input.type(f"sample-{i}", delay=CLICK_DELAY) + todo_input.press("Enter", delay=CLICK_DELAY) self.page.wait_for_selector(f"#todo-item-sample-{i}") self.page.wait_for_selector(f"#todo-item-sample-{i}-checkbox").click() self.assertRaises( From 1c5c9e66028b282b8abb7f8d07c2b9a180331140 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:49:22 -0700 Subject: [PATCH 13/23] summaries --- docs/src/features/components.md | 4 ++++ docs/src/features/decorators.md | 6 +++++- docs/src/features/hooks.md | 10 +++++++--- docs/src/features/settings.md | 6 ++++-- docs/src/features/templatetag.md | 6 +++++- docs/src/getting-started/learn-more.md | 2 +- 6 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index 54b038ff..bdbe6cf1 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -1,3 +1,7 @@ +???+ summary + + Prefabricated components can be used within your `components.py` to help simplify development. + ## 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/). diff --git a/docs/src/features/decorators.md b/docs/src/features/decorators.md index 994834ec..d0bcaa2a 100644 --- a/docs/src/features/decorators.md +++ b/docs/src/features/decorators.md @@ -1,10 +1,14 @@ +???+ summary + + Decorator utilities can be used within your `components.py` to help simplify development. + ## Auth Required You can limit access to a component to users with a specific `auth_attribute` by using this decorator. By default, this decorator checks if the user is logged in, and his/her account has not been deactivated. -Common uses of this decorator are to hide components from [`AnonymousUser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.AnonymousUser), or render a component only if the user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). +Common uses of this decorator are to only render a component if the user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). This decorator can be used with or without parentheses. diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index 31b3215c..4bc7970a 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -1,8 +1,12 @@ -???+ tip "Looking for more hooks?" +???+ summary - Standard ReactJS hooks are contained within [`idom-team/idom`](https://github.com/idom-team/idom). Please note that `idom` is installed by default alongside `django-idom`. + Prefabricated hooks can be used within your `components.py` to help simplify development. - Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html#basic-hooks) to see these hooks! +??? tip "Looking for standard ReactJS hooks?" + + Standard ReactJS hooks are contained within [`idom-team/idom`](https://github.com/idom-team/idom). Since `idom` is installed by default alongside `django-idom`, you can import them at any time. + + Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html#basic-hooks) to see what hooks are available! ## Use Query diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md index 8663f59a..3d2eb726 100644 --- a/docs/src/features/settings.md +++ b/docs/src/features/settings.md @@ -1,6 +1,8 @@ -Django IDOM uses your **Django project's** `settings.py` file to modify some behaviors of IDOM. +???+ summary -Here are the configurable variables that are available. + Django IDOM uses your **Django project's** `settings.py` file to modify the behavior of IDOM. + +## Primary `settings.py` Configuration diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md index 6e01ba1f..526f6c79 100644 --- a/docs/src/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -1,4 +1,8 @@ -Integrated within Django IDOM, we bundle a template tag. Within this tag, you can pass in keyword arguments directly into your component. +???+ summary + + Template tags can be used within your Django templates such as `my-template.html` to import IDOM features. + +## Component === "my-template.html" diff --git a/docs/src/getting-started/learn-more.md b/docs/src/getting-started/learn-more.md index 31449ea9..e9aa7419 100644 --- a/docs/src/getting-started/learn-more.md +++ b/docs/src/getting-started/learn-more.md @@ -8,4 +8,4 @@ Additionally, the vast majority of tutorials/guides you find for React can be ap | Learn More | | --- | -| [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 } | +| [Django-IDOM Advanced Usage](../features/components.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 50084c0f158eaa6fdf43e752603527f605be1fff Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 7 Oct 2022 02:00:01 -0700 Subject: [PATCH 14/23] add interface to all endpoints --- docs/src/features/components.md | 28 ++++++++++ docs/src/features/hooks.md | 79 ++++++++++++++++++++++++++++ docs/src/features/templatetag.md | 16 ++++++ src/django_idom/hooks.py | 26 +++++++-- src/django_idom/templatetags/idom.py | 18 ++++--- 5 files changed, 157 insertions(+), 10 deletions(-) diff --git a/docs/src/features/components.md b/docs/src/features/components.md index bdbe6cf1..5886fc23 100644 --- a/docs/src/features/components.md +++ b/docs/src/features/components.md @@ -203,6 +203,20 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. ) ``` +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | static_path | `str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | + + **Returns** + + | Type | Description | + | --- | --- | + | `Component` | An IDOM component. | + ??? question "Should I put `django_css` at the top of my component?" Yes, if the stylesheet contains styling for your component. @@ -266,6 +280,20 @@ Allows you to defer loading JavaScript until a component begins rendering. This ) ``` +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | static_path | `str` | The path to the static file. This path is identical to what you would use on a `static` template tag. | N/A | + + **Returns** + + | Type | Description | + | --- | --- | + | `Component` | An IDOM component. | + ??? question "Should I put `django_js` at the bottom of my component?" Yes, if your scripts are reliant on the contents of the component. diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index 4bc7970a..d896e5fb 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -47,6 +47,22 @@ The function you provide into this hook must return either a `Model` or `QuerySe text = models.CharField(max_length=255) ``` +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | query | `Callable[_Params, _Result | None]` | A callable that returns a Django `Model` or `QuerySet`. | N/A | + | *args | `_Params.args` | Positional arguments to pass into `query`. | N/A | + | **kwargs | `_Params.kwargs` | Keyword arguments to pass into `query`. | N/A | + + **Returns** + + | Type | Description | + | --- | --- | + | `Query[_Result | None]` | A dataclass containing `loading`/`error` states, your `data` (if the query has successfully executed), and a `refetch` callable that can be used to re-run the query. | + ??? question "Can I make ORM calls without hooks?" Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `SynchronousOnlyOperation` exception. @@ -106,6 +122,21 @@ The function you provide into this hook will have no return value. {% include-markdown "../../includes/examples.md" start="" end="" %} +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | mutate | `Callable[_Params, bool | None]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `False`, then your `refetch` function will not be used. | N/A | + | refetch | `Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A callable or sequence of callables that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. | `None` | + + **Returns** + + | Type | Description | + | --- | --- | + | `Mutation[_Params]` | A dataclass containing `loading`/`error` states, a `reset` callable that will set `loading`/`error` states to defaults, and a `execute` callable that will run the query. | + ??? question "Can `use_mutation` trigger a refetch of `use_query`?" Yes, `use_mutation` can queue a refetch of a `use_query` via the `refetch=...` argument. @@ -235,6 +266,18 @@ You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en return html.div(my_websocket) ``` +??? example "See Interface" + + **Parameters** + + `None` + + **Returns** + + | Type | Description | + | --- | --- | + | `IdomWebsocket` | The component's websocket. | + ## Use Scope This is a shortcut that returns the Websocket's [`scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). @@ -251,6 +294,18 @@ This is a shortcut that returns the Websocket's [`scope`](https://channels.readt return html.div(my_scope) ``` +??? example "See Interface" + + **Parameters** + + `None` + + **Returns** + + | Type | Description | + | --- | --- | + | `dict[str, Any]` | The websocket's `scope`. | + ## Use Location This is a shortcut that returns the Websocket's `path`. @@ -275,6 +330,18 @@ You can expect this hook to provide strings such as `/idom/my_path`. Check out [idom-team/idom-router#2](https://github.com/idom-team/idom-router/issues/2) for more information. +??? example "See Interface" + + **Parameters** + + `None` + + **Returns** + + | Type | Description | + | --- | --- | + | `Location` | A object containing the current URL's `pathname` and `search` query. | + ## Use Origin This is a shortcut that returns the Websocket's `origin`. @@ -292,3 +359,15 @@ You can expect this hook to provide strings such as `http://example.com`. my_origin = use_origin() return html.div(my_origin) ``` + +??? example "See Interface" + + **Parameters** + + `None` + + **Returns** + + | Type | Description | + | --- | --- | + | `str | None` | A string containing the browser's current origin, obtained from websocket headers (if available). | diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md index 526f6c79..3ad12dc3 100644 --- a/docs/src/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -55,6 +55,22 @@ ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | dotted_path | `str` | The dotted path to the component to render. | N/A | + | **kwargs | `Any` | The keyword arguments to pass to the component. | N/A | + + **Returns** + + | Type | Description | + | --- | --- | + | `Component` | An IDOM component. | + ??? question "Can I use multiple components on one page?" diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index bba1ecb7..07178e4a 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -68,6 +68,14 @@ def use_query( *args: _Params.args, **kwargs: _Params.kwargs, ) -> Query[_Result | None]: + """Hook to fetch a Django ORM query. + + Args: + query: A callable that returns a Django `Model` or `QuerySet`. + *args: Positional arguments to pass into `query`. + + Keyword Args: + **kwargs: Keyword arguments to pass into `query`.""" query_ref = use_ref(query) if query_ref.current is not query: raise ValueError(f"Query function changed from {query_ref.current} to {query}.") @@ -121,6 +129,17 @@ def use_mutation( mutate: Callable[_Params, bool | None], refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None, ) -> Mutation[_Params]: + """Hook to create, update, or delete Django ORM objects. + + Args: + mutate: A callable that performs Django ORM create, update, or delete + functionality. If this function returns `False`, then your `refetch` + function will not be used. + refetch: A callable or sequence of callables that will be called if the + mutation succeeds. This is useful for refetching data after a mutation + has been performed. + """ + loading, set_loading = use_state(False) error, set_error = use_state(cast(Union[Exception, None], None)) @@ -143,9 +162,10 @@ def execute_mutation() -> None: else: set_loading(False) set_error(None) - if should_refetch is not False: - if not refetch: - return + + # `refetch` will execute unless explicitly told not to + # or if `refetch` was not defined. + if should_refetch is not False and refetch: for query in (refetch,) if callable(refetch) else refetch: for callback in _REFETCH_CALLBACKS.get(query) or (): callback() diff --git a/src/django_idom/templatetags/idom.py b/src/django_idom/templatetags/idom.py index 5e6b7ced..b6bb6868 100644 --- a/src/django_idom/templatetags/idom.py +++ b/src/django_idom/templatetags/idom.py @@ -1,4 +1,5 @@ import json +from typing import Any from urllib.parse import urlencode from uuid import uuid4 @@ -14,13 +15,16 @@ @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. +def component(dotted_path: str, **kwargs: Any): + """This tag is used to embed an existing IDOM component into your HTML template. + + Args: + dotted_path: The dotted path to the component to render. - The first argument within this tag is your dotted path to the component function. + Keyword Args: + **kwargs: The keyword arguments to pass to the component. - Subsequent values are keyworded arguments are passed into your component:: + Example :: {% load idom %} @@ -30,7 +34,7 @@ def component(_component_id_, **kwargs): """ - _register_component(_component_id_) + _register_component(dotted_path) class_ = kwargs.pop("class", "") json_kwargs = json.dumps(kwargs, separators=(",", ":")) @@ -41,6 +45,6 @@ def component(_component_id_, **kwargs): "idom_web_modules_url": IDOM_WEB_MODULES_URL, "idom_ws_max_reconnect_timeout": IDOM_WS_MAX_RECONNECT_TIMEOUT, "idom_mount_uuid": uuid4().hex, - "idom_component_id": _component_id_, + "idom_component_id": dotted_path, "idom_component_params": urlencode({"kwargs": json_kwargs}), } From 2a07138c2f28a96b239558587dde4c0c335abeb2 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 7 Oct 2022 02:03:28 -0700 Subject: [PATCH 15/23] Add changelog entry --- CHANGELOG.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 894a1390..b91665a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,17 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (Yet) +### Added + +- `use_origin` hook to return the browser's `location.origin`. + +### Changed + +- `use_mutation` and `use_query` will now log any query failures. + +### Fixed + +- Allow `use_mutation` to have `refetch=None`, as the docs suggest is possible. ## [1.2.0] - 2022-09-19 From 3ec2436d7b5a9dd0398203aaaa959522c57be8f3 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 7 Oct 2022 02:19:19 -0700 Subject: [PATCH 16/23] cleaner logging message --- src/django_idom/components.py | 12 +----------- src/django_idom/hooks.py | 7 +++---- src/django_idom/utils.py | 13 ++++++++++++- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/django_idom/components.py b/src/django_idom/components.py index 34ce359c..0a91c5c0 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. @@ -163,14 +164,3 @@ 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/hooks.py b/src/django_idom/hooks.py index 07178e4a..2fdeb319 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -12,6 +12,7 @@ from idom.core.hooks import Context, create_context, use_context, use_effect, use_state from django_idom.types import IdomWebsocket, Mutation, Query, _Params, _Result +from django_idom.utils import _generate_obj_name _logger = logging.getLogger(__name__) @@ -112,7 +113,7 @@ def execute_query() -> None: set_loading(False) set_error(e) _logger.exception( - f"Error executing query: {query}", exc_info=True, stack_info=True + f"Failed to execute query: {_generate_obj_name(query) or query}" ) return finally: @@ -155,9 +156,7 @@ def execute_mutation() -> None: set_loading(False) set_error(e) _logger.exception( - f"Error executing mutation: {mutate}", - exc_info=True, - stack_info=True, + f"Failed to execute mutation: {_generate_obj_name(mutate) or mutate}" ) else: set_loading(False) diff --git a/src/django_idom/utils.py b/src/django_idom/utils.py index 2f6782ab..c6b308bb 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 @@ -144,3 +144,14 @@ 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. + 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 From b73e9e23e404b026d7f4ec1afc80b0684c0dbbef Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 7 Oct 2022 02:24:58 -0700 Subject: [PATCH 17/23] use_origin tests --- tests/test_app/components.py | 11 +++++++++++ tests/test_app/templates/base.html | 1 + tests/test_app/tests/test_components.py | 3 +++ 3 files changed, 15 insertions(+) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index c103e01c..fbcb7961 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -85,6 +85,17 @@ def use_location(): ) +@component +def use_origin(): + location = django_idom.hooks.use_origin() + success = bool(location) + return html.div( + {"id": "use-origin", "data-success": success}, + f"use_origin: {location}", + html.hr(), + ) + + @component def django_css(): return html.div( diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index bea9893a..8f610e96 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -27,6 +27,7 @@

IDOM Test Page

{% component "test_app.components.use_websocket" %}
{% component "test_app.components.use_scope" %}
{% component "test_app.components.use_location" %}
+
{% component "test_app.components.use_origin" %}
{% component "test_app.components.django_css" %}
{% component "test_app.components.django_js" %}
{% component "test_app.components.unauthorized_user" %}
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index ebaf9b10..923e3e93 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -65,6 +65,9 @@ def test_use_scope(self): def test_use_location(self): self.page.locator("#use-location[data-success=true]").wait_for() + def test_use_origin(self): + self.page.locator("#use-origin[data-success=true]").wait_for() + def test_static_css(self): self.assertEqual( self.page.wait_for_selector("#django-css button").evaluate( From 78ae414bea9daecab346b9da5b87029c60acf415 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:33:19 -0700 Subject: [PATCH 18/23] _fetch_lazy_fields --- src/django_idom/hooks.py | 23 ++++++++++++----------- tests/test_app/components.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 2fdeb319..e562c916 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -107,7 +107,7 @@ def execute_query() -> None: try: new_data = query(*args, **kwargs) - _fetch_deferred(new_data) + _fetch_lazy_fields(new_data) except Exception as e: set_data(None) set_loading(False) @@ -179,19 +179,20 @@ def reset() -> None: return Mutation(call, loading, error, reset) -def _fetch_deferred(data: Any) -> None: +def _fetch_lazy_fields(data: Any) -> None: + """Fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily.""" + + # `QuerySet`, which is effectively a list of `Model` instances # https://github.com/typeddjango/django-stubs/issues/704 if isinstance(data, QuerySet): # type: ignore[misc] for model in data: - _fetch_deferred_model_fields(model) + _fetch_lazy_fields(model) + + # `Model` instances elif isinstance(data, Model): - _fetch_deferred_model_fields(data) + for field in data._meta.fields: + getattr(data, field.name) + + # Unrecognized type else: raise ValueError(f"Expected a Model or QuerySet, got {data!r}") - - -def _fetch_deferred_model_fields(model: Any) -> None: - for field in model.get_deferred_fields(): - value = getattr(model, field) - if isinstance(value, Model): - _fetch_deferred_model_fields(value) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index fbcb7961..bc866604 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -178,7 +178,7 @@ def toggle_item_mutation(item: TodoItem): def todo_list(): input_value, set_input_value = hooks.use_state("") items = use_query(get_items_query) - toggle_item = use_mutation(toggle_item_mutation, refetch=get_items_query) + toggle_item = use_mutation(toggle_item_mutation) if items.error: rendered_items = html.h2(f"Error when loading - {items.error}") From 12bebeefd9436d068f8eb14703059448b334c95b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:48:06 -0700 Subject: [PATCH 19/23] fix variable name in tests --- tests/test_app/components.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index bc866604..40fbd7d6 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -87,11 +87,11 @@ def use_location(): @component def use_origin(): - location = django_idom.hooks.use_origin() - success = bool(location) + origin = django_idom.hooks.use_origin() + success = bool(origin) return html.div( {"id": "use-origin", "data-success": success}, - f"use_origin: {location}", + f"use_origin: {origin}", html.hr(), ) From 5fc28e191938b168f8fa690661b589d38563f35d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:51:34 -0700 Subject: [PATCH 20/23] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b91665a8..335c7cb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Using the following categories, list your changes in this order: ### Fixed - Allow `use_mutation` to have `refetch=None`, as the docs suggest is possible. +- `use_query` will now fetch all fields to prevent `SynchronousOnlyOperation` exceptions. ## [1.2.0] - 2022-09-19 From 4d5972b65c8cd1cb81c974a9e3a50d543b5adc30 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 10 Oct 2022 18:33:29 -0700 Subject: [PATCH 21/23] fetch -> prefetch --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 335c7cb8..20a85b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ Using the following categories, list your changes in this order: ### Fixed - Allow `use_mutation` to have `refetch=None`, as the docs suggest is possible. -- `use_query` will now fetch all fields to prevent `SynchronousOnlyOperation` exceptions. +- `use_query` will now prefetch all fields to prevent `SynchronousOnlyOperation` exceptions. ## [1.2.0] - 2022-09-19 From cd1cbd710e42c89940d4043e87d274e489aa7f2d Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Mon, 10 Oct 2022 18:43:19 -0700 Subject: [PATCH 22/23] docs cleanup after self review --- docs/src/features/decorators.md | 2 +- docs/src/features/settings.md | 30 ++++++++++++++++-------------- docs/src/features/templatetag.md | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/src/features/decorators.md b/docs/src/features/decorators.md index d0bcaa2a..8b28c08a 100644 --- a/docs/src/features/decorators.md +++ b/docs/src/features/decorators.md @@ -8,7 +8,7 @@ You can limit access to a component to users with a specific `auth_attribute` by By default, this decorator checks if the user is logged in, and his/her account has not been deactivated. -Common uses of this decorator are to only render a component if the user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). +This decorator is commonly used to selectively render a component only if a user [`is_staff`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_staff) or [`is_superuser`](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_superuser). This decorator can be used with or without parentheses. diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md index 3d2eb726..c6a7c020 100644 --- a/docs/src/features/settings.md +++ b/docs/src/features/settings.md @@ -2,25 +2,27 @@ Django IDOM uses your **Django project's** `settings.py` file to modify the behavior of IDOM. -## Primary `settings.py` Configuration +## Primary Configuration - +=== "settings.py" -```python linenums="1" -# If "idom" cache is not configured, then we'll use "default" instead -CACHES = { -"idom": {"BACKEND": ...}, -} + -# Maximum seconds between two reconnection attempts that would cause the client give up. -# 0 will disable reconnection. -IDOM_WS_MAX_RECONNECT_TIMEOUT = 604800 + ```python linenums="1" + # If "idom" cache is not configured, then we'll use "default" instead + CACHES = { + "idom": {"BACKEND": ...}, + } -# The URL for IDOM to serve websockets -IDOM_WEBSOCKET_URL = "idom/" -``` + # Maximum seconds between two reconnection attempts that would cause the client give up. + # 0 will disable reconnection. + IDOM_WS_MAX_RECONNECT_TIMEOUT = 604800 - + # The URL for IDOM to serve websockets + IDOM_WEBSOCKET_URL = "idom/" + ``` + + ??? question "Do I need to modify my settings?" diff --git a/docs/src/features/templatetag.md b/docs/src/features/templatetag.md index 3ad12dc3..3e7731d9 100644 --- a/docs/src/features/templatetag.md +++ b/docs/src/features/templatetag.md @@ -14,7 +14,7 @@ Our pre-processor relies on the template tag containing a string. - **Do not** use a Django context variable for the path string. Failure to follow this warning will result in unexpected behavior. + **Do not** use Django template/context variables for the component path. Failure to follow this warning will result in unexpected behavior. For example, **do not** do the following: From 328b5baeb3983d7b11072966b175af7c9352e232 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Thu, 13 Oct 2022 19:43:19 -0700 Subject: [PATCH 23/23] fix refetch docstring --- docs/src/features/hooks.md | 2 +- src/django_idom/hooks.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md index d896e5fb..e9e67e39 100644 --- a/docs/src/features/hooks.md +++ b/docs/src/features/hooks.md @@ -129,7 +129,7 @@ The function you provide into this hook will have no return value. | Name | Type | Description | Default | | --- | --- | --- | --- | | mutate | `Callable[_Params, bool | None]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `False`, then your `refetch` function will not be used. | N/A | - | refetch | `Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A callable or sequence of callables that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. | `None` | + | refetch | `Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A `query` function (used by the `use_query` hook) or a sequence of `query` functions that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. | `None` | **Returns** diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index e562c916..7e73df23 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -136,9 +136,9 @@ def use_mutation( mutate: A callable that performs Django ORM create, update, or delete functionality. If this function returns `False`, then your `refetch` function will not be used. - refetch: A callable or sequence of callables that will be called if the - mutation succeeds. This is useful for refetching data after a mutation - has been performed. + refetch: A `query` function (used by the `use_query` hook) or a sequence of `query` + functions that will be called if the mutation succeeds. This is useful for + refetching data after a mutation has been performed. """ loading, set_loading = use_state(False)