summaryrefslogtreecommitdiff
path: root/tests/admin_changelist
diff options
context:
space:
mode:
authormlissner <mlissner@michaeljaylissner.com>2026-01-15 12:30:21 -0800
committerJacob Walls <jacobtylerwalls@gmail.com>2026-01-30 11:45:39 -0500
commit4cecf3039586ea738afafb9a28c946bff42c37c1 (patch)
tree5d822147b21b499494ceff4b533d32bd34fd2f76 /tests/admin_changelist
parentb25bc2441827a31b5629e9f79ad7296b992648a2 (diff)
Fixed #36865 -- Removed casting from exact lookups in admin searches.
Instead of casting non-text fields to CharField (which prevents index usage), skip exact lookups when the search term fails formfield.to_python(). This preserves index usage for valid searches while gracefully handling invalid search terms by simply not including them in the query for that field. For multi-term searches like 'foo 123' on search_fields=['name', 'age__exact']: - 'foo': invalid for age, so only name lookup is used - '123': valid for both, so both lookups are used This entails a slight increase in permissiveness for search terms that can be normalized by formfield.to_python().
Diffstat (limited to 'tests/admin_changelist')
-rw-r--r--tests/admin_changelist/models.py7
-rw-r--r--tests/admin_changelist/tests.py72
2 files changed, 79 insertions, 0 deletions
diff --git a/tests/admin_changelist/models.py b/tests/admin_changelist/models.py
index a84c27a066..0b594300d2 100644
--- a/tests/admin_changelist/models.py
+++ b/tests/admin_changelist/models.py
@@ -140,3 +140,10 @@ class CharPK(models.Model):
class ProxyUser(User):
class Meta:
proxy = True
+
+
+class MixedFieldsModel(models.Model):
+ """Model with multiple field types for testing search validation."""
+
+ int_field = models.IntegerField(null=True, blank=True)
+ json_field = models.JSONField(null=True, blank=True)
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index 7a50dbff77..e0772a3e6d 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -66,6 +66,7 @@ from .models import (
Group,
Invitation,
Membership,
+ MixedFieldsModel,
Musician,
OrderedObject,
Parent,
@@ -868,6 +869,77 @@ class ChangeListTests(TestCase):
cl = m.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [])
+ def test_exact_lookup_mixed_terms(self):
+ """
+ Multi-term search validates each term independently.
+
+ For 'foo 123' with search_fields=['name__icontains', 'age__exact']:
+ - 'foo': age lookup skipped (invalid), name lookup used
+ - '123': both lookups used (valid for age)
+ No Cast should be used; invalid lookups are simply skipped.
+ """
+ child = Child.objects.create(name="foo123", age=123)
+ Child.objects.create(name="other", age=456)
+ m = admin.ModelAdmin(Child, custom_site)
+ m.search_fields = ["name__icontains", "age__exact"]
+
+ request = self.factory.get("/", data={SEARCH_VAR: "foo 123"})
+ request.user = self.superuser
+
+ # One result matching on foo and 123.
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [child])
+
+ # "xyz" - invalid for age (skipped), no match for name either.
+ request = self.factory.get("/", data={SEARCH_VAR: "xyz"})
+ request.user = self.superuser
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [])
+
+ def test_exact_lookup_with_more_lenient_formfield(self):
+ """
+ Exact lookups on BooleanField use formfield().to_python() for lenient
+ parsing. Using model field's to_python() would reject 'false' whereas
+ the form field accepts it.
+ """
+ obj = UnorderedObject.objects.create(bool=False)
+ UnorderedObject.objects.create(bool=True)
+ m = admin.ModelAdmin(UnorderedObject, custom_site)
+ m.search_fields = ["bool__exact"]
+
+ # 'false' is accepted by form field but rejected by model field.
+ request = self.factory.get("/", data={SEARCH_VAR: "false"})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [obj])
+
+ def test_exact_lookup_validates_each_field_independently(self):
+ """
+ Each field validates the search term independently without leaking
+ converted values between fields.
+
+ "3." is valid for IntegerField (converts to 3) but invalid for
+ JSONField. The converted value must not leak to the JSONField check.
+ """
+ # obj_int has int_field=3, should match "3." via IntegerField.
+ obj_int = MixedFieldsModel.objects.create(
+ int_field=3, json_field={"key": "value"}
+ )
+ # obj_json has json_field=3, should NOT match "3." because "3." is
+ # invalid JSON.
+ MixedFieldsModel.objects.create(int_field=99, json_field=3)
+ m = admin.ModelAdmin(MixedFieldsModel, custom_site)
+ m.search_fields = ["int_field__exact", "json_field__exact"]
+
+ # "3." is valid for int (becomes 3) but invalid JSON.
+ # Only obj_int should match via int_field.
+ request = self.factory.get("/", data={SEARCH_VAR: "3."})
+ request.user = self.superuser
+
+ cl = m.get_changelist_instance(request)
+ self.assertCountEqual(cl.queryset, [obj_int])
+
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)