Skip to content

Commit

Permalink
Allow users to modify component's host URLs (#172)
Browse files Browse the repository at this point in the history
### Added

-   **Distributed Computing:** ReactPy components can now optionally be rendered by a completely separate server!
    -   `REACTPY_DEFAULT_HOSTS` setting can round-robin a list of ReactPy rendering hosts.
    -   `host` argument has been added to the `component` template tag to force components to render on a specific host.
-   `reactpy_django.utils.register_component` function to manually register root components.
    -   Useful if you have dedicated ReactPy rendering application(s) that do not use HTML templates.

### Changed

-   ReactPy will now provide a warning if your HTTP URLs are not on the same prefix as your websockets.
-   Cleaner logging output for detected ReactPy root components.

### Deprecated

-   `reactpy_django.REACTPY_WEBSOCKET_PATH` is deprecated. The similar replacement is `REACTPY_WEBSOCKET_ROUTE`.
-   `settings.py:REACTPY_WEBSOCKET_URL` is deprecated. The similar replacement is `REACTPY_URL_PREFIX`.

### Removed

-   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.
  • Loading branch information
Archmonger authored Aug 18, 2023
1 parent 88acbaf commit 41641aa
Show file tree
Hide file tree
Showing 31 changed files with 584 additions and 216 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ celerybeat-schedule.*
*.sage.py

# Environments
.env
.venv
.env*/
.venv*/
env/
venv/
ENV/
Expand Down
24 changes: 23 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,29 @@ Using the following categories, list your changes in this order:

## [Unreleased]

- Nothing (yet)!
### Added

- **Distributed Computing:** ReactPy components can now optionally be rendered by a completely separate server!
- `REACTPY_DEFAULT_HOSTS` setting can round-robin a list of ReactPy rendering hosts.
- `host` argument has been added to the `component` template tag to force components to render on a specific host.
- `reactpy_django.utils.register_component` function to manually register root components.
- Useful if you have dedicated ReactPy rendering application(s) that do not use HTML templates.

### Changed

- 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

- `reactpy_django.REACTPY_WEBSOCKET_PATH` is deprecated. The identical replacement is `REACTPY_WEBSOCKET_ROUTE`.
- `settings.py:REACTPY_WEBSOCKET_URL` is deprecated. The similar replacement is `REACTPY_URL_PREFIX`.

### Removed

- 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.

## [3.3.2] - 2023-08-13

Expand Down
4 changes: 2 additions & 2 deletions docs/python/configure-asgi-middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Broken load order, only used for linting
from channels.routing import ProtocolTypeRouter, URLRouter
from reactpy_django import REACTPY_WEBSOCKET_PATH
from reactpy_django import REACTPY_WEBSOCKET_ROUTE

django_asgi_app = ""

Expand All @@ -15,7 +15,7 @@
"websocket": SessionMiddlewareStack(
AuthMiddlewareStack(
URLRouter(
[REACTPY_WEBSOCKET_PATH],
[REACTPY_WEBSOCKET_ROUTE],
)
)
),
Expand Down
4 changes: 2 additions & 2 deletions docs/python/configure-asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@


from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
from reactpy_django import REACTPY_WEBSOCKET_PATH # noqa: E402
from reactpy_django import REACTPY_WEBSOCKET_ROUTE # noqa: E402

application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": URLRouter([REACTPY_WEBSOCKET_PATH]),
"websocket": URLRouter([REACTPY_WEBSOCKET_ROUTE]),
}
)
8 changes: 8 additions & 0 deletions docs/python/register-component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig
from reactpy_django.utils import register_component


class ExampleConfig(AppConfig):
def ready(self):
# Add components to the ReactPy component registry when Django is ready
register_component("example_project.my_app.components.hello_world")
32 changes: 0 additions & 32 deletions docs/python/settings.py

This file was deleted.

2 changes: 1 addition & 1 deletion docs/src/features/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Convert any Django view into a ReactPy component by using this decorator. Compat

It is your responsibility to ensure privileged information is not leaked via this method.

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 prior to using `view_to_component`.
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...

=== "Function Based View"

Expand Down
19 changes: 14 additions & 5 deletions docs/src/features/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@

## Primary Configuration

<!--config-details-start-->

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.

=== "settings.py"
| 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.<br/>We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). |
| `REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database ReactPy uses to store session data. ReactPy requires a multiprocessing-safe and thread-safe database.<br/>If configuring `REACTPY_DATABASE`, it is mandatory to also configure `DATABASE_ROUTERS` like such:<br/>`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` |
| `REACTPY_RECONNECT_MAX` | `#!python 259200` | `#!python 96000`, `#!python 60`, `#!python 0` | Maximum seconds between reconnection attempts before giving up.<br/>Use `#!python 0` to prevent reconnection. |
| `REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy websocket and HTTP URLs. |
| `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function. |
| `REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:<br/> 1. You are using `AuthMiddlewareStack` and...<br/> 2. You are using Django's `AUTHENTICATION_BACKENDS` setting and...<br/> 3. Your Django user model does not define a `backend` attribute. |
| `REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the webserver to process web traffic while during ReactPy rendering.<br/>Vastly improves throughput with web servers such as `hypercorn` and `uvicorn`. |
| `REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | Default host(s) to use for ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing.<br/>You can use the `host` argument in your [template tag](../features/template-tag.md#component) to override this default. |

```python
{% include "../../python/settings.py" %}
```
<!--config-details-end-->

??? question "Do I need to modify my settings?"

The default configuration of ReactPy is adequate for the majority of use cases.
The default configuration of ReactPy is suitable for the majority of use cases.

You should only consider changing settings when the necessity arises.
23 changes: 22 additions & 1 deletion docs/src/features/template-tag.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The `component` template tag can be used to insert any number of ReactPy compone
| --- | --- | --- | --- |
| `dotted_path` | `str` | The dotted path to the component to render. | N/A |
| `*args` | `Any` | The positional arguments to provide to the component. | N/A |
| `host` | `str | None` | The host to use for the ReactPy connections. If set to `None`, the host will be automatically configured.<br/>Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `None` |
| `**kwargs` | `Any` | The keyword arguments to provide to the component. | N/A |

<font size="4">**Returns**</font>
Expand Down Expand Up @@ -73,6 +74,27 @@ The `component` template tag can be used to insert any number of ReactPy compone
```

<!--reserved-sarg-end-->

??? question "Can I render components on a different server (distributed computing)?"

Yes! By using the `host` keyword argument, you can render components from a completely separate ASGI server.

=== "my-template.html"

```jinja
...
{% component "example_project.my_app.components.do_something" host="127.0.0.1:8001" %}
...
```

This configuration most commonly involves you deploying multiple instances of your project. But, you can also create dedicated Django project(s) that only render specific ReactPy components if you wish.

Here's a couple of things to keep in mind:

1. If your host address are completely separate ( `origin1.com != origin2.com` ) you will need to [configure CORS headers](https://pypi.org/project/django-cors-headers/) on your main application during deployment.
2. You will not need to register ReactPy HTTP or websocket paths on any applications that do not perform any component rendering.
3. Your component will only be able to access `*args`/`**kwargs` you provide to the template tag if your applications share a common database.

<!--multiple-components-start-->

??? question "Can I use multiple components on one page?"
Expand All @@ -98,7 +120,6 @@ The `component` template tag can be used to insert any number of ReactPy compone
Additionally, in scenarios where you are trying to create a Single Page Application (SPA) within Django, you will only have one component within your `#!html <body>` tag.

<!--multiple-components-end-->

<!--args-kwargs-start-->

??? question "Can I use positional arguments instead of keyword arguments?"
Expand Down
22 changes: 22 additions & 0 deletions docs/src/features/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,25 @@ This postprocessor is designed to avoid Django's `SynchronousOnlyException` by r
| 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.
16 changes: 10 additions & 6 deletions docs/src/get-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt

??? note "Configure ReactPy settings (Optional)"

Below are a handful of values you can change within `settings.py` to modify the behavior of ReactPy.

```python linenums="0"
{% include "../../python/settings.py" %}
```
{% include "../features/settings.md" start="<!--config-details-start-->" end="<!--config-details-end-->" %}

## Step 3: Configure [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/)

Expand All @@ -62,7 +58,7 @@ Add ReactPy HTTP paths to your `urlpatterns`.

## Step 4: Configure [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/)

Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`.
Register ReactPy's Websocket using `REACTPY_WEBSOCKET_ROUTE`.

=== "asgi.py"

Expand Down Expand Up @@ -95,3 +91,11 @@ 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
```
6 changes: 2 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[tool.mypy]
exclude = [
'migrations/.*',
]
exclude = ['migrations/.*']
ignore_missing_imports = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
check_untyped_defs = true
incremental = false
incremental = true

[tool.ruff.isort]
known-first-party = ["src", "tests"]
Expand Down
53 changes: 39 additions & 14 deletions src/js/src/index.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,59 @@
import { mountLayoutWithWebSocket } from "@reactpy/client";

// Set up a websocket at the base endpoint
const LOCATION = window.location;
let HTTP_PROTOCOL = window.location.protocol;
let WS_PROTOCOL = "";
if (LOCATION.protocol == "https:") {
WS_PROTOCOL = "wss://";
if (HTTP_PROTOCOL == "https:") {
WS_PROTOCOL = "wss:";
} else {
WS_PROTOCOL = "ws://";
WS_PROTOCOL = "ws:";
}
const WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";

export function mountViewToElement(
mountElement,
reactpyWebsocketUrl,
reactpyWebModulesUrl,
maxReconnectTimeout,
componentPath
reactpyHost,
reactpyUrlPrefix,
reactpyReconnectMax,
reactpyComponentPath,
reactpyResolvedWebModulesPath
) {
const WS_URL = WS_ENDPOINT_URL + reactpyWebsocketUrl + componentPath;
const WEB_MODULE_URL = LOCATION.origin + "/" + reactpyWebModulesUrl;
// Determine the Websocket route
let wsOrigin;
if (reactpyHost) {
wsOrigin = `${WS_PROTOCOL}//${reactpyHost}`;
} else {
wsOrigin = `${WS_PROTOCOL}//${window.location.host}`;
}
const websocketUrl = `${wsOrigin}/${reactpyUrlPrefix}/${reactpyComponentPath}`;

// Determine the HTTP route
let httpOrigin;
let webModulesPath;
if (reactpyHost) {
httpOrigin = `${HTTP_PROTOCOL}//${reactpyHost}`;
webModulesPath = `${reactpyUrlPrefix}/web_module`;
} else {
httpOrigin = `${HTTP_PROTOCOL}//${window.location.host}`;
if (reactpyResolvedWebModulesPath) {
webModulesPath = reactpyResolvedWebModulesPath;
} else {
webModulesPath = `${reactpyUrlPrefix}/web_module`;
}
}
const webModuleUrl = `${httpOrigin}/${webModulesPath}`;

// Function that loads the JavaScript web module, if needed
const loadImportSource = (source, sourceType) => {
return import(
sourceType == "NAME" ? `${WEB_MODULE_URL}${source}` : source
sourceType == "NAME" ? `${webModuleUrl}/${source}` : source
);
};

// Start rendering the component
mountLayoutWithWebSocket(
mountElement,
WS_URL,
websocketUrl,
loadImportSource,
maxReconnectTimeout
reactpyReconnectMax
);
}
6 changes: 5 additions & 1 deletion src/reactpy_django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
import nest_asyncio

from reactpy_django import checks, components, decorators, hooks, types, utils
from reactpy_django.websocket.paths import REACTPY_WEBSOCKET_PATH
from reactpy_django.websocket.paths import (
REACTPY_WEBSOCKET_PATH,
REACTPY_WEBSOCKET_ROUTE,
)

__version__ = "3.3.2"
__all__ = [
"REACTPY_WEBSOCKET_PATH",
"REACTPY_WEBSOCKET_ROUTE",
"hooks",
"components",
"decorators",
Expand Down
Loading

0 comments on commit 41641aa

Please sign in to comment.