Skip to content

Commit 7eb3955

Browse files
committed
feat: Implement orgs endpoint for admin console
1 parent ef8b1d1 commit 7eb3955

4 files changed

Lines changed: 179 additions & 1 deletion

File tree

openedx_authz/rest_api/v1/serializers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ class ListRolesWithScopeResponseSerializer(serializers.Serializer): # pylint: d
169169
user_count = serializers.IntegerField()
170170

171171

172+
class OrganizationSerializer(serializers.Serializer): # pylint: disable=abstract-method
173+
"""Serializer for Organization model."""
174+
175+
id = serializers.IntegerField()
176+
name = serializers.CharField()
177+
short_name = serializers.CharField()
178+
179+
172180
class UserRoleAssignmentSerializer(serializers.Serializer): # pylint: disable=abstract-method
173181
"""Serializer for a user role assignment."""
174182

openedx_authz/rest_api/v1/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
),
1313
path("roles/", views.RoleListView.as_view(), name="role-list"),
1414
path("roles/users/", views.RoleUserAPIView.as_view(), name="role-user-list"),
15+
path("orgs/", views.OrgsAPIView.as_view(), name="orgs-view"),
1516
]

openedx_authz/rest_api/v1/views.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import edx_api_doc_tools as apidocs
1111
from django.contrib.auth import get_user_model
1212
from django.http import HttpRequest
13-
from rest_framework import status
13+
from organizations.models import Organization
14+
from rest_framework import filters, generics, status
1415
from rest_framework.response import Response
1516
from rest_framework.views import APIView
1617

@@ -32,6 +33,7 @@
3233
ListRolesWithScopeResponseSerializer,
3334
ListRolesWithScopeSerializer,
3435
ListUsersInRoleWithScopeSerializer,
36+
OrganizationSerializer,
3537
PermissionValidationResponseSerializer,
3638
PermissionValidationSerializer,
3739
RemoveUsersFromRoleWithScopeSerializer,
@@ -449,3 +451,58 @@ def get(self, request: HttpRequest) -> Response:
449451
paginated_response_data = paginator.paginate_queryset(response_data, request)
450452
serialized_data = ListRolesWithScopeResponseSerializer(paginated_response_data, many=True)
451453
return paginator.get_paginated_response(serialized_data.data)
454+
455+
456+
@view_auth_classes()
457+
class OrgsAPIView(generics.ListAPIView):
458+
"""
459+
API view for listing orgs
460+
This API is used on the filters functionality on the Admin Console.
461+
462+
**Endpoints**
463+
464+
- GET: Retrieve all organizations
465+
466+
**Query Parameters**
467+
468+
- search (Optional): Search term to filter organizations by name or short name
469+
- page (Optional): Page number for pagination
470+
- page_size (Optional): Number of items per page
471+
472+
**Response Format**
473+
474+
Returns a paginated list of organization objects, each containing:
475+
476+
- id: The organization's ID
477+
- name: The organization's name
478+
- short_name: The organization's short name
479+
480+
**Authentication and Permissions**
481+
482+
- Requires authenticated user.
483+
484+
**Example Request**
485+
486+
GET /api/authz/v1/orgs/?search=edx&page=1&page_size=10
487+
488+
**Example Response**::
489+
490+
{
491+
"count": 1,
492+
"next": null,
493+
"previous": null,
494+
"results": [
495+
{
496+
"id": 1,
497+
"name": "edX",
498+
"short_name": "edx"
499+
}
500+
]
501+
}
502+
"""
503+
504+
queryset = Organization.objects.order_by("name")
505+
serializer_class = OrganizationSerializer
506+
pagination_class = AuthZAPIViewPagination
507+
filter_backends = [filters.SearchFilter]
508+
search_fields = ["name", "short_name"]

openedx_authz/tests/rest_api/test_views.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ddt import data, ddt, unpack
1212
from django.contrib.auth import get_user_model
1313
from django.urls import reverse
14+
from organizations.models import Organization
1415
from rest_framework import status
1516
from rest_framework.test import APIClient
1617

@@ -853,6 +854,117 @@ def test_put_accepts_valid_full_course_key_scope(self, _mock_exists, _mock_assig
853854
self.assertEqual(len(response.data["completed"]), 1)
854855

855856

857+
@ddt
858+
class TestOrgsAPIView(ViewTestMixin):
859+
"""Test suite for OrgsAPIView."""
860+
861+
@classmethod
862+
def setUpTestData(cls):
863+
"""Create Organization fixtures."""
864+
super().setUpTestData()
865+
866+
Organization.objects.bulk_create(
867+
[
868+
Organization(name="Alpha University", short_name="AlphaU"),
869+
Organization(name="Beta Institute", short_name="BetaI"),
870+
Organization(name="Gamma College", short_name="GammaC"),
871+
]
872+
)
873+
874+
def setUp(self):
875+
"""Set up test fixtures."""
876+
super().setUp()
877+
self.url = reverse("openedx_authz:orgs-view")
878+
879+
def test_get_orgs_returns_all(self):
880+
"""Test that all orgs are returned when no search param is provided.
881+
882+
Expected result:
883+
- Returns 200 OK status
884+
- Returns all 3 orgs
885+
"""
886+
response = self.client.get(self.url)
887+
888+
self.assertEqual(response.status_code, status.HTTP_200_OK)
889+
self.assertEqual(response.data["count"], 3)
890+
self.assertEqual(len(response.data["results"]), 3)
891+
892+
@data(
893+
# Match by name
894+
("Alpha", 1),
895+
("university", 1),
896+
# Match by short_name
897+
("BetaI", 1),
898+
("gamma", 1),
899+
# Partial match across multiple orgs
900+
("a", 3),
901+
# No match
902+
("nonexistent", 0),
903+
)
904+
@unpack
905+
def test_get_orgs_search(self, search_term: str, expected_count: int):
906+
"""Test filtering orgs by name or short_name via the search param.
907+
908+
Expected result:
909+
- Returns 200 OK status
910+
- Returns only orgs matching the search term
911+
"""
912+
response = self.client.get(self.url, {"search": search_term})
913+
914+
self.assertEqual(response.status_code, status.HTTP_200_OK)
915+
self.assertEqual(response.data["count"], expected_count)
916+
self.assertEqual(len(response.data["results"]), expected_count)
917+
918+
@data(
919+
({}, 3, False),
920+
({"page": 1, "page_size": 2}, 2, True),
921+
({"page": 2, "page_size": 2}, 1, False),
922+
({"page": 1, "page_size": 3}, 3, False),
923+
)
924+
@unpack
925+
def test_get_orgs_pagination(self, query_params: dict, expected_count: int, has_next: bool):
926+
"""Test pagination of org results.
927+
928+
Expected result:
929+
- Returns 200 OK status
930+
- Returns correct page size and next link
931+
"""
932+
response = self.client.get(self.url, query_params)
933+
934+
self.assertEqual(response.status_code, status.HTTP_200_OK)
935+
self.assertEqual(len(response.data["results"]), expected_count)
936+
if has_next:
937+
self.assertIsNotNone(response.data["next"])
938+
else:
939+
self.assertIsNone(response.data["next"])
940+
941+
def test_get_orgs_response_shape(self):
942+
"""Test that each org result contains the expected fields.
943+
944+
Expected result:
945+
- Each result has id, name, and short_name fields
946+
"""
947+
response = self.client.get(self.url)
948+
949+
self.assertEqual(response.status_code, status.HTTP_200_OK)
950+
result = response.data["results"][0]
951+
self.assertIn("id", result)
952+
self.assertIn("name", result)
953+
self.assertIn("short_name", result)
954+
955+
def test_get_orgs_unauthenticated(self):
956+
"""Test that unauthenticated requests are rejected.
957+
958+
Expected result:
959+
- Returns 401 UNAUTHORIZED status
960+
"""
961+
self.client.force_authenticate(user=None)
962+
963+
response = self.client.get(self.url)
964+
965+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
966+
967+
856968
@ddt
857969
class TestRoleListView(ViewTestMixin):
858970
"""Test suite for RoleListView."""

0 commit comments

Comments
 (0)