From 1955667a7779e2dc716aa68cad4f1be0f9ada35e Mon Sep 17 00:00:00 2001 From: amirreza Date: Mon, 24 Feb 2025 23:53:19 +0330 Subject: [PATCH 1/4] added an async constructor when the `instance` argument is provided, database calls are made which will error in an async env, this can be used instead --- django_async_extensions/aforms/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/django_async_extensions/aforms/models.py b/django_async_extensions/aforms/models.py index 8edf524..b188d3d 100644 --- a/django_async_extensions/aforms/models.py +++ b/django_async_extensions/aforms/models.py @@ -6,6 +6,10 @@ class AsyncModelForm(ModelForm): + @classmethod + async def from_async(cls, *args, **kwargs): + return await sync_to_async(cls)(*args, **kwargs) + async def _asave_m2m(self): """ Save the many-to-many fields and generic relations for this form. From 77e57cd19a6751ad7b742aa60262ac6df5318849 Mon Sep 17 00:00:00 2001 From: amirreza Date: Mon, 24 Feb 2025 23:54:14 +0330 Subject: [PATCH 2/4] added async ways to clean and validate the forms --- django_async_extensions/aforms/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/django_async_extensions/aforms/models.py b/django_async_extensions/aforms/models.py index b188d3d..4324c2a 100644 --- a/django_async_extensions/aforms/models.py +++ b/django_async_extensions/aforms/models.py @@ -10,6 +10,18 @@ class AsyncModelForm(ModelForm): async def from_async(cls, *args, **kwargs): return await sync_to_async(cls)(*args, **kwargs) + @property + async def aerrors(self): + if self._errors is None: + await self.afull_clean() + return self._errors + + async def ais_valid(self): + return self.is_bound and not await self.aerrors + + async def afull_clean(self): + return await sync_to_async(self.full_clean)() + async def _asave_m2m(self): """ Save the many-to-many fields and generic relations for this form. From 3e7e4a85119aaf51344cec5aac6c995469ec1c5b Mon Sep 17 00:00:00 2001 From: amirreza Date: Mon, 24 Feb 2025 23:57:52 +0330 Subject: [PATCH 3/4] added async rendering methods these are supposed to be used in views, can not work in templates for now --- django_async_extensions/aforms/models.py | 4 ++- django_async_extensions/aforms/utils.py | 31 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 django_async_extensions/aforms/utils.py diff --git a/django_async_extensions/aforms/models.py b/django_async_extensions/aforms/models.py index 4324c2a..cef0ede 100644 --- a/django_async_extensions/aforms/models.py +++ b/django_async_extensions/aforms/models.py @@ -4,8 +4,10 @@ from django.forms.models import ModelForm +from django_async_extensions.aforms.utils import AsyncRenderableFormMixin -class AsyncModelForm(ModelForm): + +class AsyncModelForm(AsyncRenderableFormMixin, ModelForm): @classmethod async def from_async(cls, *args, **kwargs): return await sync_to_async(cls)(*args, **kwargs) diff --git a/django_async_extensions/aforms/utils.py b/django_async_extensions/aforms/utils.py new file mode 100644 index 0000000..4d66f9d --- /dev/null +++ b/django_async_extensions/aforms/utils.py @@ -0,0 +1,31 @@ +from asgiref.sync import sync_to_async + +from django.utils.safestring import mark_safe + + +class AsyncRenderableMixin: + async def arender(self, template_name=None, context=None, renderer=None): + renderer = renderer or self.renderer + template = template_name or self.template_name + context = context or self.get_context() + return mark_safe( # noqa:S308 + await sync_to_async(renderer.render)(template, context) + ) + + +class AsyncRenderableFormMixin(AsyncRenderableMixin): + async def aas_p(self): + """Render as

elements.""" + return await self.arender(self.template_name_p) + + async def aas_table(self): + """Render as elements excluding the surrounding tag.""" + return await self.arender(self.template_name_table) + + async def aas_ul(self): + """Render as
  • elements excluding the surrounding
      tag.""" + return await self.arender(self.template_name_ul) + + async def aas_div(self): + """Render as
      elements.""" + return await self.arender(self.template_name_div) From 0d5431ba0cce3f775166c86813d66f92c1880ff6 Mon Sep 17 00:00:00 2001 From: amirreza Date: Mon, 24 Feb 2025 23:58:27 +0330 Subject: [PATCH 4/4] adjusted the tests with the new features of model form --- tests/test_model_forms/test_model_form.py | 71 +++++++++++------------ 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/tests/test_model_forms/test_model_form.py b/tests/test_model_forms/test_model_form.py index 02cec2f..26604f6 100644 --- a/tests/test_model_forms/test_model_form.py +++ b/tests/test_model_forms/test_model_form.py @@ -310,7 +310,7 @@ def __init__(self, *args, **kwargs): assert f1.is_valid() f2 = FormForTestingIsValid(data2) - assert await sync_to_async(f2.is_valid)() + assert await f2.ais_valid() obj = await f2.asave() assert obj.character == char @@ -965,25 +965,25 @@ def setup(self): async def test_simple_unique(self): form = ProductForm({"slug": "teddy-bear-blue"}) - assert await sync_to_async(form.is_valid)() + assert await form.ais_valid() obj = await form.asave() form = ProductForm({"slug": "teddy-bear-blue"}) - await sync_to_async(form.full_clean)() - assert len(form.errors) == 1 - assert form.errors["slug"] == ["Product with this Slug already exists."] + errors = await form.aerrors + assert len(errors) == 1 + assert errors["slug"] == ["Product with this Slug already exists."] form = ProductForm({"slug": "teddy-bear-blue"}, instance=obj) - assert await sync_to_async(form.is_valid)() + assert await form.ais_valid() async def test_unique_together(self): """ModelForm test of unique_together constraint""" form = PriceForm({"price": "6.00", "quantity": "1"}) - assert await sync_to_async(form.is_valid)() + assert await form.ais_valid() await form.asave() form = PriceForm({"price": "6.00", "quantity": "1"}) - assert await sync_to_async(form.is_valid)() is False - await sync_to_async(form.full_clean)() - assert len(form.errors) == 1 - assert form.errors["__all__"] == [ + assert await form.ais_valid() is False + errors = await form.aerrors + assert len(errors) == 1 + assert errors["__all__"] == [ "Price with this Price and Quantity already exists." ] @@ -1044,15 +1044,13 @@ class Meta: async def test_unique_null(self): title = "I May Be Wrong But I Doubt It" form = BookForm({"title": title, "author": self.writer.pk}) - assert await sync_to_async(form.is_valid)() + assert await form.ais_valid() await form.asave() form = BookForm({"title": title, "author": self.writer.pk}) - assert await sync_to_async(form.is_valid)() is False - await sync_to_async(form.full_clean)() - assert len(form.errors) == 1 - assert form.errors["__all__"] == [ - "Book with this Title and Author already exists." - ] + assert await form.ais_valid() is False + errors = await form.aerrors + assert len(errors) == 1 + assert errors["__all__"] == ["Book with this Title and Author already exists."] form = BookForm({"title": title}) assert form.is_valid() await form.asave() @@ -1079,12 +1077,12 @@ def test_inherited_unique(self): async def test_inherited_unique_together(self): title = "Boss" form = BookForm({"title": title, "author": self.writer.pk}) - assert await sync_to_async(form.is_valid)() + assert await form.ais_valid() await form.asave() form = DerivedBookForm( {"title": title, "author": self.writer.pk, "isbn": "12345"} ) - assert await sync_to_async(form.is_valid)() is False + assert await form.ais_valid() is False assert len(form.errors) == 1 assert form.errors["__all__"] == [ "Book with this Title and Author already exists." @@ -1134,10 +1132,10 @@ def test_explicitpk_unspecified(self): async def test_explicitpk_unique(self): """Ensure keys and blank character strings are tested for uniqueness.""" form = ExplicitPKForm({"key": "key1", "desc": ""}) - assert await sync_to_async(form.is_valid)() + assert await form.ais_valid() await form.asave() form = ExplicitPKForm({"key": "key1", "desc": ""}) - assert await sync_to_async(form.is_valid)() is False + assert await form.ais_valid() is False if connection.features.interprets_empty_strings_as_nulls: assert len(form.errors) == 1 assert form.errors["key"] == ["Explicit pk with this Key already exists."] @@ -1397,7 +1395,7 @@ async def test_initial_values(self): }, ) assertHTMLEqual( - await sync_to_async(f.as_ul)(), + await f.aas_ul(), """
    • Headline: Headline:
    • @@ -1640,9 +1637,9 @@ async def test_multi_fields(self): ) await new_art.categories.aadd(await Category.objects.aget(name="Entertainment")) assert [art async for art in new_art.categories.all()] == [self.c1] - f = await sync_to_async(ArticleForm)(auto_id=False, instance=new_art) + f = await ArticleForm.from_async(auto_id=False, instance=new_art) assertHTMLEqual( - await sync_to_async(f.as_ul)(), + await f.aas_ul(), """
    • Headline: Headline: ' "
    • " '
    • Slug:
    • ' @@ -1855,7 +1852,7 @@ async def test_runtime_choicefield_populated(self): c4 = await Category.objects.acreate(name="Fourth", url="4th") w_bernstein = await Writer.objects.acreate(name="Carl Bernstein") assertHTMLEqual( - await sync_to_async(f.as_ul)(), + await f.aas_ul(), '
    • Headline: ' "
    • " '
    • Slug:
    • ' @@ -1984,7 +1981,7 @@ def __init__(self, *args, **kwargs): "article": "lorem ipsum", } form = MyForm(data) - assert await sync_to_async(form.is_valid)() + assert await form.ais_valid() article = await form.asave() assert article.writer == w @@ -2264,7 +2261,7 @@ class Meta: form = WriterProfileForm() assertHTMLEqual( - await sync_to_async(form.as_p)(), + await form.aas_p(), """