Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions openedx/core/djangoapps/enrollments/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,68 @@
class CourseEnrollmentsApiListForm(Form):
"""
A form that validates the query string parameters for the CourseEnrollmentsApiListView.

ADR 0033 – OEP-68 parameter naming standardization:
- ``course_key`` is the preferred parameter name; ``course_id`` is accepted
as a deprecated alias (BC strategy §1). When both are present,
``course_key`` wins.
- ``course_keys`` is the preferred parameter name; ``course_ids`` is
accepted as a deprecated alias (same precedence rule).
Internally the cleaned_data continues to expose ``course_id`` /
``course_ids`` so call sites do not need to change. Use
:meth:`legacy_param_aliases_used` to detect when the deprecated names were
sent by the client (used to emit the ``Deprecation`` HTTP header).
"""
MAX_INPUT_COUNT = 100
# Legacy / OEP-68 alias pairs: (legacy, preferred).
_LEGACY_PARAM_ALIASES = (
("course_id", "course_key"),
("course_ids", "course_keys"),
)

username = CharField(required=False)
course_id = CharField(required=False)
course_key = CharField(required=False)
course_ids = CharField(required=False)
course_keys = CharField(required=False)
email = CharField(required=False)

def __init__(self, query_params, *args, **kwargs):
# Capture the raw param names supplied on the wire, *before* Django's
# form layer resolves aliases, so :meth:`legacy_param_aliases_used`
# can later report exactly which legacy names were used.
try:
raw_keys = set(query_params.keys())
except AttributeError:
raw_keys = set()
self._raw_param_names = raw_keys

# Coalesce OEP-68 preferred names into the legacy fields so the
# downstream view code keeps reading ``course_id`` / ``course_ids``
# without changes. Preferred wins when both are sent.
if hasattr(query_params, "copy"):
data = query_params.copy()
else:
data = dict(query_params)
for legacy_name, preferred_name in self._LEGACY_PARAM_ALIASES:
preferred_value = data.get(preferred_name)
if preferred_value:
data[legacy_name] = preferred_value

super().__init__(data, *args, **kwargs)

def legacy_param_aliases_used(self):
"""
Return the list of legacy (OEP-68-violating) parameter names actually
present in the request, in declaration order.

Used by the view layer to emit the ADR 0033 ``Deprecation`` header.
"""
return [
legacy for legacy, _preferred in self._LEGACY_PARAM_ALIASES
if legacy in self._raw_param_names
]

def clean_course_id(self):
"""
Validate and return a course ID.
Expand Down
13 changes: 9 additions & 4 deletions openedx/core/djangoapps/enrollments/paginators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
"""


from rest_framework.pagination import CursorPagination
from edx_rest_framework_extensions.paginators import DefaultPagination # ADR 0032


class CourseEnrollmentsApiListPagination(CursorPagination):
class CourseEnrollmentsApiListPagination(DefaultPagination):
"""
Paginator for the Course enrollments list API.
ADR 0032 – standard pagination for the admin enrollments list API
(GET /api/enrollment/v1/enrollments).

Extends DefaultPagination with a larger default page size appropriate
for an admin-facing, bulk-query endpoint. The full 7-field response
envelope (count, num_pages, current_page, start, next, previous,
results) is provided by DefaultPagination.get_paginated_response.
"""
page_size = 100
page_size_query_param = 'page_size'
max_page_size = 100
page_query_param = 'page'
19 changes: 19 additions & 0 deletions openedx/core/djangoapps/enrollments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,22 @@ class Meta:
model = CourseEnrollmentAllowed
exclude = ["id"]
lookup_field = "user"


class UserRoleSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""Serializes a single course-level role entry for a user."""

org = serializers.CharField()
course_id = serializers.SerializerMethodField()
role = serializers.CharField()

def get_course_id(self, obj):
"""Return course_id as a string."""
return str(obj.course_id)


class UserRolesResponseSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""Serializes the full response payload for EnrollmentUserRolesView."""

roles = UserRoleSerializer(many=True)
is_staff = serializers.BooleanField()
Loading
Loading