diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f29365..577f67f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,7 @@ Using the following categories, list your changes in this order: ### Changed - Bumped the minimum ReactPy version to `1.0.2`. -- Prettier websocket URLs for components that do not have sessions. +- Prettier WebSocket URLs for components that do not have sessions. - Template tag will now only validate `args`/`kwargs` if `settings.py:DEBUG` is enabled. - Bumped the minimum `@reactpy/client` version to `0.3.1` - Bumped the minimum Django version to `4.2`. @@ -79,7 +79,7 @@ Using the following categories, list your changes in this order: ### Changed -- ReactPy will now provide a warning if your HTTP URLs are not on the same prefix as your websockets. +- ReactPy will now provide a warning if your HTTP URLs are not on the same prefix as your WebSockets. - Cleaner logging output for auto-detected ReactPy root components. ### Deprecated @@ -91,14 +91,14 @@ Using the following categories, list your changes in this order: - Warning W007 (`REACTPY_WEBSOCKET_URL doesn't end with a slash`) has been removed. ReactPy now automatically handles slashes. - Warning W008 (`REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character`) has been removed. ReactPy now automatically handles this scenario. -- Error E009 (`channels is not in settings.py:INSTALLED_APPS`) has been removed. Newer versions of `channels` do not require installation via `INSTALLED_APPS` to receive an ASGI webserver. +- Error E009 (`channels is not in settings.py:INSTALLED_APPS`) has been removed. Newer versions of `channels` do not require installation via `INSTALLED_APPS` to receive an ASGI web server. ## [3.3.2] - 2023-08-13 ### Added -- ReactPy Websocket will now decode messages via `orjson` resulting in an ~6% overall performance improvement. -- Built-in `asyncio` event loops are now patched via `nest_asyncio`, resulting in an ~10% overall performance improvement. This has no performance impact if you are running your webserver with `uvloop`. +- ReactPy WebSocket will now decode messages via `orjson` resulting in an ~6% overall performance improvement. +- Built-in `asyncio` event loops are now patched via `nest_asyncio`, resulting in an ~10% overall performance improvement. This has no performance impact if you are running your web server with `uvloop`. ### Fixed diff --git a/README.md b/README.md index 8e05e247..8aebcd8b 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,6 @@ def hello_world(recipient: str): In your **Django app**'s HTML template, you can now embed your ReactPy component using the `component` template tag. Within this tag, you will need to type in the dotted path to the component. - - Additionally, you can pass in `args` and `kwargs` into your component function. After reading the code below, pay attention to how the function definition for `hello_world` (_from the previous example_) accepts a `recipient` argument. diff --git a/docs/includes/orm.md b/docs/includes/orm.md index 22151a74..fafb6226 100644 --- a/docs/includes/orm.md +++ b/docs/includes/orm.md @@ -1,13 +1,13 @@ -Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `SynchronousOnlyOperation` exception. +Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `#!python SynchronousOnlyOperation` exception. -These `SynchronousOnlyOperation` exceptions may be resolved in a future version of Django containing an asynchronous ORM. However, it is best practice to always perform ORM calls in the background via hooks. +These `#!python SynchronousOnlyOperation` exceptions may be resolved in a future version of Django containing an asynchronous ORM. However, it is best practice to always perform ORM calls in the background via hooks. -By default, automatic recursive fetching of `ManyToMany` or `ForeignKey` fields is enabled within the default `QueryOptions.postprocessor`. This is needed to prevent `SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components. +By default, automatic recursive fetching of `#!python ManyToMany` or `#!python ForeignKey` fields is enabled within the default `#!python QueryOptions.postprocessor`. This is needed to prevent `#!python SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components. diff --git a/docs/overrides/home-code-examples/add-interactivity-demo.html b/docs/overrides/home-code-examples/add-interactivity-demo.html new file mode 100644 index 00000000..d9e99579 --- /dev/null +++ b/docs/overrides/home-code-examples/add-interactivity-demo.html @@ -0,0 +1,165 @@ +
Video description
+Video description
+Video description
+Video description
+{{ config.site_description }}
+ +
+ ReactPy lets you build user interfaces out of individual pieces called components. Create your own ReactPy
+ components like thumbnail
, like_button
, and video
. Then combine
+ them into entire screens, pages, and apps.
+
+ Whether you work on your own or with thousands of other developers, using React feels the same. It is + designed to let you seamlessly combine components written by independent people, teams, and + organizations. +
+
+ ReactPy components are Python functions. Want to show some content conditionally? Use an
+ if
statement. Displaying a list? Try using
+ list comprehension.
+ Learning ReactPy is learning programming.
+
+ ReactPy components receive data and return what should appear on the screen. You can pass them new data in + response to an interaction, like when the user types into an input. ReactPy will then update the screen to + match the new data. +
++ You don't have to build your whole page in ReactPy. Add React to your existing HTML page, and render + interactive ReactPy components anywhere on it. +
++ ReactPy is a library. It lets you put components together, but it doesn't prescribe how to do routing and + data fetching. To build an entire app with ReactPy, we recommend a backend framework like + Django. +
+ + Get Started + +- -You will need to set up a Python environment to run the ReactPy-Django test suite. - -
- ---- - -## Running Tests - -This repository uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`. - -If you plan to run tests, you will need to install the following dependencies first: - -- [Python 3.9+](https://www.python.org/downloads/) -- [Git](https://git-scm.com/downloads) - -Once done, you should clone this repository: - -```bash linenums="0" -git clone https://github.com/reactive-python/reactpy-django.git -cd reactpy-django -pip install -e . -r requirements.txt --upgrade -``` - -## Full Test Suite - -By running the command below you can run the full test suite: - -```bash linenums="0" -nox -s test -``` - -Or, if you want to run the tests in the background: - -```bash linenums="0" -nox -s test -- --headless -``` - -## Django Tests - -If you want to only run our Django tests in your current environment, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py test -``` - -## Django Test Webserver - -If you want to manually run the Django test application, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py runserver -``` diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 2dbbc4e6..d3d2eb25 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -2,8 +2,8 @@ django sanic plotly nox -websocket -websockets +WebSocket +WebSockets changelog async pre @@ -16,7 +16,6 @@ refetched refetching html jupyter -webserver iframe keyworded stylesheet diff --git a/docs/src/features/hooks.md b/docs/src/features/hooks.md deleted file mode 100644 index 6c57fda3..00000000 --- a/docs/src/features/hooks.md +++ /dev/null @@ -1,328 +0,0 @@ -## Overview - -- -Prefabricated hooks can be used within your `components.py` to help simplify development. - -
- -!!! note - - Looking for standard React hooks? - - This package only contains Django specific hooks. Standard hooks can be found within [`reactive-python/reactpy`](https://reactpy.dev/docs/reference/hooks-api.html#basic-hooks). - ---- - -## Use Query - -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 - {% include "../../python/use-query.py" %} - ``` - -=== "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - | Name | Type | Description | Default | - | --- | --- | --- | --- | - | `options` | `QueryOptions | None` | An optional `QueryOptions` object that can modify how the query is executed. | None | - | `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]` | An object 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 "How can I provide arguments to my query function?" - - `*args` and `**kwargs` can be provided to your query function via `use_query` parameters. - - === "components.py" - - ```python - {% include "../../python/use-query-args.py" %} - ``` - -??? question "Why does `get_items` in the example return `TodoItem.objects.all()`?" - - This was a technical design decision to based on [Apollo's `useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), 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 "How can I use `QueryOptions` to customize fetching behavior?" - - **`thread_sensitive`** - - Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. - - This setting only applies to sync query functions, and will be ignored for async functions. - - === "components.py" - - ```python - {% include "../../python/use-query-thread-sensitive.py" %} - ``` - - --- - - **`postprocessor`** - - {% include-markdown "../../includes/orm.md" start="" end="" %} - - However, if you... - - 1. Want to use this hook to defer IO intensive tasks to be computed in the background - 2. Want to to utilize `use_query` with a different ORM - - ... then you can either set a custom `postprocessor`, or disable all postprocessing behavior by modifying the `QueryOptions.postprocessor` parameter. In the example below, we will set the `postprocessor` to `None` to disable postprocessing behavior. - - === "components.py" - - ```python - {% include "../../python/use-query-postprocessor-disable.py" %} - ``` - - If you wish to create a custom `postprocessor`, you will need to create a callable. - - The first argument of `postprocessor` must be the query `data`. All proceeding arguments - are optional `postprocessor_kwargs` (see below). This `postprocessor` must return - the modified `data`. - - === "components.py" - - ```python - {% include "../../python/use-query-postprocessor-change.py" %} - ``` - - --- - - **`postprocessor_kwargs`** - - {% include-markdown "../../includes/orm.md" start="" end="" %} - - However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second `use_query` hook. - - You can disable the prefetching behavior of the default `postprocessor` (located at `reactpy_django.utils.django_query_postprocessor`) via the `QueryOptions.postprocessor_kwargs` parameter. - - === "components.py" - - ```python - {% include "../../python/use-query-postprocessor-kwargs.py" %} - ``` - - _Note: In Django's ORM design, the field name to access foreign keys is [postfixed with `_set`](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_one/) by default._ - -??? question "Can I define async query functions?" - - Async functions are supported by `use_query`. You can use them in the same way as a sync query function. - - However, be mindful of Django async ORM restrictions. - - === "components.py" - - ```python - {% include "../../python/use-query-async.py" %} - ``` - -??? question "Can I make ORM calls without hooks?" - - {% include-markdown "../../includes/orm.md" start="" end="" %} - -## Use Mutation - -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" - - ```python - {% include "../../python/use-mutation.py" %} - ``` - -=== "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? 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 `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** - - | Type | Description | - | --- | --- | - | `Mutation[_Params]` | An object 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 "How can I provide arguments to my mutation function?" - - `*args` and `**kwargs` can be provided to your mutation function via `mutation.execute` parameters. - - === "components.py" - - ```python - {% include "../../python/use-mutation-args-kwargs.py" %} - ``` - -??? 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. - - === "components.py" - - ```python - {% include "../../python/use-mutation-query-refetch.py" %} - ``` - - === "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? 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 - {% include "../../python/use-mutation-reset.py" %} - ``` - - === "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? question "Can I make ORM calls without hooks?" - - {% include-markdown "../../includes/orm.md" start="" end="" %} - -## Use Connection - -You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_connection`. - -=== "components.py" - - ```python - {% include "../../python/use-connection.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - `None` - - **Returns** - - | Type | Description | - | --- | --- | - | `Connection` | 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). - -=== "components.py" - - ```python - {% include "../../python/use-scope.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - `None` - - **Returns** - - | Type | Description | - | --- | --- | - | `MutableMapping[str, Any]` | The websocket's `scope`. | - -## Use Location - -This is a shortcut that returns the Websocket's `path`. - -You can expect this hook to provide strings such as `/reactpy/my_path`. - -=== "components.py" - - ```python - {% include "../../python/use-location.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - `None` - - **Returns** - - | Type | Description | - | --- | --- | - | `Location` | A object containing the current URL's `pathname` and `search` query. | - -??? 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 ReactPy URL routing support. - - Check out [reactive-python/reactpy-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`. - -=== "components.py" - - ```python - {% include "../../python/use-origin.py" %} - ``` - -??? 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/settings.md b/docs/src/features/settings.md deleted file mode 100644 index 3917d766..00000000 --- a/docs/src/features/settings.md +++ /dev/null @@ -1,38 +0,0 @@ -## Overview - -- -Your **Django project's** `settings.py` can modify the behavior of ReactPy. - -
- -!!! note - - The default configuration of ReactPy is suitable for the vast majority of use cases. - - You should only consider changing settings when the necessity arises. - ---- - -## Primary Configuration - - - -These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy. - -| Setting | Default Value | Example Value(s) | Description | -| --- | --- | --- | --- | -| `REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache.- -Utility functions provide various miscellaneous functionality. These are typically not used, but are available for advanced use cases. - -
- ---- - -## Django Query Postprocessor - -This is the default postprocessor for the `use_query` hook. - -This postprocessor is designed to avoid Django's `SynchronousOnlyException` by recursively fetching all fields within a `Model` or `QuerySet` to prevent [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy). - -=== "components.py" - - ```python - {% include "../../python/django-query-postprocessor.py" %} - ``` - -=== "models.py" - - ```python - {% include "../../python/example/models.py" %} - ``` - -??? example "See Interface" - - **Parameters** - - | Name | Type | Description | Default | - | --- | --- | --- | --- | - | `data` | `QuerySet | Model` | The `Model` or `QuerySet` to recursively fetch fields from. | N/A | - | `many_to_many` | `bool` | Whether or not to recursively fetch `ManyToManyField` relationships. | `True` | - | `many_to_one` | `bool` | Whether or not to recursively fetch `ForeignKey` relationships. | `True` | - - **Returns** - - | Type | Description | - | --- | --- | - | `QuerySet | Model` | The `Model` or `QuerySet` with all fields fetched. | - -## Register Component - -The `register_component` function is used manually register a root component with ReactPy. - -You should always call `register_component` within a Django [`AppConfig.ready()` method](https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready) to retain compatibility with ASGI webserver workers. - -=== "apps.py" - - ```python - {% include "../../python/register-component.py" %} - ``` - -??? question "Do I need to register my components?" - - You typically will not need to use this function. - - For security reasons, ReactPy does not allow non-registered components to be root components. However, all components contained within Django templates are automatically considered root components. - - You only need to use this function if your host application does not contain any HTML templates that [reference](../features/template-tag.md#component) your components. - - A common scenario where this is needed is when you are modifying the [template tag `host = ...` argument](../features/template-tag.md#component) in order to configure a dedicated Django application as a rendering server for ReactPy. On this dedicated rendering server, you would need to manually register your components. diff --git a/docs/src/get-started/choose-django-app.md b/docs/src/get-started/choose-django-app.md deleted file mode 100644 index 1594baa5..00000000 --- a/docs/src/get-started/choose-django-app.md +++ /dev/null @@ -1,26 +0,0 @@ -## Overview - -- -Set up a **Django Project** with at least one app. - -
- -!!! note - - If you have reached this point, you should have already [installed ReactPy-Django](../get-started/installation.md) through the previous steps. - ---- - -## Deciding which Django App to use - -You will now need to pick at least one **Django app** to start using ReactPy-Django on. - -For the following examples, we will assume the following: - -1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app). -2. You have placed `my_app` directly into your **Django project** folder (`./example_project/my_app`). This is common for small projects. - -??? question "How do I organize my Django project for ReactPy?" - - ReactPy-Django has no project structure requirements. Organize everything as you wish, just like any **Django project**. diff --git a/docs/src/get-started/create-component.md b/docs/src/get-started/create-component.md deleted file mode 100644 index 1f94c308..00000000 --- a/docs/src/get-started/create-component.md +++ /dev/null @@ -1,40 +0,0 @@ -## Overview - -- -You can let ReactPy know what functions are components by using the `#!python @component` decorator. - -
- ---- - -## Declaring a function as a root component - -You will need a file to start creating ReactPy components. - -We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. - -Within this file, you can define your component functions and then add ReactPy's `#!python @component` decorator. - -=== "components.py" - - {% include-markdown "../../../README.md" start="" end="" %} - -??? question "What should I name my ReactPy files and functions?" - - You have full freedom in naming/placement of your files and functions. - - We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. - - Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](./use-template-tag.md)). So, at minimum your component path needs to be valid to Python's `importlib`. - -??? question "What does the decorator actually do?" - - While not all components need to be decorated, there are a few features this decorator adds to your components. - - 1. The ability to be used as a root component. - - The decorator is required for any component that you want to reference in your Django templates ([_see next step_](./use-template-tag.md)). - 2. The ability to use [hooks](../features/hooks.md). - - The decorator is required on any component where hooks are defined. - 3. Scoped failures. - - If a decorated component generates an exception, then only that one component will fail to render. diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md deleted file mode 100644 index be727947..00000000 --- a/docs/src/get-started/installation.md +++ /dev/null @@ -1,125 +0,0 @@ -## Overview - -- -[ReactPy-Django](https://github.com/reactive-python/reactpy-django) can be used to add used to add [ReactPy](https://github.com/reactive-python/reactpy) support to an existing **Django project**. Minimal configuration is required to get started. - -
- -!!! note - - These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which involves creating and installing at least one **Django app**. - - If do not have a **Django project**, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_. - ---- - -## Step 1: Install from PyPI - -```bash linenums="0" -pip install reactpy-django -``` - -## Step 2: Configure [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) - -In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS). - -=== "settings.py" - - ```python - {% include "../../python/configure-installed-apps.py" %} - ``` - -??? warning "Enable Django Channels ASGI (Required)" - - ReactPy-Django requires ASGI Websockets from [Django Channels](https://github.com/django/channels). - - If you have not enabled ASGI on your **Django project** yet, you will need to - - 1. Install `channels[daphne]` - 2. Add `daphne` to `INSTALLED_APPS` - 3. Set your `ASGI_APPLICATION` variable. - - === "settings.py" - - ```python - {% include "../../python/configure-channels.py" %} - ``` - - Consider reading the [Django Channels Docs](https://channels.readthedocs.io/en/stable/installation.html) for more info. - -??? note "Configure ReactPy settings (Optional)" - - {% include "../features/settings.md" start="" end="" %} - -## Step 3: Configure [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) - -Add ReactPy HTTP paths to your `urlpatterns`. - -=== "urls.py" - - ```python - {% include "../../python/configure-urls.py" %} - ``` - -## Step 4: Configure [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) - -Register ReactPy's Websocket using `REACTPY_WEBSOCKET_ROUTE`. - -=== "asgi.py" - - ```python - {% include "../../python/configure-asgi.py" %} - ``` - -??? note "Add `AuthMiddlewareStack` and `SessionMiddlewareStack` (Optional)" - - There are many situations where you need to access the Django `User` or `Session` objects within ReactPy components. For example, if you want to: - - 1. Access the `User` that is currently logged in - 2. Login or logout the current `User` - 3. Access Django's `Session` object - - In these situations will need to ensure you are using `AuthMiddlewareStack` and/or `SessionMiddlewareStack`. - - ```python linenums="0" - {% include "../../python/configure-asgi-middleware.py" start="# start" %} - ``` - -??? question "Where is my `asgi.py`?" - - If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html). - -## Step 5: Run database migrations - -Run Django's database migrations to initialize ReactPy-Django's database table. - -```bash linenums="0" -python manage.py migrate -``` - -## Step 6: Check your configuration - -Run Django's check command to verify if ReactPy was set up correctly. - -```bash linenums="0" -python manage.py check -``` - -## Step 7: Create your first component! - -The [following steps](./choose-django-app.md) will show you how to create your first ReactPy component. - -Prefer a quick summary? Read the **At a Glance** section below. - -!!! info "At a Glance" - - **`my_app/components.py`** - - {% include-markdown "../../../README.md" start="" end="" %} - - --- - - **`my_app/templates/my-template.html`** - - {% include-markdown "../../../README.md" start="" end="" %} diff --git a/docs/src/get-started/learn-more.md b/docs/src/get-started/learn-more.md deleted file mode 100644 index 3ee45968..00000000 --- a/docs/src/get-started/learn-more.md +++ /dev/null @@ -1,17 +0,0 @@ -# :confetti_ball: Congratulations :confetti_ball: - -- -If you followed the previous steps, you have now created a "Hello World" component using ReactPy-Django! - -
- -!!! info "Deep Dive" - - The docs you are reading only covers our Django integration. To learn more, check out one of the following links: - - - [ReactPy-Django Feature Reference](../features/components.md) - - [ReactPy Core Documentation](https://reactpy.dev/docs/guides/creating-interfaces/index.html) - - [Ask Questions on Discord](https://discord.gg/uNb5P4hA9X) - - Additionally, the vast majority of tutorials/guides you find for ReactJS can be applied to ReactPy. diff --git a/docs/src/get-started/register-view.md b/docs/src/get-started/register-view.md deleted file mode 100644 index 472d722b..00000000 --- a/docs/src/get-started/register-view.md +++ /dev/null @@ -1,41 +0,0 @@ -## Overview - -- -Render your template containing your ReactPy component using a Django view. - -
- -!!! Note - - We assume you have [created a Django View](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) before, but we have included a simple example below. - ---- - -## Creating a Django view and URL path - -Within your **Django app**'s `views.py` file, you will need to create a function to render the HTML template containing your ReactPy components. - -In this example, we will create a view that renders `my-template.html` ([_from the previous step_](./use-template-tag.md)). - -=== "views.py" - - ```python - {% include "../../python/example/views.py" %} - ``` - -We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view). - -=== "urls.py" - - ```python - {% include "../../python/example/urls.py" %} - ``` - -??? question "Which urls.py do I add my views to?" - - For simple **Django projects**, you can easily add all of your views directly into the **Django project's** `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file. - - Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**. - - Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/#include) to link it all together. diff --git a/docs/src/get-started/run-webserver.md b/docs/src/get-started/run-webserver.md deleted file mode 100644 index cb4f87f1..00000000 --- a/docs/src/get-started/run-webserver.md +++ /dev/null @@ -1,27 +0,0 @@ -## Overview - -- -Run a webserver to display your Django view. - -
- ---- - -## Viewing your component using a webserver - -To test your new Django view, run the following command to start up a development webserver. - -```bash linenums="0" -python manage.py runserver -``` - -Now you can navigate to your **Django project** URL that contains a ReactPy component, such as [`http://127.0.0.1:8000/example/`](http://127.0.0.1:8000/example/) ([_from the previous step_](./register-view.md)). - -If you copy-pasted our example component, you will now see your component display "Hello World". - -!!! warning "Pitfall" - - Do not use `manage.py runserver` for production. - - This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/). diff --git a/docs/src/get-started/use-template-tag.md b/docs/src/get-started/use-template-tag.md deleted file mode 100644 index 8ef210fd..00000000 --- a/docs/src/get-started/use-template-tag.md +++ /dev/null @@ -1,27 +0,0 @@ -## Overview - -- -Decide where the component will be displayed by using our template tag. - -
- ---- - -## Embedding a component in a template - -{% include-markdown "../../../README.md" start="" end="" %} - -Additionally, you can pass in `args` and `kwargs` into your component function. After reading the code below, pay attention to how the function definition for `hello_world` ([_from the previous step_](./create-component.md)) accepts a `recipient` argument. - -=== "my-template.html" - - {% include-markdown "../../../README.md" start="" end="" %} - -{% include-markdown "../features/template-tag.md" start="" end="" %} - -{% include-markdown "../features/template-tag.md" start="" end="" %} - -??? question "Where is my templates folder?" - - If you do not have a `templates` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). diff --git a/docs/src/index.md b/docs/src/index.md index 6b8b3aaa..384ec5b6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,17 +1,6 @@ --- +template: home.html hide: - navigation - toc --- - -{ align=left style=height:40px } - -# ReactPy Django - -{% include-markdown "../../README.md" start="" end="" %} - -{% include-markdown "../../README.md" start="" end="" %} - -## Resources - -{% include-markdown "../../README.md" start="" end="" %} diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md new file mode 100644 index 00000000..7d7c949f --- /dev/null +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -0,0 +1,128 @@ +## Overview + ++ +If you want to add some interactivity to your existing **Django project**, you don't have to rewrite it in ReactPy. Use [ReactPy-Django](https://github.com/reactive-python/reactpy-django) to add [ReactPy](https://github.com/reactive-python/reactpy) to your existing stack, and render interactive components anywhere. + +
+ +!!! note + + These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which involves creating and installing at least one **Django app**. + + If do not have a **Django project**, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_. + +--- + +## Step 1: Install from PyPI + +Run the following command to install [`reactpy-django`](https://pypi.org/project/reactpy-django/) in your Python environment. + +```bash linenums="0" +pip install reactpy-django +``` + +## Step 2: Configure `settings.py` + +Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) file. + +=== "settings.py" + + ```python + {% include "../../python/configure-installed-apps.py" %} + ``` + +??? warning "Enable ASGI and Django Channels (Required)" + + ReactPy-Django requires Django ASGI and [Django Channels](https://github.com/django/channels) WebSockets. + + If you have not enabled ASGI on your **Django project** yet, here is a summary of the [`django`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) and [`channels`](https://channels.readthedocs.io/en/stable/installation.html) installation docs: + + 1. Install `channels[daphne]` + 2. Add `#!python "daphne"` to `#!python INSTALLED_APPS`. + + ```python linenums="0" + {% include "../../python/configure-channels-installed-app.py" %} + ``` + + 3. Set your `#!python ASGI_APPLICATION` variable. + + ```python linenums="0" + {% include "../../python/configure-channels-asgi-app.py" %} + ``` + +??? note "Configure ReactPy settings (Optional)" + + {% include "../reference/settings.md" start="" end="" %} + +## Step 3: Configure `urls.py` + +Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) file. + +=== "urls.py" + + ```python + {% include "../../python/configure-urls.py" %} + ``` + +## Step 4: Configure `asgi.py` + +Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) file. + +=== "asgi.py" + + ```python + {% include "../../python/configure-asgi.py" %} + ``` + +??? note "Add `#!python AuthMiddlewareStack` and `#!python SessionMiddlewareStack` (Optional)" + + There are many situations where you need to access the Django `#!python User` or `#!python Session` objects within ReactPy components. For example, if you want to: + + 1. Access the `#!python User` that is currently logged in + 2. Login or logout the current `#!python User` + 3. Access Django's `#!python Session` object + + In these situations will need to ensure you are using `#!python AuthMiddlewareStack` and/or `#!python SessionMiddlewareStack`. + + ```python linenums="0" + {% include "../../python/configure-asgi-middleware.py" start="# start" %} + ``` + +??? question "Where is my `asgi.py`?" + + If you do not have an `asgi.py`, follow the [`channels` installation guide](https://channels.readthedocs.io/en/stable/installation.html). + +## Step 5: Run database migrations + +Run Django's [`migrate` command](https://docs.djangoproject.com/en/dev/topics/migrations/) to initialize ReactPy-Django's database table. + +```bash linenums="0" +python manage.py migrate +``` + +## Step 6: Check your configuration + +Run Django's [`check` command](https://docs.djangoproject.com/en/dev/ref/django-admin/#check) to verify if ReactPy was set up correctly. + +```bash linenums="0" +python manage.py check +``` + +## Step 7: Create your first component + +The [next step](./your-first-component.md) will show you how to create your first ReactPy component. + +Prefer a quick summary? Read the **At a Glance** section below. + +!!! info "At a Glance: Your First Component" + + **`my_app/components.py`** + + {% include-markdown "../../../README.md" start="" end="" %} + + --- + + **`my_app/templates/my-template.html`** + + {% include-markdown "../../../README.md" start="" end="" %} diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md new file mode 100644 index 00000000..e7ddcd65 --- /dev/null +++ b/docs/src/learn/your-first-component.md @@ -0,0 +1,131 @@ +## Overview + ++ +Components are one of the core concepts of ReactPy. They are the foundation upon which you build user interfaces (UI), which makes them the perfect place to start your journey! + +
+ +!!! note + + If you have reached this point, you should have already [installed ReactPy-Django](../learn/add-reactpy-to-a-django-project.md) through the previous steps. + +--- + +## Selecting a Django App + +You will now need to pick at least one **Django app** to start using ReactPy-Django on. + +For the following examples, we will assume the following: + +1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app). +2. You have placed `my_app` directly into your **Django project** folder (`./example_project/my_app`). This is common for small projects. + +??? question "How do I organize my Django project for ReactPy?" + + ReactPy-Django has no project structure requirements. Organize everything as you wish, just like any **Django project**. + +## Defining a component + +You will need a file to start creating ReactPy components. + +We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. + +Within this file, you can define your component functions using ReactPy's `#!python @component` decorator. + +=== "components.py" + + {% include-markdown "../../../README.md" start="" end="" %} + +??? question "What should I name my ReactPy files and functions?" + + You have full freedom in naming/placement of your files and functions. + + We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. + + Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`. + +??? question "What does the decorator actually do?" + + While not all components need to be decorated, there are a few features this decorator adds to your components. + + 1. The ability to be used as a root component. + - The decorator is required for any component that you want to reference in your Django templates ([_see next step_](#embedding-in-a-template)). + 2. The ability to use [hooks](../reference/hooks.md). + - The decorator is required on any component where hooks are defined. + 3. Scoped failures. + - If a decorated component generates an exception, then only that one component will fail to render. + +## Embedding in a template + +In your **Django app**'s HTML template, you can now embed your ReactPy component using the `#!jinja {% component %}` template tag. Within this tag, you will need to type in the dotted path to the component. + +Additionally, you can pass in `#!python args` and `#!python kwargs` into your component function. After reading the code below, pay attention to how the function definition for `#!python hello_world` ([_from the previous step_](#defining-a-component)) accepts a `#!python recipient` argument. + +=== "my-template.html" + + {% include-markdown "../../../README.md" start="" end="" %} + +{% include-markdown "../reference/template-tag.md" start="" end="" %} + +{% include-markdown "../reference/template-tag.md" start="" end="" %} + +??? question "Where is my templates folder?" + + If you do not have a `./templates/` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). + +## Setting up a Django view + +Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my-template.html` ([_from the previous step_](#embedding-in-a-template)). + +=== "views.py" + + ```python + {% include "../../python/example/views.py" %} + ``` + +We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) and define what URL it should be accessible at. + +=== "urls.py" + + ```python + {% include "../../python/example/urls.py" %} + ``` + +??? question "Which urls.py do I add my views to?" + + For simple **Django projects**, you can easily add all of your views directly into the **Django project's** `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file. + + Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**. + + Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/#include) to link it all together. + +## Viewing your component + +To test your new Django view, run the following command to start up a development web server. + +```bash linenums="0" +python manage.py runserver +``` + +Now you can navigate to your **Django project** URL that contains a ReactPy component, such as [`http://127.0.0.1:8000/example/`](http://127.0.0.1:8000/example/) ([_from the previous step_](#setting-up-a-django-view)). + +If you copy-pasted our example component, you will now see your component display "Hello World". + +??? warning "Do not use `manage.py runserver` for production" + + This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/). + +## Learn more + +**Congratulations!** If you followed the previous steps, you have now created a "Hello World" component using ReactPy-Django! + +!!! info "Deep Dive" + + The docs you are reading only covers our Django integration. To learn more, check out one of the following links: + + - [ReactPy-Django Feature Reference](../reference/components.md) + - [ReactPy Core Documentation](https://reactpy.dev/docs/guides/creating-interfaces/index.html) + - [Ask Questions on Discord](https://discord.gg/uNb5P4hA9X) + + Additionally, the vast majority of tutorials/guides you find for ReactJS can be applied to ReactPy. diff --git a/docs/src/features/components.md b/docs/src/reference/components.md similarity index 51% rename from docs/src/features/components.md rename to docs/src/reference/components.md index f4dae25a..d3f235da 100644 --- a/docs/src/features/components.md +++ b/docs/src/reference/components.md @@ -24,24 +24,24 @@ Convert any Django view into a ReactPy component by using this decorator. Compat | Name | Type | Description | Default | | --- | --- | --- | --- | - | `view` | `Callable | View` | The view function or class to convert. | N/A | - | `compatibility` | `bool` | If True, the component will be rendered in an iframe. When using compatibility mode `tranforms`, `strict_parsing`, `request`, `args`, and `kwargs` arguments will be ignored. | `False` | - | `transforms` | `Sequence[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` | + | `#!python view` | `#!python Callable | View` | The view function or class to convert. | N/A | + | `#!python compatibility` | `#!python bool` | If `#!python True`, the component will be rendered in an iframe. When using compatibility mode `#!python tranforms`, `#!python strict_parsing`, `#!python request`, `#!python args`, and `#!python kwargs` arguments will be ignored. | `#!python False` | + | `#!python transforms` | `#!python Sequence[Callable[[VdomDict], Any]]` | A list of functions that transforms the newly generated VDOM. The functions will be called on each VDOM node. | `#!python tuple` | + | `#!python strict_parsing` | `#!python bool` | If `#!python True`, an exception will be generated if the HTML does not perfectly adhere to HTML5. | `#!python True` | **Returns** | Type | Description | | --- | --- | - | `_ViewComponentConstructor` | A function that takes `request, *args, key, **kwargs` and returns a ReactPy component. All parameters are directly provided to your view, besides `key` which is used by ReactPy. | + | `#!python _ViewComponentConstructor` | A function that takes `#!python request, *args, key, **kwargs` and returns a ReactPy component. All parameters are directly provided to your view, besides `#!python key` which is used by ReactPy. | -??? Warning "Potential information exposure when using `compatibility = True`" +??? Warning "Potential information exposure when using `#!python compatibility = True`" - When using `compatibility` mode, ReactPy automatically exposes a URL to your view. + When using `#!python compatibility` mode, ReactPy automatically exposes a URL to your view. It is your responsibility to ensure privileged information is not leaked via this method. - You must implement a method to ensure only authorized users can access your view. This can be done via directly writing conditionals into your view, or by adding decorators such as [`user_passes_test`](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views. For example... + You must implement a method to ensure only authorized users can access your view. This can be done via directly writing conditionals into your view, or by adding decorators such as [`#!python user_passes_test`](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) to your views. For example... === "Function Based View" @@ -57,17 +57,17 @@ Convert any Django view into a ReactPy component by using this decorator. Compat ??? info "Existing limitations" - There are currently several limitations of using `view_to_component` that may be resolved in a future version of `reactpy_django`. + There are currently several limitations of using `#!python view_to_component` that may be resolved in a future version. - Requires manual intervention to change request methods beyond `GET`. - ReactPy events cannot conveniently be attached to converted view HTML. - Has no option to automatically intercept local anchor link (such as `#!html `) click events. - _Please note these limitations do not exist when using `compatibility` mode._ + _Please note these limitations do not exist when using `#!python compatibility` mode._ ??? question "How do I use this for Class Based Views?" - You can simply pass your Class Based View directly into `view_to_component`. + You can simply pass your Class Based View directly into `#!python view_to_component`. === "components.py" @@ -77,7 +77,7 @@ Convert any Django view into a ReactPy component by using this decorator. Compat ??? question "How do I transform views from external libraries?" - In order to convert external views, you can utilize `view_to_component` as a function, rather than a decorator. + In order to convert external views, you can utilize `#!python view_to_component` as a function, rather than a decorator. === "components.py" @@ -85,11 +85,11 @@ Convert any Django view into a ReactPy component by using this decorator. Compat {% include "../../python/vtc-func.py" %} ``` -??? question "How do I provide `request`, `args`, and `kwargs` to a view?" +??? question "How do I provide `#!python request`, `#!python args`, and `#!python kwargs` to a view?" - **`Request`** + **`#!python Request`** - You can use the `request` parameter to provide the view a custom request object. + You can use the `#!python request` parameter to provide the view a custom request object. === "components.py" @@ -99,9 +99,9 @@ Convert any Django view into a ReactPy component by using this decorator. Compat --- - **`args` and `kwargs`** + **`#!python args` and `#!python kwargs`** - You can use the `args` and `kwargs` parameters to provide positional and keyworded arguments to a view. + You can use the `#!python args` and `#!python kwargs` parameters to provide positional and keyworded arguments to a view. === "components.py" @@ -109,15 +109,15 @@ Convert any Django view into a ReactPy component by using this decorator. Compat {% include "../../python/vtc-args-kwargs.py" %} ``` -??? question "How do I use `strict_parsing`, `compatibility`, and `transforms`?" +??? question "How do I use `#!python strict_parsing`, `#!python compatibility`, and `#!python transforms`?" - **`strict_parsing`** + **`#!python strict_parsing`** By default, an exception will be generated if your view's HTML does not perfectly adhere to HTML5. 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+ +Prefabricated hooks can be used within your `components.py` to help simplify development. + +
+ +!!! note + + Looking for standard React hooks? + + This package only contains Django specific hooks. Standard hooks can be found within [`reactive-python/reactpy`](https://reactpy.dev/docs/reference/hooks-api.html#basic-hooks). + +--- + +## Use Query + +This hook is used [read](https://www.sumologic.com/glossary/crud/) data from the Django ORM. + +The query function you provide must return either a `#!python Model` or `#!python QuerySet`. + +=== "components.py" + + ```python + {% include "../../python/use-query.py" %} + ``` + +=== "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python options` | `#!python QueryOptions | None` | An optional `#!python QueryOptions` object that can modify how the query is executed. | `#!python None` | + | `#!python query` | `#!python Callable[_Params, _Result | None]` | A callable that returns a Django `#!python Model` or `#!python QuerySet`. | N/A | + | `#!python *args` | `#!python _Params.args` | Positional arguments to pass into `#!python query`. | N/A | + | `#!python **kwargs` | `#!python _Params.kwargs` | Keyword arguments to pass into `#!python query`. | N/A | + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Query[_Result | None]` | An object containing `#!python loading`/`#!python error` states, your `#!python data` (if the query has successfully executed), and a `#!python refetch` callable that can be used to re-run the query. | + +??? question "How can I provide arguments to my query function?" + + `#!python *args` and `#!python **kwargs` can be provided to your query function via `#!python use_query` parameters. + + === "components.py" + + ```python + {% include "../../python/use-query-args.py" %} + ``` + +??? question "Why does `#!python get_items` in the example return `#!python TodoItem.objects.all()`?" + + This was a technical design decision to based on [Apollo's `#!javascript useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `#!python SynchronousOnlyOperation` exceptions. + + The `#!python use_query` hook ensures the provided `#!python Model` or `#!python 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 "How can I use `#!python QueryOptions` to customize fetching behavior?" + + **`#!python thread_sensitive`** + + Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`#!python sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. + + This setting only applies to sync query functions, and will be ignored for async functions. + + === "components.py" + + ```python + {% include "../../python/use-query-thread-sensitive.py" %} + ``` + + --- + + **`#!python postprocessor`** + + {% include-markdown "../../includes/orm.md" start="" end="" %} + + However, if you... + + 1. Want to use this hook to defer IO intensive tasks to be computed in the background + 2. Want to to utilize `#!python use_query` with a different ORM + + ... then you can either set a custom `#!python postprocessor`, or disable all postprocessing behavior by modifying the `#!python QueryOptions.postprocessor` parameter. In the example below, we will set the `#!python postprocessor` to `#!python None` to disable postprocessing behavior. + + === "components.py" + + ```python + {% include "../../python/use-query-postprocessor-disable.py" %} + ``` + + If you wish to create a custom `#!python postprocessor`, you will need to create a callable. + + The first argument of `#!python postprocessor` must be the query `#!python data`. All proceeding arguments + are optional `#!python postprocessor_kwargs` (see below). This `#!python postprocessor` must return + the modified `#!python data`. + + === "components.py" + + ```python + {% include "../../python/use-query-postprocessor-change.py" %} + ``` + + --- + + **`#!python postprocessor_kwargs`** + + {% include-markdown "../../includes/orm.md" start="" end="" %} + + However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second `#!python use_query` hook. + + You can disable the prefetching behavior of the default `#!python postprocessor` (located at `#!python reactpy_django.utils.django_query_postprocessor`) via the `#!python QueryOptions.postprocessor_kwargs` parameter. + + === "components.py" + + ```python + {% include "../../python/use-query-postprocessor-kwargs.py" %} + ``` + + _Note: In Django's ORM design, the field name to access foreign keys is [postfixed with `_set`](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_one/) by default._ + +??? question "Can I define async query functions?" + + Async functions are supported by `#!python use_query`. You can use them in the same way as a sync query function. + + However, be mindful of Django async ORM restrictions. + + === "components.py" + + ```python + {% include "../../python/use-query-async.py" %} + ``` + +??? question "Can I make ORM calls without hooks?" + + {% include-markdown "../../includes/orm.md" start="" end="" %} + +## Use Mutation + +This hook is used to [create, update, or delete](https://www.sumologic.com/glossary/crud/) Django ORM objects. + +The mutation function you provide should have no return value. + +=== "components.py" + + ```python + {% include "../../python/use-mutation.py" %} + ``` + +=== "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python mutate` | `#!python Callable[_Params, bool | None]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `#!python False`, then your `#!python refetch` function will not be used. | N/A | + | `#!python refetch` | `#!python Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A `#!python query` function (used by the `#!python use_query` hook) or a sequence of `#!python query` functions that will be called if the mutation succeeds. This is useful for refetching data after a mutation has been performed. | `#!python None` | + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Mutation[_Params]` | An object containing `#!python loading`/`#!python error` states, a `#!python reset` callable that will set `#!python loading`/`#!python error` states to defaults, and a `#!python execute` callable that will run the query. | + +??? question "How can I provide arguments to my mutation function?" + + `#!python *args` and `#!python **kwargs` can be provided to your mutation function via #!python mutation.execute` parameters. + + === "components.py" + + ```python + {% include "../../python/use-mutation-args-kwargs.py" %} + ``` + +??? question "Can `#!python use_mutation` trigger a refetch of `#!python use_query`?" + + Yes, `#!python use_mutation` can queue a refetch of a `#!python use_query` via the `#!python refetch=...` argument. + + The example below is a merge of the `#!python use_query` and `#!python use_mutation` examples above with the addition of a `#!python use_mutation(refetch=...)` argument. + + Please note that any `#!python use_query` hooks that use `#!python get_items` will be refetched upon a successful mutation. + + === "components.py" + + ```python + {% include "../../python/use-mutation-query-refetch.py" %} + ``` + + === "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? question "Can I make a failed `#!python use_mutation` try again?" + + Yes, a `#!python use_mutation` can be re-performed by calling `#!python reset()` on your `#!python use_mutation` instance. + + For example, take a look at `#!python reset_event` below. + + === "components.py" + + ```python + {% include "../../python/use-mutation-reset.py" %} + ``` + + === "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? question "Can I make ORM calls without hooks?" + + {% include-markdown "../../includes/orm.md" start="" end="" %} + +## Use Connection + +This hook is used to fetch the Django Channels [WebSocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer). + +=== "components.py" + + ```python + {% include "../../python/use-connection.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Connection` | The component's WebSocket. | + +## Use Scope + +This is a shortcut that returns the WebSocket's [`#!python scope`](https://channels.readthedocs.io/en/stable/topics/consumers.html#scope). + +=== "components.py" + + ```python + {% include "../../python/use-scope.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python MutableMapping[str, Any]` | The WebSocket's `#!python scope`. | + +## Use Location + +This is a shortcut that returns the WebSocket's `#!python path`. + +You can expect this hook to provide strings such as `/reactpy/my_path`. + +=== "components.py" + + ```python + {% include "../../python/use-location.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Location` | An object containing the current URL's `#!python pathname` and `#!python search` query. | + +??? info "This hook's behavior will be changed in a future update" + + This hook will be updated to return the browser's currently active HTTP path. This change will come in alongside ReactPy URL routing support. + + Check out [reactive-python/reactpy-django#147](https://github.com/reactive-python/reactpy-django/issues/147) for more information. + +## Use Origin + +This is a shortcut that returns the WebSocket's `#!python origin`. + +You can expect this hook to provide strings such as `http://example.com`. + +=== "components.py" + + ```python + {% include "../../python/use-origin.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + `#!python None` + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python str | None` | A string containing the browser's current origin, obtained from WebSocket headers (if available). | diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md new file mode 100644 index 00000000..9f108a0d --- /dev/null +++ b/docs/src/reference/settings.md @@ -0,0 +1,38 @@ +## Overview + ++ +Your **Django project's** `settings.py` can modify the behavior of ReactPy. + +
+ +!!! note + + The default configuration of ReactPy is suitable for the vast majority of use cases. + + You should only consider changing settings when the necessity arises. + +--- + +## Primary Configuration + + + +These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy. + +| Setting | Default Value | Example Value(s) | Description | +| --- | --- | --- | --- | +| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). | +| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database used to store ReactPy session data. ReactPy requires a multiprocessing-safe and thread-safe database. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:+ +Utility functions provide various miscellaneous functionality. These are typically not used, but are available for advanced use cases. + +
+ +--- + +## Django Query Postprocessor + +This is the default postprocessor for the `#!python use_query` hook. + +This postprocessor is designed to avoid Django's `#!python SynchronousOnlyException` by recursively fetching all fields within a `#!python Model` or `#!python QuerySet` to prevent [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy). + +=== "components.py" + + ```python + {% include "../../python/django-query-postprocessor.py" %} + ``` + +=== "models.py" + + ```python + {% include "../../python/example/models.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python data` | `#!python QuerySet | Model` | The `#!python Model` or `#!python QuerySet` to recursively fetch fields from. | N/A | + | `#!python many_to_many` | `#!python bool` | Whether or not to recursively fetch `#!python ManyToManyField` relationships. | `#!python True` | + | `#!python many_to_one` | `#!python bool` | Whether or not to recursively fetch `#!python ForeignKey` relationships. | `#!python True` | + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python QuerySet | Model` | The `#!python Model` or `#!python QuerySet` with all fields fetched. | + +## Register Component + +This function is used manually register a root component with ReactPy. + +=== "apps.py" + + ```python + {% include "../../python/register-component.py" %} + ``` + +??? warning "Only use this within `#!python AppConfig.ready()`" + + You should always call `#!python register_component` within a Django [`#!python AppConfig.ready()` method](https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. + +??? question "Do I need to use this?" + + You typically will not need to use this function. + + For security reasons, ReactPy does not allow non-registered components to be root components. However, all components contained within Django templates are automatically considered root components. + + This is typically only needed when you have a dedicated Django application as a rendering server that doesn't have templates, such as when modifying the [template tag `#!python host` argument](../reference/template-tag.md#component). On this dedicated rendering server, you would need to manually register your components. diff --git a/docs/src/static/css/extra.css b/docs/src/static/css/extra.css deleted file mode 100644 index d3967666..00000000 --- a/docs/src/static/css/extra.css +++ /dev/null @@ -1,376 +0,0 @@ -/* Variable overrides */ -:root { - --code-max-height: 17.25rem; -} - -[data-md-color-scheme="slate"] { - --md-code-hl-color: #ffffcf1c; - --md-hue: 225; - --md-default-bg-color: hsla(var(--md-hue), 15%, 16%, 1); - --md-default-bg-color--light: hsla(var(--md-hue), 15%, 16%, 0.54); - --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 16%, 0.26); - --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 16%, 0.07); - --md-code-bg-color: #16181d; - --md-primary-fg-color: #2b3540; - --md-default-fg-color--light: #fff; - --md-typeset-a-color: #00b0f0; - --md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43); - --tabbed-labels-color: rgb(52 58 70); -} - -[data-md-color-scheme="default"] { - --tabbed-labels-color: #7d829e26; -} - -/* General admonition styling */ -/* TODO: Write this in a way that supports the light theme */ -[data-md-color-scheme="slate"] .md-typeset details, -[data-md-color-scheme="slate"] .md-typeset .admonition { - border-color: transparent !important; -} - -.md-typeset :is(.admonition, details) { - margin: 0.55em 0; -} - -.md-typeset .admonition { - font-size: 0.7rem; -} - -.md-typeset .admonition:focus-within, -.md-typeset details:focus-within { - box-shadow: var(--md-shadow-z1) !important; -} - -/* Colors for "summary" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.summary { - background: #353a45; - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .summary .admonition-title { - font-size: 1rem; - background: transparent; - padding-left: 0.6rem; - padding-bottom: 0; -} - -[data-md-color-scheme="slate"] .md-typeset .summary .admonition-title:before { - display: none; -} - -[data-md-color-scheme="slate"] .md-typeset .admonition.summary { - border-color: #ffffff17 !important; -} - -/* Colors for "note" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.note { - background: rgb(43 110 98/ 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .note .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(68 172 153); -} - -[data-md-color-scheme="slate"] .md-typeset .note .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(68 172 153); -} - -.md-typeset .note > .admonition-title:before, -.md-typeset .note > summary:before { - -webkit-mask-image: var(--md-admonition-icon--abstract); - mask-image: var(--md-admonition-icon--abstract); -} - -/* Colors for "warning" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.warning { - background: rgb(182 87 0 / 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .warning .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(219 125 39); -} - -[data-md-color-scheme="slate"] .md-typeset .warning .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(219 125 39); -} - -/* Colors for "info" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.info { - background: rgb(43 52 145 / 0.2); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; -} - -[data-md-color-scheme="slate"] .md-typeset .info .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(136 145 236); -} - -[data-md-color-scheme="slate"] .md-typeset .info .admonition-title:before { - font-size: 1.1rem; - background-color: rgb(136 145 236); -} - -/* Colors for "example" admonition */ -[data-md-color-scheme="slate"] .md-typeset .admonition.example { - background: rgb(94 104 126); - border-radius: 0.4rem; -} - -[data-md-color-scheme="slate"] .md-typeset .example .admonition-title { - background: rgb(78 87 105); - color: rgb(246 247 249); -} - -[data-md-color-scheme="slate"] .md-typeset .example .admonition-title:before { - background-color: rgb(246 247 249); -} - -[data-md-color-scheme="slate"] .md-typeset .admonition.example code { - background: transparent; - color: #fff; -} - -/* Move the sidebars to the edges of the page */ -.md-main__inner.md-grid { - margin-left: 0; - margin-right: 0; - max-width: unset; - display: flex; - justify-content: center; -} - -.md-sidebar--primary { - margin-right: auto; -} - -.md-sidebar.md-sidebar--secondary { - margin-left: auto; -} - -.md-content { - max-width: 56rem; -} - -/* Maintain content positioning even if sidebars are disabled */ -@media screen and (min-width: 76.1875em) { - .md-sidebar { - display: block; - } - - .md-sidebar[hidden] { - visibility: hidden; - } -} - -/* Sidebar styling */ -@media screen and (min-width: 76.1875em) { - .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { - text-transform: uppercase; - } - - .md-nav__title[for="__toc"] { - text-transform: uppercase; - margin: 0.5rem; - } - - .md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link { - color: rgb(133 142 159); - margin: 0.5rem; - } - - .md-nav__item .md-nav__link { - position: relative; - } - - .md-nav__link:is(:focus, :hover):not(.md-nav__link--active) { - color: unset; - } - - .md-nav__item - .md-nav__link:is(:focus, :hover):not(.md-nav__link--active):before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.2; - z-index: -1; - background-color: grey; - } - - .md-nav__item .md-nav__link--active:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.15; - z-index: -1; - background-color: var(--md-typeset-a-color); - } - - .md-nav__link { - padding: 0.5rem 0.5rem 0.5rem 1rem; - margin: 0; - border-radius: 0 10px 10px 0; - font-weight: 600; - overflow: hidden; - } - - .md-sidebar__scrollwrap { - margin: 0; - } - - [dir="ltr"] - .md-nav--lifted - .md-nav[data-md-level="1"] - > .md-nav__list - > .md-nav__item { - padding: 0; - } - - .md-nav__item--nested .md-nav__item .md-nav__item { - padding: 0; - } - - .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { - font-weight: 300; - } - - .md-nav__item--nested .md-nav__item .md-nav__item .md-nav__link { - font-weight: 400; - padding-left: 1.25rem; - } -} - -/* Table of Contents styling */ -@media screen and (min-width: 60em) { - [data-md-component="sidebar"] .md-nav__title[for="__toc"] { - text-transform: uppercase; - margin: 0.5rem; - margin-left: 0; - } - - [data-md-component="toc"] .md-nav__item .md-nav__link--active { - position: relative; - } - - [data-md-component="toc"] .md-nav__item .md-nav__link--active:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.15; - z-index: -1; - background-color: var(--md-typeset-a-color); - } - - [data-md-component="toc"] .md-nav__link { - padding: 0.5rem 0.5rem; - margin: 0; - border-radius: 10px 0 0 10px; - } - [dir="ltr"] .md-sidebar__inner { - padding: 0; - } - - .md-nav__item { - padding: 0; - } -} - -/* Font changes */ -.md-typeset { - font-weight: 300; -} - -.md-typeset h1 { - font-weight: 500; - margin: 0; - font-size: 2.5em; -} - -.md-typeset h2 { - font-weight: 500; -} - -.md-typeset h3 { - font-weight: 600; -} - -/* Intro section styling */ -p.intro { - font-size: 0.9rem; - font-weight: 500; -} - -/* Hide invisible jump selectors */ -h2#overview { - visibility: hidden; - height: 0; - margin: 0; - padding: 0; -} - -/* Code blocks */ -.md-typeset pre > code { - border-radius: 16px; -} - -.md-typeset .highlighttable .linenos { - max-height: var(--code-max-height); - overflow: hidden; -} - -.md-typeset .tabbed-block .highlighttable code { - border-radius: 0; -} - -.md-typeset .tabbed-block { - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - overflow: hidden; -} - -.js .md-typeset .tabbed-labels { - background: var(--tabbed-labels-color); - border-top-left-radius: 8px; - border-top-right-radius: 8px; -} - -.md-typeset .tabbed-labels > label { - font-weight: 400; - font-size: 0.7rem; - padding-top: 0.55em; - padding-bottom: 0.35em; -} - -.md-typeset pre > code { - max-height: var(--code-max-height); -} - -/* Reduce height of outdated banner */ -.md-banner__inner { - margin: 0.45rem auto; -} diff --git a/mkdocs.yml b/mkdocs.yml index ae5129bc..e4e19c65 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,28 +2,22 @@ nav: - Home: index.md - Get Started: - - Install ReactPy-Django: get-started/installation.md - - Choose a Django App: get-started/choose-django-app.md - - Create a Component: get-started/create-component.md - - Use the Template Tag: get-started/use-template-tag.md - - Register a View: get-started/register-view.md - - Run the Webserver: get-started/run-webserver.md - - Learn More: get-started/learn-more.md + - Add ReactPy to a Django Project: learn/add-reactpy-to-a-django-project.md + - Your First Component: learn/your-first-component.md - Reference: - - Components: features/components.md - - Hooks: features/hooks.md - - Decorators: features/decorators.md - - Utilities: features/utils.md - - Template Tag: features/template-tag.md - - Settings: features/settings.md + - Components: reference/components.md + - Hooks: reference/hooks.md + - Decorators: reference/decorators.md + - Utilities: reference/utils.md + - Template Tag: reference/template-tag.md + - Settings: reference/settings.md - About: - Contribute: - - Code: contribute/code.md - - Docs: contribute/docs.md - - Running Tests: contribute/running-tests.md + - Code: about/code.md + - Docs: about/docs.md - GitHub Discussions: https://github.com/reactive-python/reactpy-django/discussions - Discord: https://discord.gg/uNb5P4hA9X - - Changelog: changelog/index.md + - Changelog: about/changelog.md theme: name: material @@ -34,19 +28,22 @@ theme: toggle: icon: material/white-balance-sunny name: Switch to light mode - primary: light blue - accent: light blue + primary: red # We use red to indicate that something is unthemed + accent: red - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/weather-night name: Switch to dark mode - primary: black + primary: white + accent: red features: - navigation.instant - navigation.tabs + - navigation.tabs.sticky - navigation.top - content.code.copy + - search.highlight icon: repo: fontawesome/brands/github logo: https://raw.githubusercontent.com/reactive-python/reactpy/main/branding/svg/reactpy-logo-square.svg @@ -93,10 +90,18 @@ extra: provider: mike extra_javascript: - - static/js/extra.js + - assets/js/main.js extra_css: - - static/css/extra.css + - assets/css/main.css + - assets/css/button.css + - assets/css/admonition.css + - assets/css/sidebar.css + - assets/css/navbar.css + - assets/css/table-of-contents.css + - assets/css/code.css + - assets/css/footer.css + - assets/css/home.css watch: - docs @@ -107,8 +112,8 @@ watch: site_name: ReactPy-Django site_author: Archmonger -site_description: React for Django developers. -copyright: Copyright © 2023 Reactive Python +site_description: It's React, but in Python. Now for Django developers. +copyright: Copyright © 2023 Reactive Python. repo_url: https://github.com/reactive-python/reactpy-django site_url: https://reactive-python.github.io/reactpy-django repo_name: reactive-python/reactpy-django diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py index adc437d0..0025008b 100644 --- a/src/reactpy_django/checks.py +++ b/src/reactpy_django/checks.py @@ -60,7 +60,7 @@ def reactpy_warnings(app_configs, **kwargs): Warning( "Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled " "and you running with Daphne.", - hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different webserver.", + hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different web server.", id="reactpy_django.W003", ) ) diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py index a99da927..df552e42 100644 --- a/tests/test_app/settings.py +++ b/tests/test_app/settings.py @@ -24,7 +24,7 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" -# Run in production mode when using a real webserver +# Run in production mode when using a real web server DEBUG = all( not sys.argv[0].endswith(substring) for substring in {"hypercorn", "uvicorn", "daphne"}