Skip to content

Commit

Permalink
UserModel related hooks, decorators, and settings (#190)
Browse files Browse the repository at this point in the history
-   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/).
  • Loading branch information
Archmonger authored Jan 7, 2024
1 parent 6f79c4c commit b307b3e
Show file tree
Hide file tree
Showing 59 changed files with 1,165 additions and 356 deletions.
34 changes: 25 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,38 @@ Using the following categories, list your changes in this order:

### Added

- ReactPy components can now use SEO compatible rendering!
- `settings.py:REACTPY_PRERENDER` can be set to `True` to enable this behavior by default
- Or, you can enable it on individual components via the template tag: `{% component "..." prerender="True" %}`
- `reactpy_django.components.view_to_iframe` component has been added, which uses an `<iframe>` to render a Django view.
- `reactpy_django.utils.register_iframe` function has been added, which is mandatory to use alongside `reactpy_django.components.view_to_iframe`.
- SEO compatible rendering!
- `settings.py:REACTPY_PRERENDER` can be set to `True` to make components pre-render by default.
- Or, you can enable it on individual components via the template tag: `{% component "..." prerender="True" %}`.
- New `view_to_iframe` feature!
- `reactpy_django.components.view_to_iframe` uses an `<iframe>` to render a Django view.
- `reactpy_django.utils.register_iframe` tells ReactPy which views `view_to_iframe` can use.
- 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 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.
- `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/).

### Changed

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

### Deprecated

- The `compatibility` argument on `reactpy_django.components.view_to_component` is deprecated. Use `reactpy_django.components.view_to_iframe` instead.
- Using `reactpy_django.components.view_to_component` as a decorator is deprecated. Check the docs on the new suggested usage.
- The `compatibility` argument on `reactpy_django.components.view_to_component` is deprecated.
- Use `view_to_iframe` as a replacement.
- `reactpy_django.components.view_to_component` **usage as a decorator** is deprecated.
- Check the docs on how to use `view_to_component` as a function instead.
- `reactpy_django.decorators.auth_required` is deprecated.
- Use `reactpy_django.decorators.user_passes_test` instead.
- An equivalent to `auth_required`'s default is `@user_passes_test(lambda user: user.is_active)`.

### Fixed

- Fixed a bug where exception stacks would not print on failed component renders.

## [3.5.1] - 2023-09-07

Expand Down
3 changes: 3 additions & 0 deletions docs/overrides/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ <h1>{{ config.site_name }}</h1>
<a href="{{ 'reference/components/' | url }}" class="md-button">
API Reference
</a>
<a href="{{ 'about/changelog/' | url }}" class="md-button">
Changelog
</a>
</div>
</div>

Expand Down
8 changes: 0 additions & 8 deletions docs/python/auth-required-attribute.py

This file was deleted.

7 changes: 0 additions & 7 deletions docs/python/auth-required-custom-attribute-model.py

This file was deleted.

8 changes: 0 additions & 8 deletions docs/python/auth-required-custom-attribute.py

This file was deleted.

8 changes: 0 additions & 8 deletions docs/python/auth-required-vdom-fallback.py

This file was deleted.

8 changes: 0 additions & 8 deletions docs/python/auth-required.py

This file was deleted.

9 changes: 3 additions & 6 deletions docs/python/configure-asgi-middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@

# start
from channels.auth import AuthMiddlewareStack # noqa: E402
from channels.sessions import SessionMiddlewareStack # noqa: E402

application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": SessionMiddlewareStack(
AuthMiddlewareStack(
URLRouter(
[REACTPY_WEBSOCKET_ROUTE],
)
"websocket": AuthMiddlewareStack(
URLRouter(
[REACTPY_WEBSOCKET_ROUTE],
)
),
}
Expand Down
5 changes: 3 additions & 2 deletions docs/python/use-connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@

@component
def my_component():
my_connection = use_connection()
return html.div(str(my_connection))
connection = use_connection()

return html.div(str(connection))
5 changes: 3 additions & 2 deletions docs/python/use-location.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@

@component
def my_component():
my_location = use_location()
return html.div(str(my_location))
location = use_location()

return html.div(str(location))
2 changes: 1 addition & 1 deletion docs/python/use-mutation-args-kwargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ def example_mutation(value: int, other_value: bool = False):
def my_component():
mutation = use_mutation(example_mutation)

mutation.execute(123, other_value=True)
mutation(123, other_value=True)

...
4 changes: 2 additions & 2 deletions docs/python/use-mutation-query-refetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def todo_list():

def submit_event(event):
if event["key"] == "Enter":
item_mutation.execute(text=event["target"]["value"])
item_mutation(text=event["target"]["value"])

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

return html.div(
html.label("Add an item:"),
html.input({"type": "text", "onKeyDown": submit_event}),
html.input({"type": "text", "on_key_down": submit_event}),
mutation_status,
rendered_items,
)
6 changes: 3 additions & 3 deletions docs/python/use-mutation-reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ def reset_event(event):

def submit_event(event):
if event["key"] == "Enter":
item_mutation.execute(text=event["target"]["value"])
item_mutation(text=event["target"]["value"])

if item_mutation.loading:
mutation_status = html.h2("Adding...")
elif item_mutation.error:
mutation_status = html.button({"onClick": reset_event}, "Error: Try again!")
mutation_status = html.button({"on_click": reset_event}, "Error: Try again!")
else:
mutation_status = html.h2("Mutation done.")

return html.div(
html.label("Add an item:"),
html.input({"type": "text", "onKeyDown": submit_event}),
html.input({"type": "text", "on_key_down": submit_event}),
mutation_status,
)
4 changes: 2 additions & 2 deletions docs/python/use-mutation-thread-sensitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def my_component():

def submit_event(event):
if event["key"] == "Enter":
item_mutation.execute(text=event["target"]["value"])
item_mutation(text=event["target"]["value"])

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

return html.div(
html.input({"type": "text", "onKeyDown": submit_event}),
html.input({"type": "text", "on_key_down": submit_event}),
mutation_status,
)
4 changes: 2 additions & 2 deletions docs/python/use-mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def todo_list():

def submit_event(event):
if event["key"] == "Enter":
item_mutation.execute(text=event["target"]["value"])
item_mutation(text=event["target"]["value"])

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

return html.div(
html.label("Add an item:"),
html.input({"type": "text", "onKeyDown": submit_event}),
html.input({"type": "text", "on_key_down": submit_event}),
mutation_status,
)
5 changes: 3 additions & 2 deletions docs/python/use-origin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@

@component
def my_component():
my_origin = use_origin()
return html.div(my_origin or "No origin")
origin = use_origin()

return html.div(origin or "No origin")
5 changes: 3 additions & 2 deletions docs/python/use-scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@

@component
def my_component():
my_scope = use_scope()
return html.div(str(my_scope))
scope = use_scope()

return html.div(str(scope))
25 changes: 25 additions & 0 deletions docs/python/use-user-data-defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from reactpy import component, html
from reactpy_django.hooks import use_user_data


@component
def my_component():
user_data = use_user_data(
default_data={
"basic_example": "123",
"computed_example_sync": sync_default,
"computed_example_async": async_default,
}
)

return html.div(
html.div(f"Data: {user_data.query.data}"),
)


def sync_default():
return ...


async def async_default():
return ...
19 changes: 19 additions & 0 deletions docs/python/use-user-data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from reactpy import component, html
from reactpy_django.hooks import use_user_data


@component
def my_component():
query, mutation = use_user_data()

def on_submit(event):
if event["key"] == "Enter" and query.data:
new_key = str(len(query.data))
mutation({**query.data, new_key: event["target"]["value"]})

return html.div(
html.div(f"Data: {query.data}"),
html.div(f"Loading: {query.loading | mutation.loading}"),
html.div(f"Error(s): {query.error} {mutation.error}"),
html.input({"on_key_press": on_submit}),
)
9 changes: 9 additions & 0 deletions docs/python/use-user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from reactpy import component, html
from reactpy_django.hooks import use_user


@component
def my_component():
user = use_user()

return html.div(user.username)
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from reactpy import component, html
from reactpy_django.decorators import auth_required
from reactpy_django.decorators import user_passes_test


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


def auth_check(user):
return user.is_authenticated


@user_passes_test(auth_check, fallback=my_component_fallback)
@component
@auth_required(fallback=my_component_fallback)
def my_component():
return html.div("I am logged in!")
12 changes: 12 additions & 0 deletions docs/python/user-passes-test-vdom-fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from reactpy import component, html
from reactpy_django.decorators import user_passes_test


def auth_check(user):
return user.is_authenticated


@user_passes_test(auth_check, fallback=html.div("I am NOT logged in!"))
@component
def my_component():
return html.div("I am logged in!")
12 changes: 12 additions & 0 deletions docs/python/user-passes-test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from reactpy import component, html
from reactpy_django.decorators import user_passes_test


def auth_check(user):
return user.is_authenticated


@user_passes_test(auth_check)
@component
def my_component():
return html.div("I am logged in!")
2 changes: 1 addition & 1 deletion docs/src/assets/css/banner.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
body[data-md-color-scheme="slate"] {
--md-banner-bg-color: #4d4121;
--md-banner-bg-color: rgb(55, 81, 78);
--md-banner-font-color: #fff;
}

Expand Down
5 changes: 3 additions & 2 deletions docs/src/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
--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-primary-fg-color: var(--md-default-bg-color);
--md-default-fg-color: hsla(var(--md-hue), 75%, 95%, 1);
--md-default-fg-color--light: #fff;
--md-typeset-a-color: var(--reactpy-color);
--md-accent-fg-color: var(--reactpy-color-dark);
Expand All @@ -38,7 +39,7 @@
}

.md-typeset h1 {
font-weight: 500;
font-weight: 600;
margin: 0;
font-size: 2.5em;
}
Expand All @@ -48,7 +49,7 @@
}

.md-typeset h3 {
font-weight: 600;
font-weight: 400;
}

/* Intro section styling */
Expand Down
6 changes: 3 additions & 3 deletions docs/src/learn/add-reactpy-to-a-django-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`
{% include "../../python/configure-asgi.py" %}
```

??? info "Add `#!python AuthMiddlewareStack` and `#!python SessionMiddlewareStack` (Optional)"
??? info "Add `#!python AuthMiddlewareStack` (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
2. Login or logout the current `#!python User`

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

```python linenums="0"
{% include "../../python/configure-asgi-middleware.py" start="# start" %}
Expand Down
Loading

0 comments on commit b307b3e

Please sign in to comment.