diff --git a/src/backend/InvenTree/users/admin.py b/src/backend/InvenTree/users/admin.py index c158b2b43c..d730572195 100644 --- a/src/backend/InvenTree/users/admin.py +++ b/src/backend/InvenTree/users/admin.py @@ -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) diff --git a/src/backend/InvenTree/users/tests.py b/src/backend/InvenTree/users/tests.py index 0e1668f9f3..be206f3994 100644 --- a/src/backend/InvenTree/users/tests.py +++ b/src/backend/InvenTree/users/tests.py @@ -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."""