diff options
| author | mlissner <mlissner@michaeljaylissner.com> | 2026-01-15 12:30:21 -0800 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-01-30 11:45:39 -0500 |
| commit | 4cecf3039586ea738afafb9a28c946bff42c37c1 (patch) | |
| tree | 5d822147b21b499494ceff4b533d32bd34fd2f76 /tests/admin_changelist | |
| parent | b25bc2441827a31b5629e9f79ad7296b992648a2 (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.py | 7 | ||||
| -rw-r--r-- | tests/admin_changelist/tests.py | 72 |
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) |
