Skip to content

Commit b307b3e

Browse files
authored
UserModel related hooks, decorators, and settings (#190)
- New Django `User` related features! - `reactpy_django.hooks.use_user` can be used to access the current user. - `reactpy_django.hooks.use_user_data` provides a simplified interface for storing user key-value data. - `reactpy_django.decorators.user_passes_test` is inspired by Django's [`user_passes_test`](http://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) decorator, but works with ReactPy components. - `settings.py:REACTPY_AUTO_RELOGIN` will cause component WebSocket connections to automatically [re-login](https://channels.readthedocs.io/en/latest/topics/authentication.html#how-to-log-a-user-in-out) users that are already authenticated. This is useful to continuously update `last_login` timestamps and refresh the [Django login session](https://docs.djangoproject.com/en/dev/topics/http/sessions/).
1 parent 6f79c4c commit b307b3e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1165
-356
lines changed

CHANGELOG.md

+25-9
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,38 @@ Using the following categories, list your changes in this order:
3636

3737
### Added
3838

39-
- ReactPy components can now use SEO compatible rendering!
40-
- `settings.py:REACTPY_PRERENDER` can be set to `True` to enable this behavior by default
41-
- Or, you can enable it on individual components via the template tag: `{% component "..." prerender="True" %}`
42-
- `reactpy_django.components.view_to_iframe` component has been added, which uses an `<iframe>` to render a Django view.
43-
- `reactpy_django.utils.register_iframe` function has been added, which is mandatory to use alongside `reactpy_django.components.view_to_iframe`.
39+
- SEO compatible rendering!
40+
- `settings.py:REACTPY_PRERENDER` can be set to `True` to make components pre-render by default.
41+
- Or, you can enable it on individual components via the template tag: `{% component "..." prerender="True" %}`.
42+
- New `view_to_iframe` feature!
43+
- `reactpy_django.components.view_to_iframe` uses an `<iframe>` to render a Django view.
44+
- `reactpy_django.utils.register_iframe` tells ReactPy which views `view_to_iframe` can use.
45+
- New Django `User` related features!
46+
- `reactpy_django.hooks.use_user` can be used to access the current user.
47+
- `reactpy_django.hooks.use_user_data` provides a simplified interface for storing user key-value data.
48+
- `reactpy_django.decorators.user_passes_test` is inspired by the [equivalent Django decorator](http://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test), but ours works with ReactPy components.
49+
- `settings.py:REACTPY_AUTO_RELOGIN` will cause component WebSocket connections to automatically [re-login](https://channels.readthedocs.io/en/latest/topics/authentication.html#how-to-log-a-user-in-out) users that are already authenticated. This is useful to continuously update `last_login` timestamps and refresh the [Django login session](https://docs.djangoproject.com/en/dev/topics/http/sessions/).
4450

4551
### Changed
4652

47-
- Renamed undocumented utility function `reactpy_django.utils.ComponentPreloader` to `reactpy_django.utils.RootComponentFinder`.
53+
- Renamed undocumented utility function `ComponentPreloader` to `RootComponentFinder`.
4854
- It is now recommended to call `as_view()` when using `view_to_component` or `view_to_iframe` with Class Based Views.
49-
- Thread sensitivity has been enabled in all locations where ORM queries are possible.
55+
- For thread safety, `thread_sensitive=True` has been enabled in all `sync_to_async` functions where ORM queries are possible.
56+
- `reactpy_django.hooks.use_mutation` now has a `__call__` method. So rather than writing `my_mutation.execute(...)`, you can now write `my_mutation(...)`.
5057

5158
### Deprecated
5259

53-
- The `compatibility` argument on `reactpy_django.components.view_to_component` is deprecated. Use `reactpy_django.components.view_to_iframe` instead.
54-
- Using `reactpy_django.components.view_to_component` as a decorator is deprecated. Check the docs on the new suggested usage.
60+
- The `compatibility` argument on `reactpy_django.components.view_to_component` is deprecated.
61+
- Use `view_to_iframe` as a replacement.
62+
- `reactpy_django.components.view_to_component` **usage as a decorator** is deprecated.
63+
- Check the docs on how to use `view_to_component` as a function instead.
64+
- `reactpy_django.decorators.auth_required` is deprecated.
65+
- Use `reactpy_django.decorators.user_passes_test` instead.
66+
- An equivalent to `auth_required`'s default is `@user_passes_test(lambda user: user.is_active)`.
67+
68+
### Fixed
69+
70+
- Fixed a bug where exception stacks would not print on failed component renders.
5571

5672
## [3.5.1] - 2023-09-07
5773

docs/overrides/home.html

+3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ <h1>{{ config.site_name }}</h1>
5858
<a href="{{ 'reference/components/' | url }}" class="md-button">
5959
API Reference
6060
</a>
61+
<a href="{{ 'about/changelog/' | url }}" class="md-button">
62+
Changelog
63+
</a>
6164
</div>
6265
</div>
6366

docs/python/auth-required-attribute.py

-8
This file was deleted.

docs/python/auth-required-custom-attribute-model.py

-7
This file was deleted.

docs/python/auth-required-custom-attribute.py

-8
This file was deleted.

docs/python/auth-required-vdom-fallback.py

-8
This file was deleted.

docs/python/auth-required.py

-8
This file was deleted.

docs/python/configure-asgi-middleware.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@
77

88
# start
99
from channels.auth import AuthMiddlewareStack # noqa: E402
10-
from channels.sessions import SessionMiddlewareStack # noqa: E402
1110

1211
application = ProtocolTypeRouter(
1312
{
1413
"http": django_asgi_app,
15-
"websocket": SessionMiddlewareStack(
16-
AuthMiddlewareStack(
17-
URLRouter(
18-
[REACTPY_WEBSOCKET_ROUTE],
19-
)
14+
"websocket": AuthMiddlewareStack(
15+
URLRouter(
16+
[REACTPY_WEBSOCKET_ROUTE],
2017
)
2118
),
2219
}

docs/python/use-connection.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44

55
@component
66
def my_component():
7-
my_connection = use_connection()
8-
return html.div(str(my_connection))
7+
connection = use_connection()
8+
9+
return html.div(str(connection))

docs/python/use-location.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44

55
@component
66
def my_component():
7-
my_location = use_location()
8-
return html.div(str(my_location))
7+
location = use_location()
8+
9+
return html.div(str(location))

docs/python/use-mutation-args-kwargs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ def example_mutation(value: int, other_value: bool = False):
1010
def my_component():
1111
mutation = use_mutation(example_mutation)
1212

13-
mutation.execute(123, other_value=True)
13+
mutation(123, other_value=True)
1414

1515
...

docs/python/use-mutation-query-refetch.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def todo_list():
1818

1919
def submit_event(event):
2020
if event["key"] == "Enter":
21-
item_mutation.execute(text=event["target"]["value"])
21+
item_mutation(text=event["target"]["value"])
2222

2323
# Handle all possible query states
2424
if item_query.loading:
@@ -38,7 +38,7 @@ def submit_event(event):
3838

3939
return html.div(
4040
html.label("Add an item:"),
41-
html.input({"type": "text", "onKeyDown": submit_event}),
41+
html.input({"type": "text", "on_key_down": submit_event}),
4242
mutation_status,
4343
rendered_items,
4444
)

docs/python/use-mutation-reset.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ def reset_event(event):
1616

1717
def submit_event(event):
1818
if event["key"] == "Enter":
19-
item_mutation.execute(text=event["target"]["value"])
19+
item_mutation(text=event["target"]["value"])
2020

2121
if item_mutation.loading:
2222
mutation_status = html.h2("Adding...")
2323
elif item_mutation.error:
24-
mutation_status = html.button({"onClick": reset_event}, "Error: Try again!")
24+
mutation_status = html.button({"on_click": reset_event}, "Error: Try again!")
2525
else:
2626
mutation_status = html.h2("Mutation done.")
2727

2828
return html.div(
2929
html.label("Add an item:"),
30-
html.input({"type": "text", "onKeyDown": submit_event}),
30+
html.input({"type": "text", "on_key_down": submit_event}),
3131
mutation_status,
3232
)

docs/python/use-mutation-thread-sensitive.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def my_component():
1717

1818
def submit_event(event):
1919
if event["key"] == "Enter":
20-
item_mutation.execute(text=event["target"]["value"])
20+
item_mutation(text=event["target"]["value"])
2121

2222
if item_mutation.loading or item_mutation.error:
2323
mutation_status = html.h2("Doing something...")
@@ -27,6 +27,6 @@ def submit_event(event):
2727
mutation_status = html.h2("Done.")
2828

2929
return html.div(
30-
html.input({"type": "text", "onKeyDown": submit_event}),
30+
html.input({"type": "text", "on_key_down": submit_event}),
3131
mutation_status,
3232
)

docs/python/use-mutation.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def todo_list():
1313

1414
def submit_event(event):
1515
if event["key"] == "Enter":
16-
item_mutation.execute(text=event["target"]["value"])
16+
item_mutation(text=event["target"]["value"])
1717

1818
if item_mutation.loading:
1919
mutation_status = html.h2("Adding...")
@@ -24,6 +24,6 @@ def submit_event(event):
2424

2525
return html.div(
2626
html.label("Add an item:"),
27-
html.input({"type": "text", "onKeyDown": submit_event}),
27+
html.input({"type": "text", "on_key_down": submit_event}),
2828
mutation_status,
2929
)

docs/python/use-origin.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44

55
@component
66
def my_component():
7-
my_origin = use_origin()
8-
return html.div(my_origin or "No origin")
7+
origin = use_origin()
8+
9+
return html.div(origin or "No origin")

docs/python/use-scope.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44

55
@component
66
def my_component():
7-
my_scope = use_scope()
8-
return html.div(str(my_scope))
7+
scope = use_scope()
8+
9+
return html.div(str(scope))

docs/python/use-user-data-defaults.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from reactpy import component, html
2+
from reactpy_django.hooks import use_user_data
3+
4+
5+
@component
6+
def my_component():
7+
user_data = use_user_data(
8+
default_data={
9+
"basic_example": "123",
10+
"computed_example_sync": sync_default,
11+
"computed_example_async": async_default,
12+
}
13+
)
14+
15+
return html.div(
16+
html.div(f"Data: {user_data.query.data}"),
17+
)
18+
19+
20+
def sync_default():
21+
return ...
22+
23+
24+
async def async_default():
25+
return ...

docs/python/use-user-data.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from reactpy import component, html
2+
from reactpy_django.hooks import use_user_data
3+
4+
5+
@component
6+
def my_component():
7+
query, mutation = use_user_data()
8+
9+
def on_submit(event):
10+
if event["key"] == "Enter" and query.data:
11+
new_key = str(len(query.data))
12+
mutation({**query.data, new_key: event["target"]["value"]})
13+
14+
return html.div(
15+
html.div(f"Data: {query.data}"),
16+
html.div(f"Loading: {query.loading | mutation.loading}"),
17+
html.div(f"Error(s): {query.error} {mutation.error}"),
18+
html.input({"on_key_press": on_submit}),
19+
)

docs/python/use-user.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from reactpy import component, html
2+
from reactpy_django.hooks import use_user
3+
4+
5+
@component
6+
def my_component():
7+
user = use_user()
8+
9+
return html.div(user.username)
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from reactpy import component, html
2-
from reactpy_django.decorators import auth_required
2+
from reactpy_django.decorators import user_passes_test
33

44

55
@component
66
def my_component_fallback():
77
return html.div("I am NOT logged in!")
88

99

10+
def auth_check(user):
11+
return user.is_authenticated
12+
13+
14+
@user_passes_test(auth_check, fallback=my_component_fallback)
1015
@component
11-
@auth_required(fallback=my_component_fallback)
1216
def my_component():
1317
return html.div("I am logged in!")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from reactpy import component, html
2+
from reactpy_django.decorators import user_passes_test
3+
4+
5+
def auth_check(user):
6+
return user.is_authenticated
7+
8+
9+
@user_passes_test(auth_check, fallback=html.div("I am NOT logged in!"))
10+
@component
11+
def my_component():
12+
return html.div("I am logged in!")

docs/python/user-passes-test.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from reactpy import component, html
2+
from reactpy_django.decorators import user_passes_test
3+
4+
5+
def auth_check(user):
6+
return user.is_authenticated
7+
8+
9+
@user_passes_test(auth_check)
10+
@component
11+
def my_component():
12+
return html.div("I am logged in!")

docs/src/assets/css/banner.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
body[data-md-color-scheme="slate"] {
2-
--md-banner-bg-color: #4d4121;
2+
--md-banner-bg-color: rgb(55, 81, 78);
33
--md-banner-font-color: #fff;
44
}
55

docs/src/assets/css/main.css

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
--md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 16%, 0.26);
1818
--md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 16%, 0.07);
1919
--md-primary-fg-color: var(--md-default-bg-color);
20+
--md-default-fg-color: hsla(var(--md-hue), 75%, 95%, 1);
2021
--md-default-fg-color--light: #fff;
2122
--md-typeset-a-color: var(--reactpy-color);
2223
--md-accent-fg-color: var(--reactpy-color-dark);
@@ -38,7 +39,7 @@
3839
}
3940

4041
.md-typeset h1 {
41-
font-weight: 500;
42+
font-weight: 600;
4243
margin: 0;
4344
font-size: 2.5em;
4445
}
@@ -48,7 +49,7 @@
4849
}
4950

5051
.md-typeset h3 {
51-
font-weight: 600;
52+
font-weight: 400;
5253
}
5354

5455
/* Intro section styling */

docs/src/learn/add-reactpy-to-a-django-project.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,15 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`
7777
{% include "../../python/configure-asgi.py" %}
7878
```
7979

80-
??? info "Add `#!python AuthMiddlewareStack` and `#!python SessionMiddlewareStack` (Optional)"
80+
??? info "Add `#!python AuthMiddlewareStack` (Optional)"
8181

8282
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:
8383

8484
1. Access the `#!python User` that is currently logged in
85-
2. Login or logout the current `#!python User`
8685
3. Access Django's `#!python Session` object
86+
2. Login or logout the current `#!python User`
8787

88-
In these situations will need to ensure you are using `#!python AuthMiddlewareStack` and/or `#!python SessionMiddlewareStack`.
88+
In these situations will need to ensure you are using `#!python AuthMiddlewareStack`.
8989

9090
```python linenums="0"
9191
{% include "../../python/configure-asgi-middleware.py" start="# start" %}

0 commit comments

Comments
 (0)