diff --git a/AUTHORS b/AUTHORS index 22eb211b9abe..e752d00671b3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -284,6 +284,7 @@ answer newbie questions, and generally made Django that much better: Dan Stephenson Dan Watson dave@thebarproject.com + Dave Gaeddert David Ascher David Avsajanishvili David Blewett diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index eef7bc93a5ce..be6a24800fd8 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -1,5 +1,6 @@ import itertools import math +from collections.abc import Iterator from django.core.exceptions import EmptyResultSet, FullResultSet from django.db.models.expressions import ( @@ -292,7 +293,9 @@ class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin): def get_prep_lookup(self): if hasattr(self.rhs, "resolve_expression"): return self.rhs - if any(hasattr(value, "resolve_expression") for value in self.rhs): + # Prevent iterator from being consumed by any(). + rhs = list(self.rhs) if isinstance(self.rhs, Iterator) else self.rhs + if any(hasattr(value, "resolve_expression") for value in rhs): # Wrap direct values in Value expressions so they are handled by # the database at compilation time, along with other expressions. return ExpressionList( @@ -302,11 +305,11 @@ def get_prep_lookup(self): if hasattr(value, "resolve_expression") else Value(value, getattr(self.lhs, "output_field", None)) ) - for value in self.rhs + for value in rhs ] ) prepared_values = [] - for rhs_value in self.rhs: + for rhs_value in rhs: if ( self.prepare_rhs and hasattr(self.lhs, "output_field") diff --git a/django/db/models/query.py b/django/db/models/query.py index 65d85751bd69..ae000c3e8c65 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1303,13 +1303,22 @@ def in_bulk(self, id_list=None, *, field_name="pk"): def get_obj(obj): return obj + selected_fields = tuple( + self.query.selected + or ( + *self.query.extra_select, + *self.query.values_select, + *self.query.annotation_select, + ) + ) + if issubclass(self._iterable_class, ModelIterable): # Raise an AttributeError if field_name is deferred. get_key = operator.attrgetter(field_name) elif issubclass(self._iterable_class, ValuesIterable): if field_name not in self.query.values_select: - qs = qs.values(field_name, *self.query.values_select) + qs = qs.values(field_name, *selected_fields) def get_obj(obj): # noqa: F811 # We can safely mutate the dictionaries returned by @@ -1322,16 +1331,16 @@ def get_obj(obj): # noqa: F811 elif issubclass(self._iterable_class, ValuesListIterable): try: - field_index = self.query.values_select.index(field_name) + field_index = selected_fields.index(field_name) except ValueError: - # field_name is missing from values_select, so add it. + # field_name isn't selected, so add it. field_index = 0 if issubclass(self._iterable_class, NamedValuesListIterable): kwargs = {"named": True} else: kwargs = {} get_obj = operator.itemgetter(slice(1, None)) - qs = qs.values_list(field_name, *self.query.values_select, **kwargs) + qs = qs.values_list(field_name, *selected_fields, **kwargs) get_key = operator.itemgetter(field_index) @@ -1341,7 +1350,7 @@ def get_obj(obj): # noqa: F811 get_key = get_obj else: # Transform it back into a non-flat values_list(). - qs = qs.values_list(field_name, *self.query.values_select) + qs = qs.values_list(field_name, *selected_fields) get_key = operator.itemgetter(0) get_obj = operator.itemgetter(1) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index bbb83dee045f..1a22393cf91b 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -357,11 +357,30 @@ def _order_by_pairs(self): # Avoid computing `selected_exprs` if there is no `ordering` as it's # relatively expensive. if ordering and (select := self.select): + distinct_fields = self.query.distinct_fields + annotation_select = self.query.annotation_select + # An expression can be selected at more than one position, e.g. + # when two lookup paths resolve to the same column. get_distinct() + # refers to such selections by expression and PostgreSQL binds an + # expression reference to the first position it is selected at, so + # ordering must refer to that position as well for the prefixes of + # both clauses to match. Raw selections are excluded as equal SQL + # is not necessarily interchangeable, e.g. a volatile function + # selected twice. + first_positions = {} for ordinal, (expr, _, alias) in enumerate(select, start=1): pos_expr = PositionRef(ordinal, alias, expr) + if distinct_fields and not isinstance(expr, RawSQL): + first_pos_expr = first_positions.setdefault(expr, pos_expr) + else: + first_pos_expr = pos_expr if alias: - selected_exprs[alias] = pos_expr - selected_exprs[expr] = pos_expr + # get_distinct() refers to annotations by alias, which + # binds to their own position. + selected_exprs[alias] = ( + pos_expr if alias in annotation_select else first_pos_expr + ) + selected_exprs[expr] = first_pos_expr for field in ordering: if hasattr(field, "resolve_expression"): diff --git a/docs/howto/static-files/deployment.txt b/docs/howto/static-files/deployment.txt index 19b7c9df826a..d21c608e6ba9 100644 --- a/docs/howto/static-files/deployment.txt +++ b/docs/howto/static-files/deployment.txt @@ -25,6 +25,18 @@ As with all deployment tasks, the devil's in the details. Every production setup will be a bit different, so you'll need to adapt the basic outline to fit your needs. Below are a few common patterns that might help. +Having your Django application serve static files +------------------------------------------------- + +While web servers are much better and faster at serving static files, it is +possible to have Django serve them using community-maintained packages. There +are static file packages highlighted on the `Community Ecosystem`_ page. The +Django Packages `Static Builders grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#storage-static-files +.. _Static Builders grid: https://djangopackages.org/grids/g/static-builders/ + + Serving the site and your static files from the same server ----------------------------------------------------------- diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt index 134e5d8ebe37..48be85fe8c10 100644 --- a/docs/ref/contrib/admin/actions.txt +++ b/docs/ref/contrib/admin/actions.txt @@ -407,7 +407,8 @@ decorator and passing the ``permissions`` argument:: queryset.update(status="p") The ``make_published()`` action will only be available to users that pass the -:meth:`.ModelAdmin.has_change_permission` check. +:meth:`.ModelAdmin.has_change_permission` check when it is called with +``obj=None``. If ``permissions`` has more than one permission, the action will be available as long as the user passes at least one of the checks. @@ -441,10 +442,25 @@ For example:: codename = get_permission_codename("publish", opts) return request.user.has_perm("%s.%s" % (opts.app_label, codename)) +The admin doesn't automatically check object-level permissions for each +selected object when executing an action. To limit which objects an action may +modify based on object-level permissions, perform those checks in the action +itself. For example:: + + class ArticleAdmin(admin.ModelAdmin): + actions = ["make_published"] + + @admin.action(permissions=["change"]) + def make_published(self, request, queryset): + for obj in queryset: + if self.has_change_permission(request, obj=obj): + obj.status = "p" + obj.save(update_fields=["status"]) + .. _admin-action-availability: Controlling where actions are available -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------- .. versionadded:: 6.1 diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 0243c2b980f7..422d4b3e88b8 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -78,6 +78,15 @@ Other topics Having problems? Try :doc:`/faq/admin`. +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are solutions for customizing and + extending the admin highlighted on the `Community Ecosystem`_ page. The + Django Packages `Admin interface grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#admin +.. _Admin interface grid: https://djangopackages.org/grids/g/admin-interface/ + ``ModelAdmin`` objects ====================== @@ -1959,15 +1968,24 @@ default templates used by the :class:`ModelAdmin` views: .. method:: ModelAdmin.has_view_permission(request, obj=None) - Should return ``True`` if viewing ``obj`` is permitted, ``False`` - otherwise. If obj is ``None``, should return ``True`` or ``False`` to - indicate whether viewing of objects of this type is permitted in general - (e.g., ``False`` will be interpreted as meaning that the current user is - not permitted to view any object of this type). + Should return ``True`` if the user is permitted to access the change or + history view for ``obj``, and ``False`` otherwise. If obj is ``None``, + should return ``True`` or ``False`` to indicate whether viewing of objects + of this type is permitted in general (e.g., ``False`` will be interpreted + as meaning that the current user is not permitted to view any object of + this type). The default implementation returns ``True`` if the user has either the "change" or "view" permission. + .. admonition:: Object-level view permissions aren't visibility filters + + Object-level view permissions don't prevent information about an object + from appearing elsewhere in the admin, including in the admin + changelist, related field choices, autocomplete results, and recent + actions. To restrict this information, customize the querysets used by + the relevant views and form fields. + .. method:: ModelAdmin.has_add_permission(request) Should return ``True`` if adding an object is permitted, ``False`` diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 55c7dea4464f..5a9d5744f836 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -84,6 +84,16 @@ notification and debug information that ``django-admin`` prints to the console. Available commands ================== +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are developer tool packages + highlighted on the `Community Ecosystem`_ page. The Django Packages + `Developer Tools grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#debugging-development-tools +.. _Developer Tools grid: https://djangopackages.org/grids/g/developer-tools/ + + ``check`` --------- diff --git a/docs/releases/6.1.1.txt b/docs/releases/6.1.1.txt index 940c815c9b24..180f118c3152 100644 --- a/docs/releases/6.1.1.txt +++ b/docs/releases/6.1.1.txt @@ -2,13 +2,17 @@ Django 6.1.1 release notes ========================== -*Expected September 2, 2026* +*September 2, 2026* -Django 6.1.1 fixes several bugs in 6.1. +Django 6.1.1 fixes one crash in Django 5.2 and several bugs in 6.1. Bugfixes ======== +* Fixed a crash in Django 5.2 when combining :meth:`distinct(*fields) + <.QuerySet.distinct>` with ``order_by()`` and ``values()`` and using two + lookup paths resolving to the same column (:ticket:`37222`). + * Fixed a regression in Django 6.1 where the deprecation of double-dot variable lookups incorrectly applied to string and translated template literals containing two consecutive dots, such as ``{{ "a..b" }}`` (:ticket:`37257`). @@ -50,3 +54,11 @@ Bugfixes ``choices``, and where any search term matched all rows with a ``True`` value when an ``__exact`` lookup was used on a ``BooleanField`` (:ticket:`37263`). + +* Fixed a regression in Django 6.1 that caused ``__in`` lookups on annotations + to erroneously return empty querysets and ``__range`` lookups to crash when + passed an iterator (:ticket:`37311`). + +* Fixed a bug in Django 6.1 where :meth:`.QuerySet.in_bulk` chained after + :meth:`.QuerySet.values` or :meth:`.QuerySet.values_list` could drop selected + annotations or produce incorrect mapping keys (:ticket:`37312`). diff --git a/docs/releases/6.1.2.txt b/docs/releases/6.1.2.txt new file mode 100644 index 000000000000..2ea851219af5 --- /dev/null +++ b/docs/releases/6.1.2.txt @@ -0,0 +1,12 @@ +========================== +Django 6.1.2 release notes +========================== + +*Expected October 6, 2026* + +Django 6.1.2 fixes several bugs in 6.1.1. + +Bugfixes +======== + +* ... diff --git a/docs/releases/index.txt b/docs/releases/index.txt index 7d269e8a9db4..0a27ee307c56 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -32,6 +32,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 6.1.2 6.1.1 6.1 diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 13c86d3ec30e..9ee1d8184a39 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -18,6 +18,14 @@ can be checked through Django's authorization system. You can :ref:`extend ` the default ``User`` model, or :ref:`substitute ` a completely customized model. +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are authentication and authorization + solutions highlighted on the `Community Ecosystem`_ page. The Django + Packages `Authentication grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#authentication-authorization +.. _Authentication grid: https://djangopackages.org/grids/g/authentication/ .. _authentication-backends: Other authentication sources diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 83925d009f18..dfc34ffd8a25 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -202,6 +202,16 @@ objects in the same way as any other :doc:`Django model myuser.user_permissions.remove(permission, permission, ...) myuser.user_permissions.clear() +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are authentication and authorization + solutions highlighted on the `Community Ecosystem`_ page. The Django + Packages `Authorization grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#authentication-authorization +.. _Authorization grid: https://djangopackages.org/grids/g/authorization/ + + Default permissions ------------------- diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 6af39bca9ac8..126b1695dca2 100644 --- a/docs/topics/class-based-views/index.txt +++ b/docs/topics/class-based-views/index.txt @@ -19,6 +19,16 @@ documentation`. generic-editing mixins +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are generic view extensions + highlighted on the `Community Ecosystem`_ page that build on Django's + class-based views. The Django Packages `Class Based Views grid`_ has + even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#forms-views +.. _Class Based Views grid: https://djangopackages.org/grids/g/cbv/ + Basic examples ============== diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index 78082dd06df0..d74cea445dee 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -109,6 +109,15 @@ Django handles three distinct parts of the work involved in forms: It is *possible* to write code that does all of this manually, but Django can take care of it all for you. +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are solutions for customizing how + forms are rendered highlighted on the `Community Ecosystem`_ page. The + Django Packages `Forms grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#forms-views +.. _Forms grid: https://djangopackages.org/grids/g/forms/ + Forms in Django =============== diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 1243789ec746..86c6bded7498 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -2011,6 +2011,16 @@ A number of settings can be used to adjust language cookie options: * :setting:`LANGUAGE_COOKIE_SAMESITE` * :setting:`LANGUAGE_COOKIE_SECURE` +Translating text stored in models +--------------------------------- + +Currently Django does not support translating text stored in models. However, +there are community-maintained solutions on the `Community Ecosystem`_ page. +The Django Packages `Internationalization grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#internationalization-localization +.. _Internationalization grid: https://djangopackages.org/grids/g/i18n/ + Implementation notes ==================== diff --git a/docs/topics/security.txt b/docs/topics/security.txt index dbb7a2e47788..075bc98f48cf 100644 --- a/docs/topics/security.txt +++ b/docs/topics/security.txt @@ -267,6 +267,22 @@ document protected by COOP opens a cross-origin popup window, the popup’s attacks. See :ref:`the cross-origin opener policy section of the security middleware reference ` for details. +Cross-origin resource sharing (CORS) +==================================== + +Cross-Origin Resource Sharing (CORS) controls which origins are permitted to +access resources from your site via the browser. Django does not include +built-in support for CORS headers. + +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are CORS and middleware solutions + highlighted on the `Community Ecosystem`_ page. The Django Packages + `Security grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#security-middleware +.. _Security grid: https://djangopackages.org/grids/g/security/ + Session security ================ diff --git a/docs/topics/testing/index.txt b/docs/topics/testing/index.txt index ef96895fb722..bc3148a726c1 100644 --- a/docs/topics/testing/index.txt +++ b/docs/topics/testing/index.txt @@ -28,6 +28,15 @@ You can also use any *other* Python test framework; Django provides an API and tools for that kind of integration. They are described in the :ref:`other-testing-frameworks` section of :doc:`/topics/testing/advanced`. +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are testing and fixtures solutions + highlighted on the `Community Ecosystem`_ page. The Django Packages + `Testing tools grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#testing-fixtures +.. _Testing tools grid: https://djangopackages.org/grids/g/testing/ + .. toctree:: :maxdepth: 1 diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 944701cdef16..a51e051479a8 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -6,6 +6,15 @@ Testing tools Django provides a small set of tools that come in handy when writing tests. +.. admonition:: There are community-maintained solutions! + + Django has a vibrant ecosystem. There are testing and fixtures solutions + highlighted on the `Community Ecosystem`_ page. The Django Packages + `Testing tools grid`_ has even more options for you! + +.. _Community Ecosystem: https://www.djangoproject.com/community/ecosystem/#testing-fixtures +.. _Testing tools grid: https://djangopackages.org/grids/g/testing/ + .. _test-client: The test client diff --git a/tests/distinct_on_fields/tests.py b/tests/distinct_on_fields/tests.py index f03e05ac73ce..e220a1711db2 100644 --- a/tests/distinct_on_fields/tests.py +++ b/tests/distinct_on_fields/tests.py @@ -1,5 +1,6 @@ from django.db import connection -from django.db.models import CharField, F, Max +from django.db.models import CharField, F, FloatField, Max +from django.db.models.expressions import RawSQL from django.db.models.functions import Lower from django.test import TestCase, skipUnlessDBFeature from django.test.utils import register_lookup @@ -179,6 +180,133 @@ def test_distinct_on_mixed_case_annotation(self): ) self.assertSequenceEqual(qs, [self.p1_o1, self.p2_o1, self.p3_o1]) + def test_distinct_on_duplicated_selected_columns(self): + # "stafftag__tag__name" and "tags__name" resolve to the same column, so + # it's selected twice, but DISTINCT ON refers to its first selection. + fields = ["stafftag__tag__name", "tags__name", "name"] + qs = ( + Staff.objects.order_by(*[F(field).asc(nulls_last=True) for field in fields]) + .distinct(*fields) + .values_list(*fields) + ) + self.assertSequenceEqual( + qs, + [ + ("t1", "t1", "p1"), + (None, None, "p1"), + (None, None, "p2"), + (None, None, "p3"), + ], + ) + + def test_distinct_on_duplicated_selected_columns_descending(self): + fields = ["stafftag__tag__name", "tags__name", "name"] + qs = ( + Staff.objects.order_by( + *[F(field).desc(nulls_first=True) for field in fields] + ) + .distinct(*fields) + .values_list(*fields) + ) + self.assertSequenceEqual( + qs, + [ + (None, None, "p3"), + (None, None, "p2"), + (None, None, "p1"), + ("t1", "t1", "p1"), + ], + ) + + def test_distinct_on_duplicated_selected_foreign_key_columns(self): + # The local "tag_id" and remote "tag__id" references of a foreign key + # resolve to the same column without requiring different join aliases. + fields = ["tag_id", "tag__id", "tag__name"] + qs = StaffTag.objects.order_by(*fields).distinct(*fields).values_list(*fields) + self.assertSequenceEqual(qs, [(self.t1.pk, self.t1.pk, "t1")]) + + def test_distinct_on_duplicated_selected_transforms(self): + # Transforms of the same column are equal expressions as well, and are + # also referred to by expression by DISTINCT ON. + fields = ["stafftag__tag__name__lower", "tags__name__lower", "name"] + with register_lookup(CharField, Lower): + qs = ( + Staff.objects.order_by( + *[F(field).asc(nulls_last=True) for field in fields] + ) + .distinct(*fields) + .values_list(*fields) + ) + self.assertSequenceEqual( + qs, + [ + ("t1", "t1", "p1"), + (None, None, "p1"), + (None, None, "p2"), + (None, None, "p3"), + ], + ) + + def test_distinct_on_column_selected_by_annotation_first(self): + # The annotation selects the column before the lookup path does, and + # DISTINCT ON binds to the first selection of the column regardless of + # it being selected by an annotation. + qs = ( + Staff.objects.annotate(name_alias=F("name")) + .order_by("name") + .distinct("name") + .values_list("name_alias", "name") + ) + self.assertSequenceEqual(qs, [("p1", "p1"), ("p2", "p2"), ("p3", "p3")]) + + def test_distinct_on_annotation_duplicating_selected_column(self): + # The annotation duplicates a column selected at an earlier position. + # get_distinct() refers to annotations by alias, which binds to the + # annotation's own position, so ordering by the alias must not refer + # to the earlier position. + qs = ( + Staff.objects.annotate(name_alias=F("name")) + .order_by("name_alias") + .distinct("name_alias") + .values_list("name", "name_alias") + ) + self.assertSequenceEqual(qs, [("p1", "p1"), ("p2", "p2"), ("p3", "p3")]) + + def test_distinct_on_duplicated_selected_columns_with_annotation(self): + # The annotation is selected between the two selections of the column + # it aliases. DISTINCT ON refers to annotations by alias, so it must + # not shadow the first selection of the column itself. + fields = ["stafftag__tag__name", "tags__name", "name"] + qs = ( + Staff.objects.annotate(tag_name=F("tags__name")) + .order_by(*[F(field).asc(nulls_last=True) for field in fields]) + .distinct(*fields) + .values_list("stafftag__tag__name", "tag_name", "tags__name", "name") + ) + self.assertSequenceEqual( + qs, + [ + ("t1", "t1", "t1", "p1"), + (None, None, None, "p1"), + (None, None, None, "p2"), + (None, None, None, "p3"), + ], + ) + + def test_distinct_on_duplicated_raw_sql_annotations(self): + # Identical raw SQL selections compare equal but are evaluated + # independently, so each keeps ordering by its own position. + qs = ( + Staff.objects.annotate( + a=RawSQL("random()", [], output_field=FloatField()), + b=RawSQL("random()", [], output_field=FloatField()), + ) + .values("a", "b", "id") + .distinct("name") + .order_by("name", "b") + ) + self.assertEqual(len(qs), 3) + def test_disallowed_update_distinct_on(self): qs = Staff.objects.distinct("organisation").order_by("organisation") msg = "Cannot call update() after .distinct(*fields)." diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index 9314fa05b082..acdfbfabfc6f 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -368,6 +368,94 @@ def test_in_bulk_values_fields(self): {self.a1.pk: {"headline": "Article 1"}}, ) + def test_in_bulk_values_annotation(self): + arts = ( + Article.objects.annotate(author_name=F("author__name")) + .values("headline", "author_name") + .in_bulk([self.a1.pk]) + ) + self.assertEqual( + arts, + { + self.a1.pk: { + "headline": "Article 1", + "author_name": "Author 1", + } + }, + ) + + def test_in_bulk_values_annotation_all_fields(self): + arts = ( + Article.objects.annotate(author_name=F("author__name")) + .values() + .in_bulk([self.a1.pk]) + ) + self.assertEqual( + arts, + { + self.a1.pk: { + "id": self.a1.pk, + "author_id": self.au1.pk, + "headline": "Article 1", + "pub_date": self.a1.pub_date, + "slug": "a1", + "author_name": "Author 1", + } + }, + ) + + def test_in_bulk_values_extra_select_all_fields(self): + arts = ( + Article.objects.extra(select={"marker": "1"}).values().in_bulk([self.a1.pk]) + ) + self.assertEqual( + arts, + { + self.a1.pk: { + "marker": 1, + "id": self.a1.pk, + "author_id": self.au1.pk, + "headline": "Article 1", + "pub_date": self.a1.pub_date, + "slug": "a1", + } + }, + ) + + def test_in_bulk_values_list_annotation(self): + arts = ( + Article.objects.annotate(author_name=F("author__name")) + .values_list("author_name", "headline") + .in_bulk([self.a1.pk]) + ) + self.assertEqual(arts, {self.a1.pk: ("Author 1", "Article 1")}) + + def test_in_bulk_values_list_annotation_before_pk(self): + arts = ( + Article.objects.annotate(author_name=F("author__name")) + .values_list("author_name", "pk") + .in_bulk([self.a1.pk]) + ) + self.assertEqual(arts, {self.a1.pk: ("Author 1", self.a1.pk)}) + + def test_in_bulk_values_list_named_annotation(self): + arts = ( + Article.objects.annotate(author_name=F("author__name")) + .values_list("headline", "author_name", named=True) + .in_bulk([self.a1.pk]) + ) + article = arts[self.a1.pk] + self.assertEqual(article._fields, ("pk", "headline", "author_name")) + self.assertEqual(article, (self.a1.pk, "Article 1", "Author 1")) + + def test_in_bulk_values_list_flat_annotation(self): + arts = ( + Article.objects.annotate(author_name=F("author__name")) + .values_list("author_name", flat=True) + .in_bulk([self.a1.pk]) + ) + self.assertEqual(arts, {self.a1.pk: "Author 1"}) + def test_in_bulk_values_fields_including_pk(self): arts = Article.objects.values("pk", "headline").in_bulk([self.a1.pk]) self.assertEqual( @@ -1087,6 +1175,28 @@ def test_in(self): def test_in_empty_list(self): self.assertSequenceEqual(Article.objects.filter(id__in=[]), []) + def test_in_iterator_rhs(self): + tests = [ + ("direct values", [self.a1.id, self.a2.id]), + ("expression", [self.a1.id, Value(self.a2.id)]), + ] + for case, values in tests: + with self.subTest(case=case): + self.assertCountEqual( + Article.objects.alias(article_id=F("id")).filter( + article_id__in=iter(values) + ), + [self.a1, self.a2], + ) + + def test_range_iterator_rhs(self): + self.assertCountEqual( + Article.objects.alias(article_id=F("id")).filter( + article_id__range=iter([self.a1.id, self.a2.id]) + ), + [self.a1, self.a2], + ) + def test_in_different_database(self): with self.assertRaisesMessage( ValueError,