Opened 3 weeks ago
Last modified 10 days ago
#37263 assigned Bug
Admin changelist search crashes (500) on `__exact` search_fields with choices
| Reported by: | Adam Johnson | Owned by: | Adam Johnson |
|---|---|---|---|
| Component: | contrib.admin | Version: | 6.1 |
| Severity: | Release blocker | Keywords: | |
| Cc: | Mike Lissner | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Regression in 4cecf3039586ea738afafb9a28c946bff42c37c1 (#36865), which replaced Cast-based comparison of non-text __exact search fields with per-term validation via the model field's formfield().to_python().
That validation is insufficient for two kinds of fields:
1. Crash (HTTP 500) for fields with choices
For a model field with choices (e.g. IntegerField(choices=...)), formfield() returns a TypedChoiceField whose to_python() returns the raw string unvalidated.
The term then reaches the ORM and IntegerField.get_prep_value() raises ValueError: invalid literal for int() with base 10: 'john'.
Since ModelAdmin.get_search_results() is called outside the IncorrectLookupParameters handling in ChangeList.get_queryset() (django/contrib/admin/views/main.py), the error propagates as a server error.
Minimal repro:
class Client(models.Model): name = models.CharField(max_length=30) status = models.IntegerField(choices=[(1, "Active"), (2, "Archived")]) class ClientAdmin(admin.ModelAdmin): search_fields = ["name", "status__exact"]
Searching for john in the changelist returns HTTP 500 on 6.1 and main; on 6.0 it returned the rows whose name matches.
2. Over-matching for BooleanField
For BooleanField __exact entries, forms.BooleanField.to_python() maps almost any string to True (only "false"/"0" map to False; nothing raises), so any search term OR-matches every row with a True value.
class Account(models.Model): name = models.CharField(max_length=30) active = models.BooleanField(default=True) class AccountAdmin(admin.ModelAdmin): search_fields = ["name", "active__exact"]
Searching for john returns every active account on 6.1 instead of just john (6.0 behavior).
Change History (7)
comment:1 by , 3 weeks ago
| Has patch: | set |
|---|
comment:2 by , 3 weeks ago
| Cc: | added |
|---|---|
| Patch needs improvement: | set |
| Summary: | Admin changelist search crashes (500) on `__exact` search_fields with choices and over-matches on BooleanField → Admin changelist search crashes (500) on `__exact` search_fields with choices |
| Triage Stage: | Unreviewed → Accepted |
follow-up: 4 comment:3 by , 3 weeks ago
BooleanField: The commit message explains that there is an increase in search permissiveness for values that can be normalized by formfield.to_python(). So for me, the permissive matching on truthy values is fine.
Did you understand that a search for an arbitrary string now matches every row where any boolean field is true? That’s a huge regression to me. Previously, you'd need to serach for true to match truthy rows only.
Try running the new tests on the current main: https://github.com/django/django/pull/21754/changes#diff-640602cc17dc3e9a26db47f58b40a1289d5c0f1b5b0fe547810b1d962f6cde8bR996
TypedChoiceField: I think this use case gives us a reason to reopen #34156, which, when you connect the dots a little further, says that TypedChoiceField is violating the contract that to_python() should return the correct python types.
Oh yeah, that’s a problem! Thanks for looking into it.
comment:4 by , 2 weeks ago
Replying to Adam Johnson:
Did you understand that a search for an arbitrary string now matches every row where any boolean field is true? That’s a huge regression to me. Previously, you'd need to serach for
trueto match truthy rows only.
Ah, no, I didn't contemplate the effect of all the terms being OR'd together, even though it's right there in your report, so thanks for connecting those dots for me. Agree we have to do something! Probably a distinct formfield for search. Mabye with an overridable datastructure to hold it so that it doesn't overload self.formfield_for_dbfield?
comment:5 by , 12 days ago
| Patch needs improvement: | unset |
|---|
I pushed a couple proposed tweaks to reduce the types of exceptions caught, and to push the special cases out of inline logic and into custom form classes where possible. It's ready for opinions.
follow-up: 7 comment:6 by , 11 days ago
| Patch needs improvement: | set |
|---|
Should probably look into making ChangeList.search_form_class more field-granular.
comment:7 by , 10 days ago
| Patch needs improvement: | unset |
|---|
Replying to Jacob Walls:
Should probably look into making
ChangeList.search_form_classmore field-granular.
Never mind, this idea turned out to be awkward, since def get_search_results is on ModelAdmin itself, so this would introduce a two-way reference back to the ChangeList.
Thanks. For each case:
BooleanField: The commit message explains that there is an increase in search permissiveness for values that can be normalized byformfield.to_python(). So for me, the permissive matching on truthy values is fine. It would be unexpected for me forNullBooleanFieldto yield a search termNonethat will never match a not-null database column.By the way, do you think we should have done this, to allow folks to customize? I'm not sure if that's overloading the responsibility for this hook:
django/contrib/admin/options.py
validate_field.formfield()TypedChoiceField: I think this use case gives us a reason to reopen #34156, which, when you connect the dots a little further, says thatTypedChoiceFieldis violating the contract thatto_python()should return the correct python types. The original ask in #21397 was just for coercion to produce values outsidechoices, not different python types. I'll post a draft patch on #34156.The provided PR does this:
... but I'm worried about a new can of worms/inconsistencies there.
I'll accept this for the ChoiceField issue. I think I'd rather special-case the problematic cases we know about (
forms.TypedChoiceField) and let that light a fire under us to fix it in the right layer for the next cycle (and also avoid re-committing to problematic performance with casts).