summaryrefslogtreecommitdiff
path: root/django/contrib/admin
diff options
context:
space:
mode:
Diffstat (limited to 'django/contrib/admin')
-rw-r--r--django/contrib/admin/options.py63
1 files changed, 37 insertions, 26 deletions
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 69b7031e82..9c787d2329 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -41,7 +41,6 @@ from django.core.exceptions import (
from django.core.paginator import Paginator
from django.db import models, router, transaction
from django.db.models.constants import LOOKUP_SEP
-from django.db.models.functions import Cast
from django.forms.formsets import DELETION_FIELD_NAME, all_valid
from django.forms.models import (
BaseInlineFormSet,
@@ -1137,6 +1136,12 @@ class ModelAdmin(BaseModelAdmin):
# Apply keyword searches.
def construct_search(field_name):
+ """
+ Return a tuple of (lookup, field_to_validate).
+
+ field_to_validate is set for non-text exact lookups so that
+ invalid search terms can be skipped (preserving index usage).
+ """
if field_name.startswith("^"):
return "%s__istartswith" % field_name.removeprefix("^"), None
elif field_name.startswith("="):
@@ -1148,7 +1153,7 @@ class ModelAdmin(BaseModelAdmin):
lookup_fields = field_name.split(LOOKUP_SEP)
# Go through the fields, following all relations.
prev_field = None
- for i, path_part in enumerate(lookup_fields):
+ for path_part in lookup_fields:
if path_part == "pk":
path_part = opts.pk.name
try:
@@ -1159,15 +1164,9 @@ class ModelAdmin(BaseModelAdmin):
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
+ # Use prev_field to validate the search term.
+ return field_name, prev_field
+ return field_name, None
else:
prev_field = field
if hasattr(field, "path_infos"):
@@ -1179,30 +1178,42 @@ class ModelAdmin(BaseModelAdmin):
may_have_duplicates = False
search_fields = self.get_search_fields(request)
if search_fields and search_term:
- str_aliases = {}
orm_lookups = []
for field in search_fields:
- lookup, str_alias = construct_search(str(field))
- orm_lookups.append(lookup)
- if str_alias:
- str_aliases[lookup] = str_alias
-
- if str_aliases:
- queryset = queryset.alias(**str_aliases)
+ orm_lookups.append(construct_search(str(field)))
term_queries = []
for bit in smart_split(search_term):
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
bit = unescape_string_literal(bit)
- or_queries = models.Q.create(
- [(orm_lookup, bit) for orm_lookup in orm_lookups],
- connector=models.Q.OR,
- )
- term_queries.append(or_queries)
- queryset = queryset.filter(models.Q.create(term_queries))
+ # Build term lookups, skipping values invalid for their field.
+ bit_lookups = []
+ for orm_lookup, validate_field in orm_lookups:
+ if validate_field is not None:
+ formfield = validate_field.formfield()
+ try:
+ if formfield is not None:
+ value = formfield.to_python(bit)
+ else:
+ # Fields like AutoField lack a form field.
+ value = validate_field.to_python(bit)
+ except ValidationError:
+ # Skip this lookup for invalid values.
+ continue
+ else:
+ value = bit
+ bit_lookups.append((orm_lookup, value))
+ if bit_lookups:
+ or_queries = models.Q.create(bit_lookups, connector=models.Q.OR)
+ term_queries.append(or_queries)
+ else:
+ # No valid lookups: add a filter that returns nothing.
+ term_queries.append(models.Q(pk__in=[]))
+ if term_queries:
+ queryset = queryset.filter(models.Q.create(term_queries))
may_have_duplicates |= any(
lookup_spawns_duplicates(self.opts, search_spec)
- for search_spec in orm_lookups
+ for search_spec, _ in orm_lookups
)
return queryset, may_have_duplicates