瀏覽代碼

Fixed #26001 -- Fixed non-string field exact lookups in ModelAdmin.search_fields.

0saurabh0 4 月之前
父節點
當前提交
f223729f8f
共有 3 個文件被更改,包括 48 次插入3 次删除
  1. 28 3
      django/contrib/admin/options.py
  2. 1 0
      tests/admin_changelist/admin.py
  3. 19 0
      tests/admin_changelist/tests.py

+ 28 - 3
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]:

+ 1 - 0
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")

+ 19 - 0
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,