Skip to content

Commit 8cbee2a

Browse files
committed
fix #272
1 parent c5930dd commit 8cbee2a

3 files changed

Lines changed: 82 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ Don't forget to remove deprecated code on each major release!
4949
- Removed `reactpy_django/html.py` (`pyscript` VDOM constructor). Use `html.py_script` instead.
5050
- Removed `nest_asyncio` dependency and its initialization on startup.
5151

52+
### Fixed
53+
54+
- Resolved bug where `django_form` events would sometimes not occur.
55+
5256
### [5.2.1] - 2025-01-10
5357

5458
### Changed

src/reactpy_django/forms/components.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from asyncio import iscoroutinefunction
34
from logging import getLogger
45
from pathlib import Path
56
from typing import TYPE_CHECKING, Any, Callable, Union, cast
@@ -65,6 +66,14 @@ def _django_form(
6566
form=initialized_form, submitted_data=submitted_data or {}, set_submitted_data=set_submitted_data
6667
)
6768

69+
def _call_callback(callback, event_data):
70+
"""Call a form event callback, running sync callbacks directly
71+
(not through database_sync_to_async) to avoid issues with ReactPy state
72+
setters that require an active event loop."""
73+
if iscoroutinefunction(callback):
74+
return callback(event_data)
75+
return callback(event_data)
76+
6877
# Validate and render the form
6978
@hooks.use_async_effect(dependencies=[str(submitted_data)])
7079
async def render_form():
@@ -76,9 +85,13 @@ async def render_form():
7685
await ensure_async(initialized_form.full_clean, thread_sensitive=thread_sensitive)()
7786
success = not initialized_form.errors.as_data()
7887
if success and on_success:
79-
await ensure_async(on_success, thread_sensitive=thread_sensitive)(form_event)
88+
result = _call_callback(on_success, form_event)
89+
if iscoroutinefunction(on_success):
90+
await result
8091
if not success and on_error:
81-
await ensure_async(on_error, thread_sensitive=thread_sensitive)(form_event)
92+
result = _call_callback(on_error, form_event)
93+
if iscoroutinefunction(on_error):
94+
await result
8295
if success and auto_save and isinstance(initialized_form, ModelForm):
8396
await ensure_async(initialized_form.save)()
8497
set_submitted_data(None)
@@ -101,15 +114,19 @@ async def on_submit_callback(new_data: dict[str, Any]):
101114
new_form_event = FormEventData(
102115
form=initialized_form, submitted_data=new_data, set_submitted_data=set_submitted_data
103116
)
104-
await ensure_async(on_receive_data, thread_sensitive=thread_sensitive)(new_form_event)
117+
result = _call_callback(on_receive_data, new_form_event)
118+
if iscoroutinefunction(on_receive_data):
119+
await result
105120

106121
if submitted_data != new_data:
107122
set_submitted_data(new_data)
108123

109124
async def _on_change(_event):
110125
"""Event that exist solely to allow the user to detect form changes."""
111126
if on_change:
112-
await ensure_async(on_change, thread_sensitive=thread_sensitive)(form_event)
127+
result = _call_callback(on_change, form_event)
128+
if iscoroutinefunction(on_change):
129+
await result
113130

114131
if not rendered_form:
115132
return None

tests/test_app/tests/test_components.py

Lines changed: 57 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -859,63 +859,60 @@ def test_form_orm_model(self):
859859
finally:
860860
os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE")
861861

862-
# FIXME: Re-enable once we fix the following:
863-
# https://github.com/reactive-python/reactpy-django/issues/272
864-
865-
# @navigate_to_page("/form/sync_event/")
866-
# def test_form_sync_events(self):
867-
# self.page.wait_for_selector("form")
868-
869-
# # Check initial state
870-
# self.page.wait_for_selector("#success[data-value='false']")
871-
# self.page.wait_for_selector("#error[data-value='false']")
872-
# self.page.wait_for_selector("#receive_data[data-value='false']")
873-
# self.page.wait_for_selector("#change[data-value='false']")
874-
875-
# # Submit empty the form
876-
# self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
877-
878-
# # The empty form was submitted, should result in an error
879-
# self.page.wait_for_selector("#success[data-value='false']")
880-
# self.page.wait_for_selector("#error[data-value='true']")
881-
# self.page.wait_for_selector("#receive_data[data-value='true']")
882-
# self.page.wait_for_selector("#change[data-value='false']")
883-
884-
# # Fill out the form and re-submit
885-
# self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
886-
# self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
887-
888-
# # Form should have been successfully submitted
889-
# self.page.wait_for_selector("#success[data-value='true']")
890-
# self.page.wait_for_selector("#error[data-value='true']")
891-
# self.page.wait_for_selector("#receive_data[data-value='true']")
892-
# self.page.wait_for_selector("#change[data-value='true']")
893-
894-
# @navigate_to_page("/form/async_event/")
895-
# def test_form_async_events(self):
896-
# self.page.wait_for_selector("form")
897-
898-
# # Check initial state
899-
# self.page.wait_for_selector("#success[data-value='false']")
900-
# self.page.wait_for_selector("#error[data-value='false']")
901-
# self.page.wait_for_selector("#receive_data[data-value='false']")
902-
# self.page.wait_for_selector("#change[data-value='false']")
903-
904-
# # Submit empty the form
905-
# self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
906-
907-
# # The empty form was submitted, should result in an error
908-
# self.page.wait_for_selector("#success[data-value='false']")
909-
# self.page.wait_for_selector("#error[data-value='true']")
910-
# self.page.wait_for_selector("#receive_data[data-value='true']")
911-
# self.page.wait_for_selector("#change[data-value='false']")
912-
913-
# # Fill out the form and re-submit
914-
# self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
915-
# self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
916-
917-
# # Form should have been successfully submitted
918-
# self.page.wait_for_selector("#success[data-value='true']")
919-
# self.page.wait_for_selector("#error[data-value='true']")
920-
# self.page.wait_for_selector("#receive_data[data-value='true']")
921-
# self.page.wait_for_selector("#change[data-value='true']")
862+
@navigate_to_page("/form/sync_event/")
863+
def test_form_sync_events(self):
864+
self.page.wait_for_selector("form")
865+
866+
# Check initial state
867+
self.page.wait_for_selector("#success[data-value='false']")
868+
self.page.wait_for_selector("#error[data-value='false']")
869+
self.page.wait_for_selector("#receive_data[data-value='false']")
870+
self.page.wait_for_selector("#change[data-value='false']")
871+
872+
# Submit empty the form
873+
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
874+
875+
# The empty form was submitted, should result in an error
876+
self.page.wait_for_selector("#success[data-value='false']")
877+
self.page.wait_for_selector("#error[data-value='true']")
878+
self.page.wait_for_selector("#receive_data[data-value='true']")
879+
self.page.wait_for_selector("#change[data-value='false']")
880+
881+
# Fill out the form and re-submit
882+
self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
883+
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
884+
885+
# Form should have been successfully submitted
886+
self.page.wait_for_selector("#success[data-value='true']")
887+
self.page.wait_for_selector("#error[data-value='true']")
888+
self.page.wait_for_selector("#receive_data[data-value='true']")
889+
self.page.wait_for_selector("#change[data-value='true']")
890+
891+
@navigate_to_page("/form/async_event/")
892+
def test_form_async_events(self):
893+
self.page.wait_for_selector("form")
894+
895+
# Check initial state
896+
self.page.wait_for_selector("#success[data-value='false']")
897+
self.page.wait_for_selector("#error[data-value='false']")
898+
self.page.wait_for_selector("#receive_data[data-value='false']")
899+
self.page.wait_for_selector("#change[data-value='false']")
900+
901+
# Submit empty the form
902+
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
903+
904+
# The empty form was submitted, should result in an error
905+
self.page.wait_for_selector("#success[data-value='false']")
906+
self.page.wait_for_selector("#error[data-value='true']")
907+
self.page.wait_for_selector("#receive_data[data-value='true']")
908+
self.page.wait_for_selector("#change[data-value='false']")
909+
910+
# Fill out the form and re-submit
911+
self.page.wait_for_selector("#id_char_field").type("test", delay=DELAY)
912+
self.page.wait_for_selector("input[type=submit]").click(delay=DELAY)
913+
914+
# Form should have been successfully submitted
915+
self.page.wait_for_selector("#success[data-value='true']")
916+
self.page.wait_for_selector("#error[data-value='true']")
917+
self.page.wait_for_selector("#receive_data[data-value='true']")
918+
self.page.wait_for_selector("#change[data-value='true']")

0 commit comments

Comments
 (0)