Skip to content

Commit cf985f5

Browse files
committed
feat: Implemented team member assignments endpoint
1 parent a710eea commit cf985f5

5 files changed

Lines changed: 149 additions & 2 deletions

File tree

openedx_authz/rest_api/data.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ class SortField(BaseEnum):
2020
EMAIL = "email"
2121

2222

23+
class AssignmentSortField(BaseEnum):
24+
"""Enum for the role assignment fields to sort by."""
25+
26+
ROLE = "role"
27+
ORG = "org"
28+
SCOPE = "scope"
29+
30+
2331
class SortOrder(BaseEnum):
2432
"""Enum for the order to sort by."""
2533

openedx_authz/rest_api/utils.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from openedx_authz.api.users import is_user_allowed
1818
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE, VIEW_LIBRARY
19-
from openedx_authz.rest_api.data import SearchField, SortField, SortOrder
19+
from openedx_authz.rest_api.data import AssignmentSortField, SearchField, SortField, SortOrder
2020

2121
logger = logging.getLogger(__name__)
2222

@@ -131,6 +131,40 @@ def filter_users(users: list[dict], search: str | None, roles: list[str] | None)
131131
return filtered_users
132132

133133

134+
def sort_assignments(
135+
assignments: list[RoleAssignmentData],
136+
sort_by: AssignmentSortField = AssignmentSortField.ROLE,
137+
order: SortOrder = SortOrder.ASC,
138+
) -> list[RoleAssignmentData]:
139+
"""
140+
Sort role assignments by a given field and order.
141+
142+
Args:
143+
assignments (list[RoleAssignmentData]): The assignments to sort.
144+
sort_by (SortField, optional): The field to sort by. Defaults to AssignmentSortField.ROLE.
145+
order (SortOrder, optional): The order to sort by. Defaults to SortOrder.ASC.
146+
147+
Raises:
148+
ValueError: If the sort field is invalid.
149+
ValueError: If the sort order is invalid.
150+
151+
Returns:
152+
list[RoleAssignmentData]: The sorted assignments.
153+
"""
154+
if sort_by not in AssignmentSortField.values():
155+
raise ValueError(f"Invalid field: '{sort_by}'. Must be one of {AssignmentSortField.values()}")
156+
157+
if order not in SortOrder.values():
158+
raise ValueError(f"Invalid order: '{order}'. Must be one of {SortOrder.values()}")
159+
160+
sorted_assignments = sorted(
161+
assignments,
162+
key=lambda assignment: (assignment.get(sort_by) or "").lower(),
163+
reverse=order == SortOrder.DESC,
164+
)
165+
return sorted_assignments
166+
167+
134168
@dataclass
135169
class UserAssignments:
136170
user: "User"

openedx_authz/rest_api/v1/serializers.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,47 @@ def get_email(self, obj: UserAssignments) -> str:
250250
def get_assignation_count(self, obj: UserAssignments) -> int:
251251
"""Get the assignation count for the given role assignment."""
252252
return len(obj.assignments)
253+
254+
255+
class ListTeamMemberAssignmentsSerializer(serializers.Serializer): # pylint: disable=abstract-method
256+
"""Serializer for listing team member assignments."""
257+
258+
orgs = CaseSensitiveCommaSeparatedListField(required=False, default=[])
259+
roles = CaseSensitiveCommaSeparatedListField(required=False, default=[])
260+
sort_by = serializers.ChoiceField(
261+
required=False,
262+
choices=[(e.value, e.name) for e in SortField],
263+
default=SortField.USERNAME,
264+
)
265+
order = serializers.ChoiceField(
266+
required=False,
267+
choices=[(e.value, e.name) for e in SortOrder],
268+
default=SortOrder.ASC,
269+
)
270+
271+
272+
class TeamMemberAssignmentSerializer(serializers.Serializer): # pylint: disable=abstract-method
273+
"""Serializer for team member assignments."""
274+
275+
role = serializers.SerializerMethodField()
276+
org = serializers.SerializerMethodField()
277+
scope = serializers.SerializerMethodField()
278+
permission_count = serializers.SerializerMethodField()
279+
280+
def get_role(self, obj: api.RoleAssignmentData) -> str:
281+
"""Get the role for the given role assignment."""
282+
return obj.roles[0].external_key if obj.roles else ""
283+
284+
def get_org(self, obj: api.RoleAssignmentData) -> str:
285+
"""Get the org for the given role assignment."""
286+
if isinstance(obj.scope, (api.ContentLibraryData, api.OrgContentLibraryGlobData)):
287+
return obj.scope.org
288+
return ""
289+
290+
def get_scope(self, obj: api.RoleAssignmentData) -> str:
291+
"""Get the scope for the given role assignment."""
292+
return obj.scope.external_key
293+
294+
def get_permission_count(self, obj: api.RoleAssignmentData) -> int:
295+
"""Get the permission count for the given role assignment."""
296+
return len(obj.roles[0].permissions) if obj.roles else 0

openedx_authz/rest_api/v1/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@
1313
path("roles/", views.RoleListView.as_view(), name="role-list"),
1414
path("roles/users/", views.RoleUserAPIView.as_view(), name="role-user-list"),
1515
path("users/", views.TeamMembersAPIView.as_view(), name="user-list"),
16+
path("users/<str:username>/assignments", views.TeamMemberAssignmentsAPIView.as_view(), name="user-assignment-list"),
1617
]

openedx_authz/rest_api/v1/views.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from openedx_authz import api
1818
from openedx_authz.constants import permissions
19-
from openedx_authz.rest_api.data import RoleOperationError, RoleOperationStatus, SortField
19+
from openedx_authz.rest_api.data import AssignmentSortField, RoleOperationError, RoleOperationStatus, SortField
2020
from openedx_authz.rest_api.decorators import authz_permissions, view_auth_classes
2121
from openedx_authz.rest_api.utils import (
2222
UserAssignments,
@@ -25,6 +25,7 @@
2525
get_generic_scope,
2626
get_user_assignment_map,
2727
get_user_map,
28+
sort_assignments,
2829
sort_users,
2930
)
3031
from openedx_authz.rest_api.v1.paginators import AuthZAPIViewPagination
@@ -33,11 +34,13 @@
3334
AddUsersToRoleWithScopeSerializer,
3435
ListRolesWithScopeResponseSerializer,
3536
ListRolesWithScopeSerializer,
37+
ListTeamMemberAssignmentsSerializer,
3638
ListTeamMembersSerializer,
3739
ListUsersInRoleWithScopeSerializer,
3840
PermissionValidationResponseSerializer,
3941
PermissionValidationSerializer,
4042
RemoveUsersFromRoleWithScopeSerializer,
43+
TeamMemberAssignmentSerializer,
4144
TeamMemberSerializer,
4245
UserRoleAssignmentSerializer,
4346
)
@@ -527,3 +530,60 @@ def get(self, request: HttpRequest) -> Response:
527530
paginator = self.pagination_class()
528531
paginated_response_data = paginator.paginate_queryset(sorted_team_members, request)
529532
return paginator.get_paginated_response(paginated_response_data)
533+
534+
535+
@view_auth_classes()
536+
class TeamMemberAssignmentsAPIView(APIView):
537+
"""
538+
API view for listing user role assignments
539+
"""
540+
541+
pagination_class = AuthZAPIViewPagination
542+
543+
@apidocs.schema(
544+
parameters=[
545+
apidocs.query_parameter("orgs", str, description="The orgs to query assignations for"),
546+
apidocs.query_parameter("roles", str, description="The roles to query assignations for"),
547+
apidocs.query_parameter("sort_by", str, description="The field to sort by"),
548+
apidocs.query_parameter("order", str, description="The order to sort by"),
549+
apidocs.query_parameter("page", int, description="Page number for pagination"),
550+
apidocs.query_parameter("page_size", int, description="Number of items per page"),
551+
],
552+
responses={
553+
status.HTTP_200_OK: TeamMemberAssignmentSerializer(many=True),
554+
status.HTTP_400_BAD_REQUEST: "The request parameters are invalid",
555+
status.HTTP_401_UNAUTHORIZED: "The user is not authenticated or does not have the required permissions",
556+
},
557+
)
558+
def get(self, request: HttpRequest, username: str) -> Response:
559+
"""Retrieve all user role assignments."""
560+
serializer = ListTeamMemberAssignmentsSerializer(data=request.query_params)
561+
serializer.is_valid(raise_exception=True)
562+
query_params = serializer.validated_data
563+
564+
user_role_assignments = api.get_user_role_assignments(user_external_key=username)
565+
# Filter assignments based on the user's permissions
566+
user_role_assignments = filter_allowed_assignments(user=request.user, assignments=user_role_assignments)
567+
568+
orgs = query_params.get("orgs")
569+
roles = query_params.get("roles")
570+
order = query_params.get("order")
571+
sort_by = query_params.get("sort_by", AssignmentSortField.ROLE)
572+
573+
if orgs:
574+
# Filter by orgs
575+
user_role_assignments = [a for a in user_role_assignments if a.scope.org in orgs]
576+
577+
if roles:
578+
# Filter by roles
579+
user_role_assignments = [
580+
a for a in user_role_assignments if any(role.external_key in roles for role in a.roles)
581+
]
582+
583+
assignments = TeamMemberAssignmentSerializer(user_role_assignments, many=True)
584+
# Sort
585+
sorted_assignments = sort_assignments(assignments=assignments.data, sort_by=sort_by, order=order)
586+
# Paginate
587+
paginator = self.pagination_class()
588+
paginated_response_data = paginator.paginate_queryset(sorted_assignments, request)
589+
return paginator.get_paginated_response(paginated_response_data)

0 commit comments

Comments
 (0)