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 Adam Johnson, 3 weeks ago

Has patch: set

comment:2 by Jacob Walls, 3 weeks ago

Cc: Mike Lissner added
Patch needs improvement: set
Summary: Admin changelist search crashes (500) on `__exact` search_fields with choices and over-matches on BooleanFieldAdmin changelist search crashes (500) on `__exact` search_fields with choices
Triage Stage: UnreviewedAccepted

Thanks. For each case:

  • 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. It would be unexpected for me for NullBooleanField to yield a search term None that 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

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index 184d11f28a..05c06ad7f6 100644
    a b class ModelAdmin(BaseModelAdmin):  
    13631363                bit_lookups = []
    13641364                for orm_lookup, validate_field in orm_lookups:
    13651365                    if validate_field is not None:
    1366                         formfield = validate_field.formfield()
     1366                        formfield = self.formfield_for_dbfield(validate_field, request)
    13671367                        try:
    13681368                            if formfield is not None:
    13691369                                value = formfield.to_python(bit)
  • 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. The original ask in #21397 was just for coercion to produce values outside choices, not different python types. I'll post a draft patch on #34156.

The provided PR does this:

                                if isinstance(value, str):
                                    # Form fields such as TypedChoiceField may
                                    # return the string unconverted. Ensure
                                    # the model field accepts the value, so
                                    # that filtering on it cannot crash.
                                    value = validate_field.to_python(value)

... 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).

comment:3 by Adam Johnson, 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.

in reply to:  3 comment:4 by Jacob Walls, 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 true to 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?

Last edited 2 weeks ago by Jacob Walls (previous) (diff)

comment:5 by Jacob Walls, 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.

comment:6 by Jacob Walls, 11 days ago

Patch needs improvement: set

Should probably look into making ChangeList.search_form_class more field-granular.

in reply to:  6 comment:7 by Jacob Walls, 10 days ago

Patch needs improvement: unset

Replying to Jacob Walls:

Should probably look into making ChangeList.search_form_class more 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.

Note: See TracTickets for help on using tickets.
Back to Top