0
0
mirror of https://github.com/django/django.git synced 2024-11-24 02:47:35 +01:00

Refs #26001 -- Handled relationship exact lookups in ModelAdmin.search_fields.

This commit is contained in:
Sarah Boyce 2024-11-04 16:09:55 +01:00
parent 5bd5805811
commit 5fa4ccab7e
3 changed files with 47 additions and 29 deletions

View File

@ -1178,17 +1178,17 @@ class ModelAdmin(BaseModelAdmin):
# Apply keyword searches. # Apply keyword searches.
def construct_search(field_name): def construct_search(field_name):
if field_name.startswith("^"): if field_name.startswith("^"):
return "%s__istartswith" % field_name.removeprefix("^") return "%s__istartswith" % field_name.removeprefix("^"), None
elif field_name.startswith("="): elif field_name.startswith("="):
return "%s__iexact" % field_name.removeprefix("=") return "%s__iexact" % field_name.removeprefix("="), None
elif field_name.startswith("@"): elif field_name.startswith("@"):
return "%s__search" % field_name.removeprefix("@") return "%s__search" % field_name.removeprefix("@"), None
# Use field_name if it includes a lookup. # Use field_name if it includes a lookup.
opts = queryset.model._meta opts = queryset.model._meta
lookup_fields = field_name.split(LOOKUP_SEP) lookup_fields = field_name.split(LOOKUP_SEP)
# Go through the fields, following all relations. # Go through the fields, following all relations.
prev_field = None prev_field = None
for path_part in lookup_fields: for i, path_part in enumerate(lookup_fields):
if path_part == "pk": if path_part == "pk":
path_part = opts.pk.name path_part = opts.pk.name
try: try:
@ -1196,44 +1196,39 @@ class ModelAdmin(BaseModelAdmin):
except FieldDoesNotExist: except FieldDoesNotExist:
# Use valid query lookups. # Use valid query lookups.
if prev_field and prev_field.get_lookup(path_part): if prev_field and prev_field.get_lookup(path_part):
return field_name if path_part == "exact" and not isinstance(
prev_field, (models.CharField, models.TextField)
):
field_name_without_exact = "__".join(lookup_fields[:i])
alias = Cast(
field_name_without_exact,
output_field=models.CharField(),
)
alias_name = "_".join(lookup_fields[:i])
return f"{alias_name}_str", alias
else:
return field_name, None
else: else:
prev_field = field prev_field = field
if hasattr(field, "path_infos"): if hasattr(field, "path_infos"):
# Update opts to follow the relation. # Update opts to follow the relation.
opts = field.path_infos[-1].to_opts opts = field.path_infos[-1].to_opts
# Otherwise, use the field with icontains. # Otherwise, use the field with icontains.
return "%s__icontains" % field_name return "%s__icontains" % field_name, None
may_have_duplicates = False may_have_duplicates = False
search_fields = self.get_search_fields(request) search_fields = self.get_search_fields(request)
if search_fields and search_term: if search_fields and search_term:
str_annotations = {} str_aliases = {}
orm_lookups = [] orm_lookups = []
for field in search_fields: for field in search_fields:
if field.endswith("__exact"): lookup, str_alias = construct_search(str(field))
field_name = field.rsplit("__exact", 1)[0] orm_lookups.append(lookup)
try: if str_alias:
field_obj = queryset.model._meta.get_field(field_name) str_aliases[lookup] = str_alias
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: if str_aliases:
queryset = queryset.annotate(**str_annotations) queryset = queryset.alias(**str_aliases)
term_queries = [] term_queries = []
for bit in smart_split(search_term): for bit in smart_split(search_term):

View File

@ -56,6 +56,7 @@ class ChildAdmin(admin.ModelAdmin):
class GrandChildAdmin(admin.ModelAdmin): class GrandChildAdmin(admin.ModelAdmin):
list_display = ["name", "parent__name", "parent__parent__name"] list_display = ["name", "parent__name", "parent__parent__name"]
search_fields = ["parent__name__exact", "parent__age__exact"]
site.register(GrandChild, GrandChildAdmin) site.register(GrandChild, GrandChildAdmin)

View File

@ -879,6 +879,28 @@ class ChangeListTests(TestCase):
cl = model_admin.get_changelist_instance(request) cl = model_admin.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, expected_result) self.assertCountEqual(cl.queryset, expected_result)
def test_search_with_exact_lookup_relationship_field(self):
child = Child.objects.create(name="I am a child", age=11)
grandchild = GrandChild.objects.create(name="I am a grandchild", parent=child)
model_admin = GrandChildAdmin(GrandChild, custom_site)
request = self.factory.get("/", data={SEARCH_VAR: "'I am a child'"})
request.user = self.superuser
cl = model_admin.get_changelist_instance(request)
self.assertCountEqual(cl.queryset, [grandchild])
for search_term, expected_result in [
("11", [grandchild]),
("'I am a child'", [grandchild]),
("1", []),
("A", []),
("random", []),
]:
request = self.factory.get("/", data={SEARCH_VAR: search_term})
request.user = self.superuser
with self.subTest(search_term=search_term):
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): 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, If a ManyToManyField is in list_filter but isn't in any lookup params,