Skip to content

Commit f0b21c7

Browse files
committed
Full support for database backed forms
1 parent cc4fd22 commit f0b21c7

File tree

8 files changed

+66
-25
lines changed

8 files changed

+66
-25
lines changed

src/reactpy_django/components.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
if TYPE_CHECKING:
2828
from collections.abc import Sequence
2929

30-
from django.forms import Form
30+
from django.forms import Form, ModelForm
3131
from django.views import View
3232

3333
from reactpy_django.types import FormEvent
@@ -119,13 +119,14 @@ def django_js(static_path: str, key: Key | None = None):
119119

120120

121121
def django_form(
122-
form: type[Form],
122+
form: type[Form | ModelForm],
123123
*,
124124
extra_props: dict[str, Any] | None = None,
125125
on_success: Callable[[FormEvent], None] | None = None,
126126
on_error: Callable[[FormEvent], None] | None = None,
127127
on_submit: Callable[[FormEvent], None] | None = None,
128128
on_change: Callable[[FormEvent], None] | None = None,
129+
auto_save: bool = True,
129130
form_template: str | None = None,
130131
top_children: Sequence = (),
131132
bottom_children: Sequence = (),
@@ -138,6 +139,7 @@ def django_form(
138139
on_error=on_error,
139140
on_submit=on_submit,
140141
on_change=on_change,
142+
auto_save=auto_save,
141143
form_template=form_template,
142144
top_children=top_children,
143145
bottom_children=bottom_children,

src/reactpy_django/forms/components.py

+17-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from uuid import uuid4
66

77
from channels.db import database_sync_to_async
8-
from django.forms import Form
8+
from django.forms import Form, ModelForm
99
from reactpy import component, hooks, html, utils
1010
from reactpy.core.events import event
1111
from reactpy.web import export, module_from_file
@@ -31,12 +31,13 @@
3131

3232
@component
3333
def _django_form(
34-
form: type[Form],
34+
form: type[Form | ModelForm],
3535
extra_props: dict,
3636
on_success: Callable[[FormEvent], None] | None,
3737
on_error: Callable[[FormEvent], None] | None,
3838
on_submit: Callable[[FormEvent], None] | None,
3939
on_change: Callable[[FormEvent], None] | None,
40+
auto_save: bool,
4041
form_template: str | None,
4142
top_children: Sequence,
4243
bottom_children: Sequence,
@@ -46,7 +47,6 @@ def _django_form(
4647
# Perhaps pre-rendering is robust enough already handle this scenario?
4748
# Additionally, "URL" mode would limit the user to one form per page.
4849
# TODO: Test this with django-colorfield, django-ace, django-crispy-forms
49-
# TODO: Add auto-save option for database-backed forms
5050
uuid_ref = hooks.use_ref(uuid4().hex.replace("-", ""))
5151
top_children_count = hooks.use_ref(len(top_children))
5252
bottom_children_count = hooks.use_ref(len(bottom_children))
@@ -59,20 +59,17 @@ def _django_form(
5959
msg = "Dynamically changing the number of top or bottom children is not allowed."
6060
raise ValueError(msg)
6161

62+
# Ensure the provided form is a Django Form
63+
if not isinstance(form, (type(Form), type(ModelForm))):
64+
msg = (
65+
"The provided form must be an uninitialized Django Form. "
66+
"Do NOT initialize your form by calling it (ex. `MyForm()`)."
67+
)
68+
raise TypeError(msg)
69+
6270
# Try to initialize the form with the provided data
63-
try:
64-
initialized_form = form(data=submitted_data)
65-
except Exception as e:
66-
if not isinstance(form, type(Form)):
67-
msg = (
68-
"The provided form must be an uninitialized Django Form. "
69-
"Do NOT initialize your form by calling it (ex. `MyForm()`)."
70-
)
71-
raise TypeError(msg) from e
72-
raise
73-
74-
# Set up the form event object
75-
form_event = FormEvent(form=initialized_form, data=submitted_data or {})
71+
initialized_form = form(data=submitted_data)
72+
form_event = FormEvent(form=initialized_form, data=submitted_data or {}, set_data=set_submitted_data)
7673

7774
# Validate and render the form
7875
@hooks.use_effect
@@ -85,6 +82,9 @@ async def render_form():
8582
on_success(form_event)
8683
if not success and on_error:
8784
on_error(form_event)
85+
if success and auto_save and isinstance(initialized_form, ModelForm):
86+
await database_sync_to_async(initialized_form.save)()
87+
set_submitted_data(None)
8888

8989
set_rendered_form(await database_sync_to_async(initialized_form.render)(form_template))
9090

@@ -99,7 +99,7 @@ def on_submit_callback(new_data: dict[str, Any]):
9999
convert_boolean_fields(new_data, initialized_form)
100100

101101
if on_submit:
102-
on_submit(FormEvent(form=initialized_form, data=new_data))
102+
on_submit(FormEvent(form=initialized_form, data=new_data, set_data=set_submitted_data))
103103

104104
# TODO: The `use_state`` hook really should be de-duplicating this by itself. Needs upstream fix.
105105
if submitted_data != new_data:

src/reactpy_django/forms/utils.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from __future__ import annotations
2+
13
from typing import Any
24

3-
from django.forms import BooleanField, Form, ModelMultipleChoiceField, MultipleChoiceField, NullBooleanField
5+
from django.forms import BooleanField, Form, ModelForm, ModelMultipleChoiceField, MultipleChoiceField, NullBooleanField
46

57

6-
def convert_multiple_choice_fields(data: dict[str, Any], initialized_form: Form) -> None:
8+
def convert_multiple_choice_fields(data: dict[str, Any], initialized_form: Form | ModelForm) -> None:
79
multi_choice_fields = {
810
field_name
911
for field_name, field in initialized_form.fields.items()
@@ -16,7 +18,7 @@ def convert_multiple_choice_fields(data: dict[str, Any], initialized_form: Form)
1618
data[choice_field_name] = [data[choice_field_name]]
1719

1820

19-
def convert_boolean_fields(data: dict[str, Any], initialized_form: Form) -> None:
21+
def convert_boolean_fields(data: dict[str, Any], initialized_form: Form | ModelForm) -> None:
2022
boolean_fields = {
2123
field_name
2224
for field_name, field in initialized_form.fields.items()

src/reactpy_django/types.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
if TYPE_CHECKING:
2020
from collections.abc import MutableMapping, Sequence
2121

22-
from django.forms import Form
22+
from django.forms import Form, ModelForm
2323

2424
from reactpy_django.websocket.consumer import ReactpyAsyncWebsocketConsumer
2525

@@ -57,8 +57,9 @@ def __call__(self, *args: FuncParams.args, **kwargs: FuncParams.kwargs) -> None:
5757
class FormEvent:
5858
"""State of a form provided to Form custom events."""
5959

60-
form: Form
60+
form: Form | ModelForm
6161
data: dict[str, Any]
62+
set_data: Callable[[dict[str, Any] | None], None]
6263

6364

6465
class AsyncPostprocessor(Protocol):

tests/test_app/forms/components.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from reactpy_django.components import django_form
44

5-
from .forms import BasicForm
5+
from .forms import BasicForm, DatabaseBackedForm
66

77

88
@component
@@ -17,3 +17,8 @@ def bootstrap_form():
1717
extra_props={"style": {"maxWidth": "600px", "margin": "auto"}},
1818
form_template="bootstrap_form_template.html",
1919
)
20+
21+
22+
@component
23+
def database_backed_form():
24+
return django_form(DatabaseBackedForm, bottom_children=(html.input({"type": "submit"}),))

tests/test_app/forms/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
urlpatterns = [
66
path("form/", views.form),
77
path("form/bootstrap/", views.bootstrap_form),
8+
path("form/database/", views.databased_backed_form),
89
]

tests/test_app/forms/views.py

+4
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ def form(request):
77

88
def bootstrap_form(request):
99
return render(request, "bootstrap_form.html", {})
10+
11+
12+
def databased_backed_form(request):
13+
return render(request, "database_backed_form.html", {})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{% load static %} {% load reactpy %}
2+
<!DOCTYPE html>
3+
<html lang="en">
4+
5+
<head>
6+
<meta charset="UTF-8" />
7+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
8+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
9+
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}" />
10+
<title>ReactPy</title>
11+
<style>
12+
iframe {
13+
width: 100%;
14+
height: 45px;
15+
}
16+
</style>
17+
</head>
18+
19+
<body>
20+
<h1>ReactPy Database Form Test Page</h1>
21+
<hr>
22+
{% component "test_app.forms.components.database_backed_form" %}
23+
<hr>
24+
</body>
25+
26+
</html>

0 commit comments

Comments
 (0)