Skip to content

Commit d98c870

Browse files
committed
feat: add GET API endpoint to list enterprise admin members
1 parent 078fa56 commit d98c870

4 files changed

Lines changed: 440 additions & 0 deletions

File tree

enterprise/api/v1/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2375,3 +2375,18 @@ class EnterpriseSSOUserInfoRequestSerializer(serializers.Serializer):
23752375
"""
23762376
org_id = serializers.CharField(required=True)
23772377
external_user_id = serializers.CharField(required=True)
2378+
2379+
2380+
class EnterpriseAdminMemberSerializer(serializers.Serializer):
2381+
"""
2382+
Response serializer for enterprise admin members list endpoint.
2383+
2384+
Represents both active admins and pending (invited) admins with a
2385+
unified schema derived from annotated ORM querysets.
2386+
"""
2387+
id = serializers.IntegerField(source='admin_id')
2388+
name = serializers.CharField(allow_null=True)
2389+
email = serializers.EmailField()
2390+
invited_date = serializers.DateTimeField(allow_null=True, format='%b %d, %Y')
2391+
joined_date = serializers.DateTimeField(allow_null=True, format='%b %d, %Y')
2392+
status = serializers.CharField()

enterprise/api/v1/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
analytics_summary,
1111
coupon_codes,
1212
default_enterprise_enrollments,
13+
enterprise_admin_members,
1314
enterprise_catalog_query,
1415
enterprise_course_enrollment,
1516
enterprise_customer,
@@ -243,6 +244,11 @@
243244
),
244245
name='enterprise-course-enrollment-admin'
245246
),
247+
re_path(
248+
r'^(?P<enterprise_uuid>[A-Za-z0-9-]+)/admins$',
249+
enterprise_admin_members.EnterpriseAdminMembersViewSet.as_view({'get': 'list'}),
250+
name='enterprise-admin-members',
251+
),
246252
]
247253

248254
urlpatterns += router.urls
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""
2+
Views for the ``enterprise-admin-members`` API endpoint.
3+
"""
4+
5+
from edx_rbac.decorators import permission_required
6+
from rest_framework import filters, mixins, permissions, viewsets
7+
from rest_framework.pagination import PageNumberPagination
8+
9+
from django.db.models import CharField, F, Value
10+
from django.db.models.functions import Coalesce
11+
12+
from enterprise import models
13+
from enterprise.api.v1 import serializers
14+
from enterprise.api.v1.views.base_views import EnterpriseViewSet
15+
from enterprise.logging import getEnterpriseLogger
16+
17+
LOGGER = getEnterpriseLogger(__name__)
18+
19+
20+
class EnterpriseAdminMembersPagination(PageNumberPagination):
21+
"""
22+
Pagination class for the enterprise admin members endpoint.
23+
"""
24+
page_size = 10
25+
page_size_query_param = 'page_size'
26+
max_page_size = 100
27+
28+
29+
class EnterpriseAdminMembersViewSet(
30+
EnterpriseViewSet,
31+
mixins.ListModelMixin,
32+
viewsets.GenericViewSet,
33+
):
34+
"""
35+
API for listing enterprise admin members (both active and pending).
36+
37+
GET /{enterprise_uuid}/admins
38+
39+
Returns a paginated list of enterprise administrators with status:
40+
- ``Admin`` -- Active admin (joined)
41+
- ``Pending`` -- Admin invited but not yet joined
42+
43+
Query parameters:
44+
- ``user_query`` -- Filter by name or email (case-insensitive contains)
45+
- ``ordering`` -- One of: name, email, joined_date, invited_date, status
46+
Prefix with ``-`` for descending (e.g. ``-name``)
47+
"""
48+
serializer_class = serializers.EnterpriseAdminMemberSerializer
49+
permission_classes = (permissions.IsAuthenticated,)
50+
pagination_class = EnterpriseAdminMembersPagination
51+
filter_backends = (filters.OrderingFilter,)
52+
53+
# DRF OrderingFilter settings
54+
ordering_fields = ['name', 'email', 'joined_date', 'invited_date', 'status']
55+
ordering = ['name']
56+
57+
@classmethod
58+
def _get_active_admins_qs(cls, enterprise_uuid):
59+
"""
60+
Return annotated ValuesQuerySet of active (joined) admins.
61+
62+
Clears default model ordering to allow safe use with ``.union()``.
63+
"""
64+
return models.EnterpriseCustomerAdmin.objects.filter(
65+
enterprise_customer_user__enterprise_customer__uuid=enterprise_uuid,
66+
enterprise_customer_user__user_fk__is_active=True,
67+
).annotate(
68+
admin_id=F('enterprise_customer_user__id'),
69+
name=Coalesce(
70+
'enterprise_customer_user__user_fk__first_name',
71+
'enterprise_customer_user__user_fk__username',
72+
output_field=CharField(),
73+
),
74+
email=F('enterprise_customer_user__user_fk__email'),
75+
invited_date=Value(None, output_field=CharField()),
76+
joined_date=F('created'),
77+
status=Value('Admin', output_field=CharField()),
78+
).order_by().values(
79+
'admin_id', 'name', 'email', 'invited_date', 'joined_date', 'status',
80+
)
81+
82+
@classmethod
83+
def _get_pending_admins_qs(cls, enterprise_uuid):
84+
"""
85+
Return annotated ValuesQuerySet of pending (invited) admins.
86+
87+
Clears default model ordering to allow safe use with ``.union()``.
88+
"""
89+
return models.PendingEnterpriseCustomerAdminUser.objects.filter(
90+
enterprise_customer__uuid=enterprise_uuid,
91+
).annotate(
92+
admin_id=F('id'),
93+
name=Value(None, output_field=CharField()),
94+
email=F('user_email'),
95+
invited_date=F('created'),
96+
joined_date=Value(None, output_field=CharField()),
97+
status=Value('Pending', output_field=CharField()),
98+
).order_by().values(
99+
'admin_id', 'name', 'email', 'invited_date', 'joined_date', 'status',
100+
)
101+
102+
@permission_required(
103+
'enterprise.can_access_admin_dashboard',
104+
fn=lambda request, *args, **kwargs: kwargs.get('enterprise_uuid'),
105+
)
106+
def list(self, request, *args, **kwargs):
107+
"""
108+
List enterprise admin members with DRF-native ordering and pagination.
109+
"""
110+
return super().list(request, *args, **kwargs)
111+
112+
def get_queryset(self):
113+
"""
114+
Build a union queryset of active + pending admins, scoped to the
115+
requested enterprise. Applies ``user_query`` search filtering
116+
*before* the union (since union querysets do not support ``.filter()``).
117+
"""
118+
enterprise_uuid = self.kwargs.get('enterprise_uuid')
119+
user_query = self.request.query_params.get('user_query', '').strip()
120+
121+
active_qs = self._get_active_admins_qs(enterprise_uuid)
122+
pending_qs = self._get_pending_admins_qs(enterprise_uuid)
123+
124+
if user_query:
125+
active_qs = (
126+
active_qs.filter(name__icontains=user_query) |
127+
active_qs.filter(email__icontains=user_query)
128+
)
129+
pending_qs = pending_qs.filter(email__icontains=user_query)
130+
131+
return active_qs.union(pending_qs)

0 commit comments

Comments
 (0)