Skip to content

Commit 9c261e8

Browse files
committed
feat: Support filtering by multiple orgs in scopes endpoint
1 parent 7afbff4 commit 9c261e8

2 files changed

Lines changed: 26 additions & 18 deletions

File tree

openedx_authz/rest_api/v1/serializers.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,11 @@ def validate(self, attrs) -> dict:
160160
role_value = validated_data["role"]
161161

162162
if scope and scopes is not None:
163-
raise serializers.ValidationError(
164-
"Provide either 'scope' or 'scopes', not both."
165-
)
163+
raise serializers.ValidationError("Provide either 'scope' or 'scopes', not both.")
166164

167165
scopes_list = scopes if scopes is not None else ([scope] if scope else None)
168166
if not scopes_list:
169-
raise serializers.ValidationError(
170-
"Either 'scope' or 'scopes' must be provided."
171-
)
167+
raise serializers.ValidationError("Either 'scope' or 'scopes' must be provided.")
172168

173169
for scope_value in scopes_list:
174170
self._validate_scope_and_role(scope_value, role_value)
@@ -285,6 +281,7 @@ class ListScopesQuerySerializer(OrgMixin): # pylint: disable=abstract-method
285281
choices=[(e.value, e.name) for e in ScopesTypeField], required=False, default=None, allow_null=True
286282
)
287283
search = serializers.CharField(required=False, default="", allow_blank=True)
284+
orgs = CaseSensitiveCommaSeparatedListField(required=False, default=[])
288285

289286

290287
class ListTeamMembersSerializer(OrderMixin): # pylint: disable=abstract-method

openedx_authz/rest_api/v1/views.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,9 @@ def get_queryset(self) -> QuerySet:
595595
parameters=[
596596
apidocs.query_parameter("search", str, description="Filter scopes by display name"),
597597
apidocs.query_parameter("org", str, description="Filter scopes by org"),
598+
apidocs.query_parameter(
599+
"orgs", str, description="Filter scopes by multiple orgs (comma separated list of orgs)"
600+
),
598601
apidocs.query_parameter("page", int, description="Page number for pagination"),
599602
apidocs.query_parameter("page_size", int, description="Number of items per page"),
600603
apidocs.query_parameter(
@@ -631,6 +634,7 @@ class ScopesAPIView(generics.ListAPIView):
631634
632635
- search (Optional): Search term to filter scopes by display name
633636
- org (Optional): Filter scopes by org
637+
- orgs (Optional): Filter scopes by multiple orgs (comma separated list of orgs)
634638
- page (Optional): Page number for pagination
635639
- page_size (Optional): Number of items per page
636640
- scope_type (Optional): Filter scopes by type. Supported values are `course` and `library`.
@@ -700,7 +704,7 @@ def _get_courses_queryset(
700704
allowed_ids: set | None = None,
701705
allowed_orgs: set | None = None,
702706
search: str = "",
703-
org: str = "",
707+
orgs: set[str] | None = None,
704708
) -> QuerySet:
705709
"""Return a CourseOverview queryset projected to the unified scope shape.
706710
@@ -717,8 +721,8 @@ def _get_courses_queryset(
717721
qs = qs.none()
718722
else:
719723
qs = qs.filter(combined_filter)
720-
if org:
721-
qs = qs.filter(org=org)
724+
if orgs:
725+
qs = qs.filter(org__in=orgs)
722726
if search:
723727
qs = qs.filter(display_name__icontains=search)
724728
return qs.annotate(
@@ -738,7 +742,7 @@ def _get_libraries_queryset(
738742
allowed_pairs: set | None = None,
739743
allowed_orgs: set | None = None,
740744
search: str = "",
741-
org: str = "",
745+
orgs: set[str] | None = None,
742746
) -> QuerySet:
743747
"""Return a ContentLibrary queryset projected to the unified scope shape.
744748
@@ -759,8 +763,8 @@ def _get_libraries_queryset(
759763
qs = qs.none()
760764
else:
761765
qs = qs.filter(combined)
762-
if org:
763-
qs = qs.filter(org__short_name=org)
766+
if orgs:
767+
qs = qs.filter(org__short_name__in=orgs)
764768
if search:
765769
qs = qs.filter(learning_package__title__icontains=search)
766770
return qs.annotate(
@@ -785,7 +789,7 @@ def _get_allowed_scope_queryset(
785789
queryset_builder: callable,
786790
extract_ids: callable,
787791
search: str = "",
788-
org: str = "",
792+
orgs: set[str] | None = None,
789793
) -> QuerySet:
790794
"""Resolve allowed scopes from Casbin and return a filtered queryset.
791795
@@ -811,7 +815,7 @@ def _get_allowed_scope_queryset(
811815
specific_scopes = [s for s in allowed_scopes if not isinstance(s, glob_cls)]
812816
allowed_ids = extract_ids(specific_scopes)
813817
allowed_orgs = {s.org for s in allowed_scopes if isinstance(s, glob_cls)}
814-
return queryset_builder(allowed_ids, allowed_orgs, search=search, org=org)
818+
return queryset_builder(allowed_ids, allowed_orgs, search=search, orgs=orgs)
815819

816820
def _build_queryset(self, courses_qs: QuerySet | None, libraries_qs: QuerySet | None) -> QuerySet:
817821
"""Union the provided querysets and sort deterministically.
@@ -834,17 +838,24 @@ def get_queryset(self) -> QuerySet:
834838
scope_type = params_serializer.validated_data["scope_type"]
835839
search = params_serializer.validated_data["search"]
836840
org = params_serializer.validated_data.get("org", "")
841+
orgs_param = params_serializer.validated_data.get("orgs", [])
842+
843+
orgs = set()
844+
orgs.update(orgs_param)
845+
846+
if org:
847+
orgs.add(org)
837848

838849
# Staff and superusers can see all scopes, skip permission filtering.
839850
if user.is_staff or user.is_superuser:
840851
return self._build_queryset(
841852
courses_qs=(
842-
self._get_courses_queryset(search=search, org=org)
853+
self._get_courses_queryset(search=search, orgs=orgs)
843854
if scope_type != ScopesTypeField.LIBRARY
844855
else None
845856
),
846857
libraries_qs=(
847-
self._get_libraries_queryset(search=search, org=org)
858+
self._get_libraries_queryset(search=search, orgs=orgs)
848859
if scope_type != ScopesTypeField.COURSE
849860
else None
850861
),
@@ -867,7 +878,7 @@ def get_permission(scope_cls):
867878
queryset_builder=self._get_courses_queryset,
868879
extract_ids=lambda scopes: {s.external_key for s in scopes},
869880
search=search,
870-
org=org,
881+
orgs=orgs,
871882
)
872883

873884
libraries_qs = None
@@ -882,7 +893,7 @@ def get_permission(scope_cls):
882893
(s.external_key.split(":")[1], s.external_key.split(":")[2]) for s in scopes
883894
},
884895
search=search,
885-
org=org,
896+
orgs=orgs,
886897
)
887898

888899
# Union the requested querysets and sort by org at the DB level.

0 commit comments

Comments
 (0)