mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Refs #26001 -- Handled relationship exact lookups in ModelAdmin.search_fields.
This commit is contained in:
parent
5bd5805811
commit
5fa4ccab7e
@ -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]
|
|
||||||
try:
|
|
||||||
field_obj = queryset.model._meta.get_field(field_name)
|
|
||||||
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)
|
orm_lookups.append(lookup)
|
||||||
|
if str_alias:
|
||||||
|
str_aliases[lookup] = str_alias
|
||||||
|
|
||||||
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):
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user