Skip to content

Commit 7b94847

Browse files
bigfootjonfelixxm
authored andcommitted
Fixed #34139 -- Fixed acreate(), aget_or_create(), and aupdate_or_create() methods for related managers.
Bug in 58b27e0.
1 parent 76e3751 commit 7b94847

File tree

8 files changed

+155
-0
lines changed

8 files changed

+155
-0
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ answer newbie questions, and generally made Django that much better:
495495
John Shaffer <[email protected]>
496496
Jökull Sólberg Auðunsson <[email protected]>
497497
Jon Dufresne <[email protected]>
498+
Jon Janzen <[email protected]>
498499
Jonas Haag <[email protected]>
499500
Jonas Lundberg <[email protected]>
500501
Jonathan Davis <[email protected]>

django/contrib/contenttypes/fields.py

+17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import itertools
33
from collections import defaultdict
44

5+
from asgiref.sync import sync_to_async
6+
57
from django.contrib.contenttypes.models import ContentType
68
from django.core import checks
79
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
@@ -747,6 +749,11 @@ def create(self, **kwargs):
747749

748750
create.alters_data = True
749751

752+
async def acreate(self, **kwargs):
753+
return await sync_to_async(self.create)(**kwargs)
754+
755+
acreate.alters_data = True
756+
750757
def get_or_create(self, **kwargs):
751758
kwargs[self.content_type_field_name] = self.content_type
752759
kwargs[self.object_id_field_name] = self.pk_val
@@ -755,6 +762,11 @@ def get_or_create(self, **kwargs):
755762

756763
get_or_create.alters_data = True
757764

765+
async def aget_or_create(self, **kwargs):
766+
return await sync_to_async(self.get_or_create)(**kwargs)
767+
768+
aget_or_create.alters_data = True
769+
758770
def update_or_create(self, **kwargs):
759771
kwargs[self.content_type_field_name] = self.content_type
760772
kwargs[self.object_id_field_name] = self.pk_val
@@ -763,4 +775,9 @@ def update_or_create(self, **kwargs):
763775

764776
update_or_create.alters_data = True
765777

778+
async def aupdate_or_create(self, **kwargs):
779+
return await sync_to_async(self.update_or_create)(**kwargs)
780+
781+
aupdate_or_create.alters_data = True
782+
766783
return GenericRelatedObjectManager

django/db/models/fields/related_descriptors.py

+38
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class Child(Model):
6363
``ReverseManyToManyDescriptor``, use ``ManyToManyDescriptor`` instead.
6464
"""
6565

66+
from asgiref.sync import sync_to_async
67+
6668
from django.core.exceptions import FieldError
6769
from django.db import (
6870
DEFAULT_DB_ALIAS,
@@ -793,6 +795,11 @@ def create(self, **kwargs):
793795

794796
create.alters_data = True
795797

798+
async def acreate(self, **kwargs):
799+
return await sync_to_async(self.create)(**kwargs)
800+
801+
acreate.alters_data = True
802+
796803
def get_or_create(self, **kwargs):
797804
self._check_fk_val()
798805
kwargs[self.field.name] = self.instance
@@ -801,6 +808,11 @@ def get_or_create(self, **kwargs):
801808

802809
get_or_create.alters_data = True
803810

811+
async def aget_or_create(self, **kwargs):
812+
return await sync_to_async(self.get_or_create)(**kwargs)
813+
814+
aget_or_create.alters_data = True
815+
804816
def update_or_create(self, **kwargs):
805817
self._check_fk_val()
806818
kwargs[self.field.name] = self.instance
@@ -809,6 +821,11 @@ def update_or_create(self, **kwargs):
809821

810822
update_or_create.alters_data = True
811823

824+
async def aupdate_or_create(self, **kwargs):
825+
return await sync_to_async(self.update_or_create)(**kwargs)
826+
827+
aupdate_or_create.alters_data = True
828+
812829
# remove() and clear() are only provided if the ForeignKey can have a
813830
# value of null.
814831
if rel.field.null:
@@ -1191,6 +1208,13 @@ def create(self, *, through_defaults=None, **kwargs):
11911208

11921209
create.alters_data = True
11931210

1211+
async def acreate(self, *, through_defaults=None, **kwargs):
1212+
return await sync_to_async(self.create)(
1213+
through_defaults=through_defaults, **kwargs
1214+
)
1215+
1216+
acreate.alters_data = True
1217+
11941218
def get_or_create(self, *, through_defaults=None, **kwargs):
11951219
db = router.db_for_write(self.instance.__class__, instance=self.instance)
11961220
obj, created = super(ManyRelatedManager, self.db_manager(db)).get_or_create(
@@ -1204,6 +1228,13 @@ def get_or_create(self, *, through_defaults=None, **kwargs):
12041228

12051229
get_or_create.alters_data = True
12061230

1231+
async def aget_or_create(self, *, through_defaults=None, **kwargs):
1232+
return await sync_to_async(self.get_or_create)(
1233+
through_defaults=through_defaults, **kwargs
1234+
)
1235+
1236+
aget_or_create.alters_data = True
1237+
12071238
def update_or_create(self, *, through_defaults=None, **kwargs):
12081239
db = router.db_for_write(self.instance.__class__, instance=self.instance)
12091240
obj, created = super(
@@ -1217,6 +1248,13 @@ def update_or_create(self, *, through_defaults=None, **kwargs):
12171248

12181249
update_or_create.alters_data = True
12191250

1251+
async def aupdate_or_create(self, *, through_defaults=None, **kwargs):
1252+
return await sync_to_async(self.update_or_create)(
1253+
through_defaults=through_defaults, **kwargs
1254+
)
1255+
1256+
aupdate_or_create.alters_data = True
1257+
12201258
def _get_target_ids(self, target_field_name, objs):
12211259
"""
12221260
Return the set of ids of `objs` that the target field references.

docs/ref/models/relations.txt

+7
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ Related objects reference
7676
intermediate instance(s).
7777

7878
.. method:: create(through_defaults=None, **kwargs)
79+
.. method:: acreate(through_defaults=None, **kwargs)
80+
81+
*Asynchronous version*: ``acreate``
7982

8083
Creates a new object, saves it and puts it in the related object set.
8184
Returns the newly created object::
@@ -110,6 +113,10 @@ Related objects reference
110113
needed. You can use callables as values in the ``through_defaults``
111114
dictionary.
112115

116+
.. versionchanged:: 4.1
117+
118+
``acreate()`` method was added.
119+
113120
.. method:: remove(*objs, bulk=True)
114121

115122
Removes the specified model objects from the related object set::

docs/releases/4.1.4.txt

+4
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ Bugfixes
1616
an empty :meth:`Sitemap.items() <django.contrib.sitemaps.Sitemap.items>` and
1717
a callable :attr:`~django.contrib.sitemaps.Sitemap.lastmod`
1818
(:ticket:`34088`).
19+
20+
* Fixed a bug in Django 4.1 that caused a crash of ``acreate()``,
21+
``aget_or_create()``, and ``aupdate_or_create()`` asynchronous methods for
22+
related managers (:ticket:`34139`).

tests/async/models.py

+4
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ class RelatedModel(models.Model):
99
class SimpleModel(models.Model):
1010
field = models.IntegerField()
1111
created = models.DateTimeField(default=timezone.now)
12+
13+
14+
class ManyToManyModel(models.Model):
15+
simples = models.ManyToManyField("SimpleModel")
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from django.test import TestCase
2+
3+
from .models import ManyToManyModel, SimpleModel
4+
5+
6+
class AsyncRelatedManagersOperationTest(TestCase):
7+
@classmethod
8+
def setUpTestData(cls):
9+
cls.mtm1 = ManyToManyModel.objects.create()
10+
cls.s1 = SimpleModel.objects.create(field=0)
11+
12+
async def test_acreate(self):
13+
await self.mtm1.simples.acreate(field=2)
14+
new_simple = await self.mtm1.simples.aget()
15+
self.assertEqual(new_simple.field, 2)
16+
17+
async def test_acreate_reverse(self):
18+
await self.s1.relatedmodel_set.acreate()
19+
new_relatedmodel = await self.s1.relatedmodel_set.aget()
20+
self.assertEqual(new_relatedmodel.simple, self.s1)
21+
22+
async def test_aget_or_create(self):
23+
new_simple, created = await self.mtm1.simples.aget_or_create(field=2)
24+
self.assertIs(created, True)
25+
self.assertEqual(await self.mtm1.simples.acount(), 1)
26+
self.assertEqual(new_simple.field, 2)
27+
new_simple, created = await self.mtm1.simples.aget_or_create(
28+
id=new_simple.id, through_defaults={"field": 3}
29+
)
30+
self.assertIs(created, False)
31+
self.assertEqual(await self.mtm1.simples.acount(), 1)
32+
self.assertEqual(new_simple.field, 2)
33+
34+
async def test_aget_or_create_reverse(self):
35+
new_relatedmodel, created = await self.s1.relatedmodel_set.aget_or_create()
36+
self.assertIs(created, True)
37+
self.assertEqual(await self.s1.relatedmodel_set.acount(), 1)
38+
self.assertEqual(new_relatedmodel.simple, self.s1)
39+
40+
async def test_aupdate_or_create(self):
41+
new_simple, created = await self.mtm1.simples.aupdate_or_create(field=2)
42+
self.assertIs(created, True)
43+
self.assertEqual(await self.mtm1.simples.acount(), 1)
44+
self.assertEqual(new_simple.field, 2)
45+
new_simple, created = await self.mtm1.simples.aupdate_or_create(
46+
id=new_simple.id, defaults={"field": 3}
47+
)
48+
self.assertIs(created, False)
49+
self.assertEqual(await self.mtm1.simples.acount(), 1)
50+
self.assertEqual(new_simple.field, 3)
51+
52+
async def test_aupdate_or_create_reverse(self):
53+
new_relatedmodel, created = await self.s1.relatedmodel_set.aupdate_or_create()
54+
self.assertIs(created, True)
55+
self.assertEqual(await self.s1.relatedmodel_set.acount(), 1)
56+
self.assertEqual(new_relatedmodel.simple, self.s1)

tests/generic_relations/tests.py

+28
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def comp_func(self, obj):
4545
# Original list of tags:
4646
return obj.tag, obj.content_type.model_class(), obj.object_id
4747

48+
async def test_generic_async_acreate(self):
49+
await self.bacon.tags.acreate(tag="orange")
50+
self.assertEqual(await self.bacon.tags.acount(), 3)
51+
4852
def test_generic_update_or_create_when_created(self):
4953
"""
5054
Should be able to use update_or_create from the generic related manager
@@ -70,6 +74,18 @@ def test_generic_update_or_create_when_updated(self):
7074
self.assertEqual(count + 1, self.bacon.tags.count())
7175
self.assertEqual(tag.tag, "juicy")
7276

77+
async def test_generic_async_aupdate_or_create(self):
78+
tag, created = await self.bacon.tags.aupdate_or_create(
79+
id=self.fatty.id, defaults={"tag": "orange"}
80+
)
81+
self.assertIs(created, False)
82+
self.assertEqual(tag.tag, "orange")
83+
self.assertEqual(await self.bacon.tags.acount(), 2)
84+
tag, created = await self.bacon.tags.aupdate_or_create(tag="pink")
85+
self.assertIs(created, True)
86+
self.assertEqual(await self.bacon.tags.acount(), 3)
87+
self.assertEqual(tag.tag, "pink")
88+
7389
def test_generic_get_or_create_when_created(self):
7490
"""
7591
Should be able to use get_or_create from the generic related manager
@@ -96,6 +112,18 @@ def test_generic_get_or_create_when_exists(self):
96112
# shouldn't had changed the tag
97113
self.assertEqual(tag.tag, "stinky")
98114

115+
async def test_generic_async_aget_or_create(self):
116+
tag, created = await self.bacon.tags.aget_or_create(
117+
id=self.fatty.id, defaults={"tag": "orange"}
118+
)
119+
self.assertIs(created, False)
120+
self.assertEqual(tag.tag, "fatty")
121+
self.assertEqual(await self.bacon.tags.acount(), 2)
122+
tag, created = await self.bacon.tags.aget_or_create(tag="orange")
123+
self.assertIs(created, True)
124+
self.assertEqual(await self.bacon.tags.acount(), 3)
125+
self.assertEqual(tag.tag, "orange")
126+
99127
def test_generic_relations_m2m_mimic(self):
100128
"""
101129
Objects with declared GenericRelations can be tagged directly -- the

0 commit comments

Comments
 (0)