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,