From b9473cac65190822e7c94f695f1f7b4d5b49502a Mon Sep 17 00:00:00 2001 From: Olivier Tabone Date: Thu, 20 Jul 2023 17:50:06 +0200 Subject: [PATCH] Fixed #34714 -- Added aget_object_or_404()/aget_list_or_404() shortcuts. --- django/shortcuts.py | 34 +++++++++++++++++ docs/releases/5.0.txt | 4 ++ docs/topics/http/shortcuts.txt | 26 ++++++++++--- tests/async/test_async_shortcuts.py | 58 +++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 tests/async/test_async_shortcuts.py diff --git a/django/shortcuts.py b/django/shortcuts.py index 90ec1bedc5..822e6107ac 100644 --- a/django/shortcuts.py +++ b/django/shortcuts.py @@ -89,6 +89,23 @@ def get_object_or_404(klass, *args, **kwargs): ) +async def aget_object_or_404(klass, *args, **kwargs): + """See get_object_or_404().""" + queryset = _get_queryset(klass) + if not hasattr(queryset, "aget"): + klass__name = ( + klass.__name__ if isinstance(klass, type) else klass.__class__.__name__ + ) + raise ValueError( + "First argument to aget_object_or_404() must be a Model, Manager, or " + f"QuerySet, not '{klass__name}'." + ) + try: + return await queryset.aget(*args, **kwargs) + except queryset.model.DoesNotExist: + raise Http404(f"No {queryset.model._meta.object_name} matches the given query.") + + def get_list_or_404(klass, *args, **kwargs): """ Use filter() to return a list of objects, or raise an Http404 exception if @@ -114,6 +131,23 @@ def get_list_or_404(klass, *args, **kwargs): return obj_list +async def aget_list_or_404(klass, *args, **kwargs): + """See get_list_or_404().""" + queryset = _get_queryset(klass) + if not hasattr(queryset, "filter"): + klass__name = ( + klass.__name__ if isinstance(klass, type) else klass.__class__.__name__ + ) + raise ValueError( + "First argument to aget_list_or_404() must be a Model, Manager, or " + f"QuerySet, not '{klass__name}'." + ) + obj_list = [obj async for obj in queryset.filter(*args, **kwargs)] + if not obj_list: + raise Http404(f"No {queryset.model._meta.object_name} matches the given query.") + return obj_list + + def resolve_url(to, *args, **kwargs): """ Return a URL appropriate for the arguments passed. diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index 815fb062b4..e7fcc6e07f 100644 --- a/docs/releases/5.0.txt +++ b/docs/releases/5.0.txt @@ -364,6 +364,10 @@ Models * The new :attr:`.UniqueConstraint.nulls_distinct` attribute allows customizing the treatment of ``NULL`` values on PostgreSQL 15+. +* The new :func:`~django.shortcuts.aget_object_or_404` and + :func:`~django.shortcuts.aget_list_or_404` asynchronous shortcuts allow + asynchronous getting objects. + Pagination ~~~~~~~~~~ diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt index f3cbd151aa..3e4778f0f2 100644 --- a/docs/topics/http/shortcuts.txt +++ b/docs/topics/http/shortcuts.txt @@ -162,10 +162,13 @@ will be returned:: ======================= .. function:: get_object_or_404(klass, *args, **kwargs) +.. function:: aget_object_or_404(klass, *args, **kwargs) - Calls :meth:`~django.db.models.query.QuerySet.get()` on a given model manager, - but it raises :class:`~django.http.Http404` instead of the model's - :class:`~django.db.models.Model.DoesNotExist` exception. + *Asynchronous version*: ``aget_object_or_404()`` + + Calls :meth:`~django.db.models.query.QuerySet.get()` on a given model + manager, but it raises :class:`~django.http.Http404` instead of the model's + :class:`~django.db.models.Model.DoesNotExist` exception. Arguments --------- @@ -236,14 +239,21 @@ Note: As with ``get()``, a :class:`~django.core.exceptions.MultipleObjectsReturned` exception will be raised if more than one object is found. +.. versionchanged:: 5.0 + + ``aget_object_or_404()`` function was added. + ``get_list_or_404()`` ===================== .. function:: get_list_or_404(klass, *args, **kwargs) +.. function:: aget_list_or_404(klass, *args, **kwargs) - Returns the result of :meth:`~django.db.models.query.QuerySet.filter()` on a - given model manager cast to a list, raising :class:`~django.http.Http404` if - the resulting list is empty. + *Asynchronous version*: ``aget_list_or_404()`` + + Returns the result of :meth:`~django.db.models.query.QuerySet.filter()` on + a given model manager cast to a list, raising :class:`~django.http.Http404` + if the resulting list is empty. Arguments --------- @@ -280,3 +290,7 @@ This example is equivalent to:: my_objects = list(MyModel.objects.filter(published=True)) if not my_objects: raise Http404("No MyModel matches the given query.") + +.. versionchanged:: 5.0 + + ``aget_list_or_404()`` function was added. diff --git a/tests/async/test_async_shortcuts.py b/tests/async/test_async_shortcuts.py new file mode 100644 index 0000000000..09c76873fa --- /dev/null +++ b/tests/async/test_async_shortcuts.py @@ -0,0 +1,58 @@ +from django.db.models import Q +from django.http import Http404 +from django.shortcuts import aget_list_or_404, aget_object_or_404 +from django.test import TestCase + +from .models import RelatedModel, SimpleModel + + +class GetListObjectOr404Test(TestCase): + @classmethod + def setUpTestData(cls): + cls.s1 = SimpleModel.objects.create(field=0) + cls.s2 = SimpleModel.objects.create(field=1) + cls.r1 = RelatedModel.objects.create(simple=cls.s1) + + async def test_aget_object_or_404(self): + self.assertEqual(await aget_object_or_404(SimpleModel, field=1), self.s2) + self.assertEqual(await aget_object_or_404(SimpleModel, Q(field=0)), self.s1) + self.assertEqual( + await aget_object_or_404(SimpleModel.objects.all(), field=1), self.s2 + ) + self.assertEqual( + await aget_object_or_404(self.s1.relatedmodel_set, pk=self.r1.pk), self.r1 + ) + # Http404 is returned if the list is empty. + msg = "No SimpleModel matches the given query." + with self.assertRaisesMessage(Http404, msg): + await aget_object_or_404(SimpleModel, field=2) + + async def test_get_list_or_404(self): + self.assertEqual(await aget_list_or_404(SimpleModel, field=1), [self.s2]) + self.assertEqual(await aget_list_or_404(SimpleModel, Q(field=0)), [self.s1]) + self.assertEqual( + await aget_list_or_404(SimpleModel.objects.all(), field=1), [self.s2] + ) + self.assertEqual( + await aget_list_or_404(self.s1.relatedmodel_set, pk=self.r1.pk), [self.r1] + ) + # Http404 is returned if the list is empty. + msg = "No SimpleModel matches the given query." + with self.assertRaisesMessage(Http404, msg): + await aget_list_or_404(SimpleModel, field=2) + + async def test_get_object_or_404_bad_class(self): + msg = ( + "First argument to aget_object_or_404() must be a Model, Manager, or " + "QuerySet, not 'str'." + ) + with self.assertRaisesMessage(ValueError, msg): + await aget_object_or_404("SimpleModel", field=0) + + async def test_get_list_or_404_bad_class(self): + msg = ( + "First argument to aget_list_or_404() must be a Model, Manager, or " + "QuerySet, not 'list'." + ) + with self.assertRaisesMessage(ValueError, msg): + await aget_list_or_404([SimpleModel], field=1)