diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index 11a7fa23..00172d4f 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -19,10 +19,11 @@ jobs: python-version: 3.x - name: Install dependencies run: pip install --upgrade pip hatch uv - - name: Publish Develop Docs + - name: Configure Git run: | git config user.name github-actions git config user.email github-actions@github.com - hatch run docs:deploy_develop + - name: Publish Develop Docs + run: hatch run docs:deploy_develop concurrency: group: publish-docs diff --git a/.github/workflows/publish-latest-docs.yml b/.github/workflows/publish-latest-docs.yml index 697b10da..41ced54d 100644 --- a/.github/workflows/publish-latest-docs.yml +++ b/.github/workflows/publish-latest-docs.yml @@ -19,10 +19,11 @@ jobs: python-version: 3.x - name: Install dependencies run: pip install --upgrade pip hatch uv - - name: Publish ${{ github.event.release.name }} Docs + - name: Configure Git run: | git config user.name github-actions git config user.email github-actions@github.com - hatch run docs:deploy_latest ${{ github.ref_name }} + - name: Publish ${{ github.event.release.name }} Docs + run: hatch run docs:deploy_latest ${{ github.ref_name }} concurrency: group: publish-docs diff --git a/.github/workflows/publish-py.yml b/.github/workflows/publish-python.yml similarity index 76% rename from .github/workflows/publish-py.yml rename to .github/workflows/publish-python.yml index ae1ace7f..93fbc969 100644 --- a/.github/workflows/publish-py.yml +++ b/.github/workflows/publish-python.yml @@ -1,6 +1,3 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - name: Publish Python on: diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 8faca864..0a7ae35a 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -11,7 +11,7 @@ on: - cron: "0 0 * * *" jobs: - python: + python-source: runs-on: ubuntu-latest strategy: matrix: @@ -29,6 +29,23 @@ jobs: run: pip install --upgrade pip hatch uv - name: Run Single DB Tests run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_single_db -v + + python-source-multi-db: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Use Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Python Dependencies + run: pip install --upgrade pip hatch uv - name: Run Multi-DB Tests run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_multi_db -v @@ -46,3 +63,18 @@ jobs: run: pip install --upgrade pip hatch uv - name: Check Python formatting run: hatch fmt src tests --check + + python-types: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install Python Dependencies + run: pip install --upgrade pip hatch uv + - name: Check Python formatting + run: hatch run python:type_check diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c848e4..4c72dd2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,11 +21,30 @@ Don't forget to remove deprecated code on each major release! - Nothing (yet)! +### [5.2.0] - 2024-12-29 + +### Added + +- User login/logout features! + - `reactpy_django.hooks.use_auth` to provide **persistent** `login` and `logout` functionality to your components. + - `settings.py:REACTPY_AUTH_TOKEN_MAX_AGE` to control the maximum seconds before ReactPy's login token expires. + - `settings.py:REACTPY_CLEAN_AUTH_TOKENS` to control whether ReactPy should clean up expired authentication tokens during automatic cleanups. +- Automatically convert Django forms to ReactPy forms via the new `reactpy_django.components.django_form` component! +- The ReactPy component tree can now be forcibly re-rendered via the new `reactpy_django.hooks.use_rerender` hook. + +### Changed + +- Refactoring of internal code to improve maintainability. No changes to publicly documented API. + +### Fixed + +- Fixed bug where pre-rendered components could generate a `SynchronousOnlyOperation` exception if they access a freshly logged out Django user object. + ## [5.1.1] - 2024-12-02 ### Fixed -- Fixed regression in v5.1.0 where components would sometimes not output debug messages when `settings.py:DEBUG` is enabled. +- Fixed regression from the previous release where components would sometimes not output debug messages when `settings.py:DEBUG` is enabled. ### Changed @@ -525,7 +544,8 @@ Don't forget to remove deprecated code on each major release! - Support for IDOM within the Django -[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/5.1.1...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/5.2.0...HEAD +[5.2.0]: https://github.com/reactive-python/reactpy-django/compare/5.1.1...5.2.0 [5.1.1]: https://github.com/reactive-python/reactpy-django/compare/5.1.0...5.1.1 [5.1.0]: https://github.com/reactive-python/reactpy-django/compare/5.0.0...5.1.0 [5.0.0]: https://github.com/reactive-python/reactpy-django/compare/4.0.0...5.0.0 diff --git a/README.md b/README.md index 89d1fb11..f60e7a2d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - [Multiple root components](https://reactive-python.github.io/reactpy-django/latest/reference/template-tag/) - [Cross-process communication/signaling](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#use-channel-layer) - [Django view to ReactPy component conversion](https://reactive-python.github.io/reactpy-django/latest/reference/components/#view-to-component) +- [Django form to ReactPy component conversion](https://reactive-python.github.io/reactpy-django/latest/reference/components/#django-form) - [Django static file access](https://reactive-python.github.io/reactpy-django/latest/reference/components/#django-css) - [Django database access](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#use-query) diff --git a/docs/examples/html/django_form_bootstrap.html b/docs/examples/html/django_form_bootstrap.html new file mode 100644 index 00000000..6aba84ca --- /dev/null +++ b/docs/examples/html/django_form_bootstrap.html @@ -0,0 +1,11 @@ +{% load django_bootstrap5 %} + + +{% bootstrap_css %} +{% bootstrap_javascript %} + + +{% bootstrap_form form %} +{% bootstrap_button button_type="submit" content="OK" %} +{% bootstrap_button button_type="reset" content="Reset" %} diff --git a/docs/examples/python/django_form.py b/docs/examples/python/django_form.py new file mode 100644 index 00000000..51960db1 --- /dev/null +++ b/docs/examples/python/django_form.py @@ -0,0 +1,10 @@ +from reactpy import component, html + +from example.forms import MyForm +from reactpy_django.components import django_form + + +@component +def basic_form(): + children = [html.input({"type": "submit"})] + return django_form(MyForm, bottom_children=children) diff --git a/docs/examples/python/django_form_bootstrap.py b/docs/examples/python/django_form_bootstrap.py new file mode 100644 index 00000000..449e1cc4 --- /dev/null +++ b/docs/examples/python/django_form_bootstrap.py @@ -0,0 +1,9 @@ +from reactpy import component + +from example.forms import MyForm +from reactpy_django.components import django_form + + +@component +def basic_form(): + return django_form(MyForm, form_template="bootstrap_form.html") diff --git a/docs/examples/python/django_form_class.py b/docs/examples/python/django_form_class.py new file mode 100644 index 00000000..e556295e --- /dev/null +++ b/docs/examples/python/django_form_class.py @@ -0,0 +1,5 @@ +from django import forms + + +class MyForm(forms.Form): + username = forms.CharField(label="Username") diff --git a/docs/examples/python/django_form_on_success.py b/docs/examples/python/django_form_on_success.py new file mode 100644 index 00000000..d8b6927c --- /dev/null +++ b/docs/examples/python/django_form_on_success.py @@ -0,0 +1,21 @@ +from reactpy import component, hooks, html +from reactpy_router import navigate + +from example.forms import MyForm +from reactpy_django.components import django_form +from reactpy_django.types import FormEventData + + +@component +def basic_form(): + submitted, set_submitted = hooks.use_state(False) + + def on_submit(event: FormEventData): + """This function will be called when the form is successfully submitted.""" + set_submitted(True) + + if submitted: + return navigate("/homepage") + + children = [html.input({"type": "submit"})] + return django_form(MyForm, on_success=on_submit, bottom_children=children) diff --git a/docs/examples/python/example/forms.py b/docs/examples/python/example/forms.py new file mode 100644 index 00000000..8d3eefc0 --- /dev/null +++ b/docs/examples/python/example/forms.py @@ -0,0 +1,4 @@ +from django import forms + + +class MyForm(forms.Form): ... diff --git a/docs/examples/python/example/models.py b/docs/examples/python/example/models.py new file mode 100644 index 00000000..2bf062b9 --- /dev/null +++ b/docs/examples/python/example/models.py @@ -0,0 +1,4 @@ +from django.db import models + + +class TodoItem(models.Model): ... diff --git a/docs/examples/python/pyscript_ffi.py b/docs/examples/python/pyscript_ffi.py new file mode 100644 index 00000000..d744dd88 --- /dev/null +++ b/docs/examples/python/pyscript_ffi.py @@ -0,0 +1,14 @@ +from pyscript import document, window +from reactpy import component, html + + +@component +def root(): + def on_click(event): + my_element = document.querySelector("#example") + my_element.innerText = window.location.hostname + + return html.div( + {"id": "example"}, + html.button({"onClick": on_click}, "Click Me!"), + ) diff --git a/docs/examples/python/use_auth.py b/docs/examples/python/use_auth.py new file mode 100644 index 00000000..2bb1bcbb --- /dev/null +++ b/docs/examples/python/use_auth.py @@ -0,0 +1,23 @@ +from django.contrib.auth import get_user_model +from reactpy import component, html + +from reactpy_django.hooks import use_auth, use_user + + +@component +def my_component(): + auth = use_auth() + user = use_user() + + async def login_user(event): + new_user, _created = await get_user_model().objects.aget_or_create(username="ExampleUser") + await auth.login(new_user) + + async def logout_user(event): + await auth.logout() + + return html.div( + f"Current User: {user}", + html.button({"onClick": login_user}, "Login"), + html.button({"onClick": logout_user}, "Logout"), + ) diff --git a/docs/examples/python/use_rerender.py b/docs/examples/python/use_rerender.py new file mode 100644 index 00000000..cd160e17 --- /dev/null +++ b/docs/examples/python/use_rerender.py @@ -0,0 +1,15 @@ +from uuid import uuid4 + +from reactpy import component, html + +from reactpy_django.hooks import use_rerender + + +@component +def my_component(): + rerender = use_rerender() + + def on_click(): + rerender() + + return html.div(f"UUID: {uuid4()}", html.button({"onClick": on_click}, "Rerender")) diff --git a/docs/includes/auth-middleware-stack.md b/docs/includes/auth-middleware-stack.md new file mode 100644 index 00000000..7cc0c7f8 --- /dev/null +++ b/docs/includes/auth-middleware-stack.md @@ -0,0 +1,3 @@ +```python linenums="0" +{% include "../examples/python/configure_asgi_middleware.py" start="# start" %} +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 100b669b..bde5ac08 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -124,6 +124,6 @@ site_description: It's React, but in Python. Now with Django integration. copyright: '©
Reactive Python and affiliates. ' repo_url: https://github.com/reactive-python/reactpy-django site_url: https://reactive-python.github.io/reactpy-django -repo_name: ReactPy Django (GitHub) +repo_name: ReactPy Django edit_uri: edit/main/docs/src docs_dir: src diff --git a/docs/src/about/contributing.md b/docs/src/about/contributing.md index 59f4f989..b78a9508 100644 --- a/docs/src/about/contributing.md +++ b/docs/src/about/contributing.md @@ -62,6 +62,7 @@ By utilizing `hatch`, the following commands are available to manage the develop | `hatch fmt --formatter` | Run only formatters | | `hatch run javascript:check` | Run the JavaScript linter/formatter | | `hatch run javascript:fix` | Run the JavaScript linter/formatter and write fixes to disk | +| `hatch run python:type_check` | Run the Python type checker | ??? tip "Configure your IDE for linting" diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 1b4ce080..d2ff722d 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -48,3 +48,4 @@ linter linters linting formatters +bootstrap_form diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md index 407fe61d..371893e1 100644 --- a/docs/src/learn/add-reactpy-to-a-django-project.md +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -87,9 +87,7 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [` In these situations will need to ensure you are using `#!python AuthMiddlewareStack`. - ```python linenums="0" - {% include "../../examples/python/configure_asgi_middleware.py" start="# start" %} - ``` + {% include "../../includes/auth-middleware-stack.md" %} ??? question "Where is my `asgi.py`?" diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index 4186af42..26feda67 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -156,7 +156,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ??? info "Existing limitations" - There are currently several limitations of using `#!python view_to_component` that may be resolved in a future version. + There are currently several limitations of using `#!python view_to_component` that will be [resolved in a future version](https://github.com/reactive-python/reactpy-django/issues/269). - Requires manual intervention to change HTTP methods to anything other than `GET`. - ReactPy events cannot conveniently be attached to converted view HTML. @@ -292,12 +292,12 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ??? info "Existing limitations" - There are currently several limitations of using `#!python view_to_iframe` that may be resolved in a future version. + There are currently several limitations of using `#!python view_to_iframe` which may be [resolved in a future version](https://github.com/reactive-python/reactpy-django/issues/268). - No built-in method of signalling events back to the parent component. - - All provided `#!python *args` and `#!python *kwargs` must be serializable values, since they are encoded into the URL. + - All provided `#!python args` and `#!python kwargs` must be serializable values, since they are encoded into the URL. - The `#!python iframe` will always load **after** the parent component. - - CSS styling for `#!python iframe` elements tends to be awkward/difficult. + - CSS styling for `#!python iframe` elements tends to be awkward. ??? question "How do I use this for Class Based Views?" @@ -381,6 +381,104 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. --- +## Django Form + +Automatically convert a Django form into a ReactPy component. + +Compatible with both [standard Django forms](https://docs.djangoproject.com/en/stable/topics/forms/#building-a-form) and [ModelForms](https://docs.djangoproject.com/en/stable/topics/forms/modelforms/). + +=== "components.py" + + ```python + {% include "../../examples/python/django_form.py" %} + ``` + +=== "forms.py" + + ```python + {% include "../../examples/python/django_form_class.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python form` | `#!python type[Form | ModelForm]` | The form to convert. | N/A | + | `#!python on_success` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form is successfully submitted. | `#!python None` | + | `#!python on_error` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form submission fails. | `#!python None` | + | `#!python on_receive_data` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called before newly submitted form data is rendered. | `#!python None` | + | `#!python on_change` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when a form field is modified by the user. | `#!python None` | + | `#!python auto_save` | `#!python bool` | If `#!python True`, the form will automatically call `#!python save` on successful submission of a `#!python ModelForm`. This has no effect on regular `#!python Form` instances. | `#!python True` | + | `#!python extra_props` | `#!python dict[str, Any] | None` | Additional properties to add to the `#!html