mirror of
https://github.com/inventree/InvenTree.git
synced 2026-10-14 14:11:55 +00:00
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:
1 parent
cbcb81f3d7
commit
db90e48a7d
2 files changed
+131
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in new issue
Block a user