3939from openedx_authz .api .utils import get_user_map
4040from openedx_authz .constants import permissions
4141from openedx_authz .models .scopes import get_content_library_model , get_course_overview_model
42- from openedx_authz .rest_api .data import RoleOperationError , RoleOperationStatus
42+ from openedx_authz .rest_api .data import RoleOperationError , RoleOperationStatus , ScopesQuerySetFields , ScopesTypeField
4343from openedx_authz .rest_api .decorators import authz_permissions , view_auth_classes
4444from openedx_authz .rest_api .utils import (
4545 filter_users ,
@@ -675,6 +675,14 @@ class ScopesAPIView(generics.ListAPIView):
675675 pagination_class = AuthZAPIViewPagination
676676 permission_classes = [AnyScopePermission ]
677677
678+ # Priority for fields used for stable sorting (first has more priority)
679+ ordering_priority = (
680+ ScopesQuerySetFields .ORG_NAME ,
681+ ScopesQuerySetFields .SCOPE_TYPE ,
682+ ScopesQuerySetFields .DISPLAY_NAME_COL ,
683+ ScopesQuerySetFields .SCOPE_ID ,
684+ )
685+
678686 def get_serializer_context (self ):
679687 context = super ().get_serializer_context ()
680688 context ["org_map" ] = Organization .objects .filter (active = True ).in_bulk (field_name = "short_name" )
@@ -711,7 +719,12 @@ def _get_courses_queryset(
711719 display_name_col = Cast ("display_name" , output_field = CharField (db_collation = "utf8mb4_unicode_ci" )),
712720 org_name = Cast ("org" , output_field = CharField (db_collation = "utf8mb4_unicode_ci" )),
713721 scope_type = Value ("course" , output_field = CharField (db_collation = "utf8mb4_unicode_ci" )),
714- ).values ("scope_id" , "display_name_col" , "org_name" , "scope_type" )
722+ ).values (
723+ ScopesQuerySetFields .SCOPE_ID ,
724+ ScopesQuerySetFields .DISPLAY_NAME_COL ,
725+ ScopesQuerySetFields .ORG_NAME ,
726+ ScopesQuerySetFields .SCOPE_TYPE ,
727+ )
715728
716729 def _get_libraries_queryset (
717730 self ,
@@ -748,19 +761,23 @@ def _get_libraries_queryset(
748761 display_name_col = Cast ("learning_package__title" , output_field = CharField (db_collation = "utf8mb4_unicode_ci" )),
749762 org_name = Cast ("org__short_name" , output_field = CharField (db_collation = "utf8mb4_unicode_ci" )),
750763 scope_type = Value ("library" , output_field = CharField (db_collation = "utf8mb4_unicode_ci" )),
751- ).values ("scope_id" , "display_name_col" , "org_name" , "scope_type" )
764+ ).values (
765+ ScopesQuerySetFields .SCOPE_ID ,
766+ ScopesQuerySetFields .DISPLAY_NAME_COL ,
767+ ScopesQuerySetFields .ORG_NAME ,
768+ ScopesQuerySetFields .SCOPE_TYPE ,
769+ )
752770
753771 def _build_queryset (self , courses_qs : QuerySet | None , libraries_qs : QuerySet | None ) -> QuerySet :
754772 """Union the provided querysets and sort deterministically.
755773
756774 Orders by org_name first (satisfying the 'ordered by org' requirement), then by
757775 scope_type, display_name_col, and scope_id as tiebreakers to ensure stable pagination.
758776 """
759- ordering = ("org_name" , "scope_type" , "display_name_col" , "scope_id" )
760777 if courses_qs is not None and libraries_qs is not None :
761- return courses_qs .union (libraries_qs ).order_by (* ordering )
778+ return courses_qs .union (libraries_qs ).order_by (* self . ordering_priority )
762779 qs = courses_qs if courses_qs is not None else libraries_qs
763- return qs .order_by (* ordering )
780+ return qs .order_by (* self . ordering_priority )
764781
765782 def get_queryset (self ) -> QuerySet :
766783 """Return scopes ordered by org, filtered by the user's permissions."""
@@ -776,8 +793,16 @@ def get_queryset(self) -> QuerySet:
776793 # Staff and superusers can see all scopes, skip permission filtering.
777794 if user .is_staff or user .is_superuser :
778795 return self ._build_queryset (
779- courses_qs = (self ._get_courses_queryset (search = search , org = org ) if scope_type != "library" else None ),
780- libraries_qs = (self ._get_libraries_queryset (search = search , org = org ) if scope_type != "course" else None ),
796+ courses_qs = (
797+ self ._get_courses_queryset (search = search , org = org )
798+ if scope_type != ScopesTypeField .LIBRARY
799+ else None
800+ ),
801+ libraries_qs = (
802+ self ._get_libraries_queryset (search = search , org = org )
803+ if scope_type != ScopesTypeField .COURSE
804+ else None
805+ ),
781806 )
782807
783808 management_only = params_serializer .validated_data ["management_permission_only" ]
@@ -788,7 +813,7 @@ def get_permission(scope_cls):
788813
789814 # Resolve allowed scopes from Casbin in a single call per scope type.
790815 courses_qs = None
791- if scope_type != "library" :
816+ if scope_type != ScopesTypeField . LIBRARY :
792817 allowed_course_scopes = get_scopes_for_user_and_permission (
793818 user .username , get_permission (CourseOverviewData ).identifier
794819 )
@@ -804,7 +829,7 @@ def get_permission(scope_cls):
804829 )
805830
806831 libraries_qs = None
807- if scope_type != "course" :
832+ if scope_type != ScopesTypeField . COURSE :
808833 allowed_library_scopes = get_scopes_for_user_and_permission (
809834 user .username , get_permission (ContentLibraryData ).identifier
810835 )
0 commit comments