Skip to content

Commit 41641aa

Browse files
authored
Allow users to modify component's host URLs (#172)
### 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.
1 parent 88acbaf commit 41641aa

31 files changed

+584
-216
lines changed

.gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ celerybeat-schedule.*
8989
*.sage.py
9090

9191
# Environments
92-
.env
93-
.venv
92+
.env*/
93+
.venv*/
9494
env/
9595
venv/
9696
ENV/

CHANGELOG.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,29 @@ Using the following categories, list your changes in this order:
3434

3535
## [Unreleased]
3636

37-
- Nothing (yet)!
37+
### Added
38+
39+
- **Distributed Computing:** ReactPy components can now optionally be rendered by a completely separate server!
40+
- `REACTPY_DEFAULT_HOSTS` setting can round-robin a list of ReactPy rendering hosts.
41+
- `host` argument has been added to the `component` template tag to force components to render on a specific host.
42+
- `reactpy_django.utils.register_component` function to manually register root components.
43+
- Useful if you have dedicated ReactPy rendering application(s) that do not use HTML templates.
44+
45+
### Changed
46+
47+
- ReactPy will now provide a warning if your HTTP URLs are not on the same prefix as your websockets.
48+
- Cleaner logging output for auto-detected ReactPy root components.
49+
50+
### Deprecated
51+
52+
- `reactpy_django.REACTPY_WEBSOCKET_PATH` is deprecated. The identical replacement is `REACTPY_WEBSOCKET_ROUTE`.
53+
- `settings.py:REACTPY_WEBSOCKET_URL` is deprecated. The similar replacement is `REACTPY_URL_PREFIX`.
54+
55+
### Removed
56+
57+
- Warning W007 (`REACTPY_WEBSOCKET_URL doesn't end with a slash`) has been removed. ReactPy now automatically handles slashes.
58+
- Warning W008 (`REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character`) has been removed. ReactPy now automatically handles this scenario.
59+
- 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.
3860

3961
## [3.3.2] - 2023-08-13
4062

docs/python/configure-asgi-middleware.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Broken load order, only used for linting
22
from channels.routing import ProtocolTypeRouter, URLRouter
3-
from reactpy_django import REACTPY_WEBSOCKET_PATH
3+
from reactpy_django import REACTPY_WEBSOCKET_ROUTE
44

55
django_asgi_app = ""
66

@@ -15,7 +15,7 @@
1515
"websocket": SessionMiddlewareStack(
1616
AuthMiddlewareStack(
1717
URLRouter(
18-
[REACTPY_WEBSOCKET_PATH],
18+
[REACTPY_WEBSOCKET_ROUTE],
1919
)
2020
)
2121
),

docs/python/configure-asgi.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111

1212
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
13-
from reactpy_django import REACTPY_WEBSOCKET_PATH # noqa: E402
13+
from reactpy_django import REACTPY_WEBSOCKET_ROUTE # noqa: E402
1414

1515
application = ProtocolTypeRouter(
1616
{
1717
"http": django_asgi_app,
18-
"websocket": URLRouter([REACTPY_WEBSOCKET_PATH]),
18+
"websocket": URLRouter([REACTPY_WEBSOCKET_ROUTE]),
1919
}
2020
)

docs/python/register-component.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.apps import AppConfig
2+
from reactpy_django.utils import register_component
3+
4+
5+
class ExampleConfig(AppConfig):
6+
def ready(self):
7+
# Add components to the ReactPy component registry when Django is ready
8+
register_component("example_project.my_app.components.hello_world")

docs/python/settings.py

-32
This file was deleted.

docs/src/features/components.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Convert any Django view into a ReactPy component by using this decorator. Compat
3737

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

40-
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`.
40+
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...
4141

4242
=== "Function Based View"
4343

docs/src/features/settings.md

+14-5
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,25 @@
66

77
## Primary Configuration
88

9+
<!--config-details-start-->
10+
911
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.
1012

11-
=== "settings.py"
13+
| Setting | Default Value | Example Value(s) | Description |
14+
| --- | --- | --- | --- |
15+
| `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). |
16+
| `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", ...]` |
17+
| `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. |
18+
| `REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy websocket and HTTP URLs. |
19+
| `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. |
20+
| `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. |
21+
| `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`. |
22+
| `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. |
1223

13-
```python
14-
{% include "../../python/settings.py" %}
15-
```
24+
<!--config-details-end-->
1625

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

19-
The default configuration of ReactPy is adequate for the majority of use cases.
28+
The default configuration of ReactPy is suitable for the majority of use cases.
2029

2130
You should only consider changing settings when the necessity arises.

docs/src/features/template-tag.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The `component` template tag can be used to insert any number of ReactPy compone
2020
| --- | --- | --- | --- |
2121
| `dotted_path` | `str` | The dotted path to the component to render. | N/A |
2222
| `*args` | `Any` | The positional arguments to provide to the component. | N/A |
23+
| `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` |
2324
| `**kwargs` | `Any` | The keyword arguments to provide to the component. | N/A |
2425

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

7576
<!--reserved-sarg-end-->
77+
78+
??? question "Can I render components on a different server (distributed computing)?"
79+
80+
Yes! By using the `host` keyword argument, you can render components from a completely separate ASGI server.
81+
82+
=== "my-template.html"
83+
84+
```jinja
85+
...
86+
{% component "example_project.my_app.components.do_something" host="127.0.0.1:8001" %}
87+
...
88+
```
89+
90+
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.
91+
92+
Here's a couple of things to keep in mind:
93+
94+
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.
95+
2. You will not need to register ReactPy HTTP or websocket paths on any applications that do not perform any component rendering.
96+
3. Your component will only be able to access `*args`/`**kwargs` you provide to the template tag if your applications share a common database.
97+
7698
<!--multiple-components-start-->
7799

78100
??? question "Can I use multiple components on one page?"
@@ -98,7 +120,6 @@ The `component` template tag can be used to insert any number of ReactPy compone
98120
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.
99121

100122
<!--multiple-components-end-->
101-
102123
<!--args-kwargs-start-->
103124

104125
??? question "Can I use positional arguments instead of keyword arguments?"

docs/src/features/utils.md

+22
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,25 @@ This postprocessor is designed to avoid Django's `SynchronousOnlyException` by r
3737
| Type | Description |
3838
| --- | --- |
3939
| `QuerySet | Model` | The `Model` or `QuerySet` with all fields fetched. |
40+
41+
## Register Component
42+
43+
The `register_component` function is used manually register a root component with ReactPy.
44+
45+
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.
46+
47+
=== "apps.py"
48+
49+
```python
50+
{% include "../../python/register-component.py" %}
51+
```
52+
53+
??? question "Do I need to register my components?"
54+
55+
You typically will not need to use this function.
56+
57+
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.
58+
59+
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.
60+
61+
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.

docs/src/get-started/installation.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,7 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt
4444

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

47-
Below are a handful of values you can change within `settings.py` to modify the behavior of ReactPy.
48-
49-
```python linenums="0"
50-
{% include "../../python/settings.py" %}
51-
```
47+
{% include "../features/settings.md" start="<!--config-details-start-->" end="<!--config-details-end-->" %}
5248

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

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

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

65-
Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`.
61+
Register ReactPy's Websocket using `REACTPY_WEBSOCKET_ROUTE`.
6662

6763
=== "asgi.py"
6864

@@ -95,3 +91,11 @@ Run Django's database migrations to initialize ReactPy-Django's database table.
9591
```bash linenums="0"
9692
python manage.py migrate
9793
```
94+
95+
## Step 6: Check your configuration
96+
97+
Run Django's check command to verify if ReactPy was set up correctly.
98+
99+
```bash linenums="0"
100+
python manage.py check
101+
```

pyproject.toml

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ requires = ["setuptools>=42", "wheel"]
33
build-backend = "setuptools.build_meta"
44

55
[tool.mypy]
6-
exclude = [
7-
'migrations/.*',
8-
]
6+
exclude = ['migrations/.*']
97
ignore_missing_imports = true
108
warn_unused_configs = true
119
warn_redundant_casts = true
1210
warn_unused_ignores = true
1311
check_untyped_defs = true
14-
incremental = false
12+
incremental = true
1513

1614
[tool.ruff.isort]
1715
known-first-party = ["src", "tests"]

src/js/src/index.js

+39-14
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,59 @@
11
import { mountLayoutWithWebSocket } from "@reactpy/client";
22

33
// Set up a websocket at the base endpoint
4-
const LOCATION = window.location;
4+
let HTTP_PROTOCOL = window.location.protocol;
55
let WS_PROTOCOL = "";
6-
if (LOCATION.protocol == "https:") {
7-
WS_PROTOCOL = "wss://";
6+
if (HTTP_PROTOCOL == "https:") {
7+
WS_PROTOCOL = "wss:";
88
} else {
9-
WS_PROTOCOL = "ws://";
9+
WS_PROTOCOL = "ws:";
1010
}
11-
const WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
1211

1312
export function mountViewToElement(
1413
mountElement,
15-
reactpyWebsocketUrl,
16-
reactpyWebModulesUrl,
17-
maxReconnectTimeout,
18-
componentPath
14+
reactpyHost,
15+
reactpyUrlPrefix,
16+
reactpyReconnectMax,
17+
reactpyComponentPath,
18+
reactpyResolvedWebModulesPath
1919
) {
20-
const WS_URL = WS_ENDPOINT_URL + reactpyWebsocketUrl + componentPath;
21-
const WEB_MODULE_URL = LOCATION.origin + "/" + reactpyWebModulesUrl;
20+
// Determine the Websocket route
21+
let wsOrigin;
22+
if (reactpyHost) {
23+
wsOrigin = `${WS_PROTOCOL}//${reactpyHost}`;
24+
} else {
25+
wsOrigin = `${WS_PROTOCOL}//${window.location.host}`;
26+
}
27+
const websocketUrl = `${wsOrigin}/${reactpyUrlPrefix}/${reactpyComponentPath}`;
28+
29+
// Determine the HTTP route
30+
let httpOrigin;
31+
let webModulesPath;
32+
if (reactpyHost) {
33+
httpOrigin = `${HTTP_PROTOCOL}//${reactpyHost}`;
34+
webModulesPath = `${reactpyUrlPrefix}/web_module`;
35+
} else {
36+
httpOrigin = `${HTTP_PROTOCOL}//${window.location.host}`;
37+
if (reactpyResolvedWebModulesPath) {
38+
webModulesPath = reactpyResolvedWebModulesPath;
39+
} else {
40+
webModulesPath = `${reactpyUrlPrefix}/web_module`;
41+
}
42+
}
43+
const webModuleUrl = `${httpOrigin}/${webModulesPath}`;
44+
45+
// Function that loads the JavaScript web module, if needed
2246
const loadImportSource = (source, sourceType) => {
2347
return import(
24-
sourceType == "NAME" ? `${WEB_MODULE_URL}${source}` : source
48+
sourceType == "NAME" ? `${webModuleUrl}/${source}` : source
2549
);
2650
};
2751

52+
// Start rendering the component
2853
mountLayoutWithWebSocket(
2954
mountElement,
30-
WS_URL,
55+
websocketUrl,
3156
loadImportSource,
32-
maxReconnectTimeout
57+
reactpyReconnectMax
3358
);
3459
}

src/reactpy_django/__init__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
import nest_asyncio
44

55
from reactpy_django import checks, components, decorators, hooks, types, utils
6-
from reactpy_django.websocket.paths import REACTPY_WEBSOCKET_PATH
6+
from reactpy_django.websocket.paths import (
7+
REACTPY_WEBSOCKET_PATH,
8+
REACTPY_WEBSOCKET_ROUTE,
9+
)
710

811
__version__ = "3.3.2"
912
__all__ = [
1013
"REACTPY_WEBSOCKET_PATH",
14+
"REACTPY_WEBSOCKET_ROUTE",
1115
"hooks",
1216
"components",
1317
"decorators",

0 commit comments

Comments
 (0)