Fix admin search and autocomplete for Owner model (#12987)

Owner uses a GenericForeignKey to User or Group where 'name' is a method rather than a database field. Standard Django admin search caused FieldError when autocomplete looked up the responsible field. Implemented custom get_search_results on OwnerAdmin to properly filter User and Group instances and map them to Owner records.

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Bhumin PaladiyaandMatthias Mair authored and GitHub committed 2026-10-13 09:09:07 +11:00
1 parent cbcb81f3d7
commit db90e48a7d
2 files changed
+131

No files matched your search

+41
View File
@@ -6,6 +6,9 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.text import smart_split
from django.utils.translation import gettext_lazy as _
from users.models import ApiToken, Owner, RuleSet
@@ -164,6 +167,44 @@ class OwnerAdmin(admin.ModelAdmin):
"""Custom admin interface for the Owner model."""
search_fields = ['name']
ordering = ['owner_type', 'owner_id']
def get_search_results(self, request, queryset, search_term):
"""Custom search implementation for Owner model.
An Owner is a generic foreign key to either a User or a Group instance.
Standard Django search_fields lookup fails because 'name' is a method,
not a database field, on the Owner model.
"""
# Call super with empty search term to avoid looking up non-existent 'name' field
queryset, _ = super().get_search_results(request, queryset, '')
if not search_term:
return queryset, False
user_type = ContentType.objects.get_for_model(User)
group_type = ContentType.objects.get_for_model(Group)
for bit in smart_split(search_term):
if not bit:
continue
user_q = (
Q(username__icontains=bit)
| Q(first_name__icontains=bit)
| Q(last_name__icontains=bit)
| Q(email__icontains=bit)
)
matching_users = User.objects.filter(user_q).values_list('pk', flat=True)
matching_groups = Group.objects.filter(name__icontains=bit).values_list(
'pk', flat=True
)
bit_q = Q(owner_type=user_type, owner_id__in=matching_users) | Q(
owner_type=group_type, owner_id__in=matching_groups
)
queryset = queryset.filter(bit_q)
return queryset, True
admin.site.unregister(User)
+90
View File
@@ -418,6 +418,96 @@ class AdminTest(AdminTestCase):
# Additionally test str fnc
self.assertEqual(str(my_token), my_token.token)
def test_owner_admin_search(self):
"""Test search functionality in OwnerAdmin."""
from django.contrib.admin.sites import site
from django.contrib.auth import get_user_model
from django.test import RequestFactory
from users.admin import OwnerAdmin
owner_admin = OwnerAdmin(Owner, site)
request = RequestFactory().get('/admin/')
request.user = self.user
# Create distinct user and group
test_user = get_user_model().objects.create_user(
username='searchable_user',
first_name='Searchy',
last_name='Tester',
email='searchy@example.com',
password='password',
)
test_group = Group.objects.create(name='SearchableGroup')
user_owner = Owner.get_owner(test_user)
group_owner = Owner.get_owner(test_group)
# Empty search returns full queryset
qs, use_distinct = owner_admin.get_search_results(
request, Owner.objects.all(), ''
)
self.assertFalse(use_distinct)
self.assertIn(user_owner, qs)
self.assertIn(group_owner, qs)
# Search by username
qs, use_distinct = owner_admin.get_search_results(
request, Owner.objects.all(), 'searchable_user'
)
self.assertTrue(use_distinct)
self.assertIn(user_owner, qs)
self.assertNotIn(group_owner, qs)
# Search by first name
qs, _ = owner_admin.get_search_results(request, Owner.objects.all(), 'Searchy')
self.assertIn(user_owner, qs)
self.assertNotIn(group_owner, qs)
# Search by last name
qs, _ = owner_admin.get_search_results(request, Owner.objects.all(), 'Tester')
self.assertIn(user_owner, qs)
self.assertNotIn(group_owner, qs)
# Search by email
qs, _ = owner_admin.get_search_results(
request, Owner.objects.all(), 'searchy@example.com'
)
self.assertIn(user_owner, qs)
self.assertNotIn(group_owner, qs)
# Search by group name
qs, _ = owner_admin.get_search_results(
request, Owner.objects.all(), 'SearchableGroup'
)
self.assertIn(group_owner, qs)
self.assertNotIn(user_owner, qs)
# Search non-existent term
qs, _ = owner_admin.get_search_results(
request, Owner.objects.all(), 'nonexistentxyzterm'
)
self.assertEqual(qs.count(), 0)
def test_owner_admin_autocomplete(self):
"""Test admin autocomplete endpoint for SalesOrder responsible field."""
url = reverse('admin:autocomplete')
response = self.get(
url,
data={
'term': self.user.username,
'app_label': 'order',
'model_name': 'salesorder',
'field_name': 'responsible',
},
expected_code=200,
)
data = response.json()
self.assertIn('results', data)
user_owner = Owner.get_owner(self.user)
owner_ids = [res['id'] for res in data['results']]
self.assertIn(str(user_owner.pk), owner_ids)
class UserProfileTest(InvenTreeAPITestCase):
"""Tests for the user profile API endpoints."""