summaryrefslogtreecommitdiff
path: root/django/contrib/admin/options.py
diff options
context:
space:
mode:
authorSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-11-04 16:09:55 +0100
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-11-05 09:06:14 +0100
commit5fa4ccab7e42e86fa4a0681d21bd1326c9c5eac3 (patch)
treeba4a1d65c8b3e47e5c7c02971851a64b63a44c13 /django/contrib/admin/options.py
parent5bd58058116d2e6f07b72881218548fd9904167e (diff)
Refs #26001 -- Handled relationship exact lookups in ModelAdmin.search_fields.
Diffstat (limited to 'django/contrib/admin/options.py')
-rw-r--r--django/contrib/admin/options.py53
1 files changed, 24 insertions, 29 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 78063a134d..69b0cc0373 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1178,17 +1178,17 @@ class ModelAdmin(BaseModelAdmin):
# Apply keyword searches.
def construct_search(field_name):
if field_name.startswith("^"):
- return "%s__istartswith" % field_name.removeprefix("^")
+ return "%s__istartswith" % field_name.removeprefix("^"), None
elif field_name.startswith("="):
- return "%s__iexact" % field_name.removeprefix("=")
+ return "%s__iexact" % field_name.removeprefix("="), None
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.
opts = queryset.model._meta
lookup_fields = field_name.split(LOOKUP_SEP)
# Go through the fields, following all relations.
prev_field = None
- for path_part in lookup_fields:
+ for i, path_part in enumerate(lookup_fields):
if path_part == "pk":
path_part = opts.pk.name
try:
@@ -1196,44 +1196,39 @@ class ModelAdmin(BaseModelAdmin):
except FieldDoesNotExist:
# Use valid query lookups.
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:
prev_field = field
if hasattr(field, "path_infos"):
# Update opts to follow the relation.
opts = field.path_infos[-1].to_opts
# Otherwise, use the field with icontains.
- return "%s__icontains" % field_name
+ return "%s__icontains" % field_name, None
may_have_duplicates = False
search_fields = self.get_search_fields(request)
if search_fields and search_term:
- str_annotations = {}
+ str_aliases = {}
orm_lookups = []
for field in search_fields:
- if field.endswith("__exact"):
- 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)
+ lookup, str_alias = construct_search(str(field))
+ orm_lookups.append(lookup)
+ if str_alias:
+ str_aliases[lookup] = str_alias
- if str_annotations:
- queryset = queryset.annotate(**str_annotations)
+ if str_aliases:
+ queryset = queryset.alias(**str_aliases)
term_queries = []
for bit in smart_split(search_term):