mirror of
https://github.com/inventree/InvenTree.git
synced 2026-10-15 14:40:58 +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 import get_user_model
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.contrib.auth.models import Group
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from users.models import ApiToken, Owner, RuleSet
|
from users.models import ApiToken, Owner, RuleSet
|
||||||
@@ -164,6 +167,44 @@ class OwnerAdmin(admin.ModelAdmin):
|
|||||||
"""Custom admin interface for the Owner model."""
|
"""Custom admin interface for the Owner model."""
|
||||||
|
|
||||||
search_fields = ['name']
|
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)
|
admin.site.unregister(User)
|
||||||
|
|||||||
@@ -418,6 +418,96 @@ class AdminTest(AdminTestCase):
|
|||||||
# Additionally test str fnc
|
# Additionally test str fnc
|
||||||
self.assertEqual(str(my_token), my_token.token)
|
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):
|
class UserProfileTest(InvenTreeAPITestCase):
|
||||||
"""Tests for the user profile API endpoints."""
|
"""Tests for the user profile API endpoints."""
|
||||||
|
|||||||
Reference in new issue
Block a user