summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author0saurabh0 <saurabhrai1717@gmail.com>2024-11-01 08:26:10 +0100
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-11-04 09:39:47 +0100
commitf223729f8f8380901473b244c0fc7c7dc397e3f1 (patch)
treeed434b9e50481d160a9804aaeb1fe8a7784d49cb
parentcd4497254e8b52e7d28dda764b6a2567253059f4 (diff)
Fixed #26001 -- Fixed non-string field exact lookups in ModelAdmin.search_fields.
-rw-r--r--django/contrib/admin/options.py31
-rw-r--r--tests/admin_changelist/admin.py1
-rw-r--r--tests/admin_changelist/tests.py19
3 files changed, 48 insertions, 3 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 6d5c0708a3..78063a134d 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -41,6 +41,7 @@ from django.core.exceptions import (
from django.core.paginator import Paginator
from django.db import models, router, transaction
from django.db.models.constants import LOOKUP_SEP
+from django.db.models.functions import Cast
from django.forms.formsets import DELETION_FIELD_NAME, all_valid
from django.forms.models import (
BaseInlineFormSet,
@@ -1207,9 +1208,33 @@ class ModelAdmin(BaseModelAdmin):
may_have_duplicates = False
search_fields = self.get_search_fields(request)
if search_fields and search_term:
- orm_lookups = [
- construct_search(str(search_field)) for search_field in search_fields
- ]
+ str_annotations = {}
+ orm_lookups = []
+ for field in search_fields:
+ if field.endswith("__exact"):
+ field_name = field.rsplit("__exact", 1)[0]
+ try:
+ field_obj = queryset.model._meta.get_field(field_name)
+ except FieldDoesNotExist:
+ lookup = construct_search(field)
+ orm_lookups.append(lookup)
+ continue
+ # Add string cast annotations for non-string exact lookups.
+ if not isinstance(field_obj, (models.CharField, models.TextField)):
+ str_annotations[f"{field_name}_str"] = Cast(
+ field_name, output_field=models.CharField()
+ )
+ orm_lookups.append(f"{field_name}_str__exact")
+ else:
+ lookup = construct_search(field)
+ orm_lookups.append(lookup)
+ else:
+ lookup = construct_search(str(field))
+ orm_lookups.append(lookup)
+
+ if str_annotations:
+ queryset = queryset.annotate(**str_annotations)
+
term_queries = []
for bit in smart_split(search_term):
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py
index 349ef7d465..d9dc498e84 100644
--- a/tests/admin_changelist/admin.py
+++ b/tests/admin_changelist/admin.py
@@ -48,6 +48,7 @@ class ChildAdmin(admin.ModelAdmin):
list_display = ["name", "parent"]
list_per_page = 10
list_filter = ["parent", "age"]
+ search_fields = ["age__exact", "name__exact"]
def get_queryset(self, request):
return super().get_queryset(request).select_related("parent")
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index d8055a809b..a823a72f7d 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -860,6 +860,25 @@ class ChangeListTests(TestCase):
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [abcd])
+ def test_search_with_exact_lookup_for_non_string_field(self):
+ child = Child.objects.create(name="Asher", age=11)
+ model_admin = ChildAdmin(Child, custom_site)
+
+ for search_term, expected_result in [
+ ("11", [child]),
+ ("Asher", [child]),
+ ("1", []),
+ ("A", []),
+ ("random", []),
+ ]:
+ request = self.factory.get("/", data={SEARCH_VAR: search_term})
+ request.user = self.superuser
+ with self.subTest(search_term=search_term):
+ # 1 query for filtered result, 1 for filtered count, 1 for total count.
+ with self.assertNumQueries(3):
+ cl = model_admin.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, expected_result)
+
def test_no_distinct_for_m2m_in_list_filter_without_params(self):
"""
If a ManyToManyField is in list_filter but isn't in any lookup params,