From 8dbd3db76c3d95238bb2c96e71805bb56ab927da Mon Sep 17 00:00:00 2001 From: Jesse Stewart Date: Wed, 7 Jan 2026 12:07:59 -0500 Subject: [PATCH 01/10] feat: create schema for enrollment api --- .../0003-instructor-enrollment-api-spec.rst | 156 +++++ .../instructor-v2-enrollment-api-spec.yaml | 621 ++++++++++++++++++ 2 files changed, 777 insertions(+) create mode 100644 lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst create mode 100644 lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst new file mode 100644 index 000000000000..d43cf4dbb3db --- /dev/null +++ b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst @@ -0,0 +1,156 @@ +Instructor Enrollment API Specification +---------------------------------------- + +Status +====== + +**Draft** (=> **Provisional**) + +Context +======= + +The instructor dashboard is being migrated to a Micro-Frontend (MFE) architecture, which requires RESTful API endpoints. The current implementation provides enrollment operations (enroll, unenroll, list enrollments) through legacy endpoints in ``lms/djangoapps/instructor/enrollment.py``. + +Decisions +========= + +#. **RESTful Resource-Oriented Design** + + Use resource-oriented URLs: ``/api/instructor/v2/courses/{course_key}/enrollments`` + + Use appropriate HTTP methods per Open edX REST API Conventions: + + * ``GET`` for read operations (list enrollments, get enrollment details) + * ``POST`` for enrollments (enroll one or more learners) + * ``DELETE`` for unenrollments (unenroll a single learner) + +#. **Synchronous vs Asynchronous Execution** + + * Operations targeting a single learner execute synchronously and return ``200 OK`` + with immediate results (< 5s typical, typically 100-500ms) + * Operations targeting multiple learners queue a background task and return + ``202 Accepted`` with task tracking information + * Task monitoring uses shared Task API endpoint: + ``GET /api/instructor/v2/courses/{course_key}/tasks/{task_id}`` + (defined in separate Task API specification) + +#. **Enrollment State Model** + + * Support both active enrollments (``CourseEnrollment``) and pre-enrollments + (``CourseEnrollmentAllowed``) + * Track enrollment state transitions with before/after snapshots + * Handle cases where user doesn't exist yet (creates CourseEnrollmentAllowed) + * Support auto-enrollment upon user registration + * Support multiple enrollment modes (audit, honor, verified, professional, etc.) + +#. **Pagination and Performance** + + * Use DRF standard pagination format with ``next``, ``previous``, ``count``, + ``num_pages``, and ``results`` fields (not nested pagination) + * Default page size of 25, maximum of 100 per page + * 1-indexed page numbers for consistency with DRF defaults + * Return basic enrollment data by default to optimize performance + +#. **Optional Fields via requested_fields Parameter** + + * Support ``requested_fields`` query parameter per Open edX conventions + * Available optional fields: ``beta_tester``, ``profile_image`` + * Comma-delimited list format: ``?requested_fields=beta_tester,profile_image`` + * Reduces database queries and improves performance when optional data not needed + +#. **Authentication and Authorization** + + * Support both OAuth2 (for mobile clients and micro-services) and + Session-based authentication (for mobile webviews and browser clients) + * Require instructor-level permissions for all enrollment operations + * Follow separation of filtering and authorization (explicit filtering in URLs) + +#. **Error Handling** + + * Follow Open edX REST API Conventions error format + * Include ``error_code`` (machine-readable), ``developer_message``, + ``user_message`` (internationalized), and ``status_code`` + * Support ``field_errors`` object for field-specific validation errors + * Use appropriate HTTP status codes: 200, 202, 400, 401, 403, 404 + +#. **Date/Time Serialization** + + * Serialize all dates and timestamps to ISO 8601 format with explicit timezone offsets + * Prefer UTC timestamps + * Example format: ``2024-01-15T10:30:00Z`` + +#. **Email Notifications** + + * Support optional email notifications via ``email_students`` parameter + * Use different message types based on user state: + + * ``enrolled_enroll``: User already registered, being enrolled + * ``allowed_enroll``: User not yet registered, pre-enrollment created + * ``enrolled_unenroll``: User being unenrolled + * ``allowed_unenroll``: Pre-enrollment being removed + + * Support optional ``reason`` parameter included in notification emails + +#. **OpenAPI Specification** + + Maintain an OpenAPI specification at ``../references/instructor-v2-enrollment-api-spec.yaml`` + to guide implementation. This static specification serves as a reference during development, + but ``/api-docs/`` is the source of truth for what is actually deployed. Once implementation + is complete and the endpoints are live in ``/api-docs/``, the static spec file will be + deleted to avoid maintaining outdated documentation. + +Consequences +============ + +Positive +~~~~~~~~ + +* Consistent URL patterns following Open edX conventions make the API predictable +* Explicit sync/async behavior based on operation scope allows proper UI feedback +* Pagination support efficiently handles courses with thousands of enrollments +* Optional fields optimize performance by avoiding unnecessary database queries +* OpenAPI specification enables automated validation, testing, and type-safe client generation +* Resource-oriented design makes it easy to add new operations +* Support for both enrollments and pre-enrollments handles all use cases +* Before/after state tracking provides clear audit trail of changes +* Email notification support maintains current functionality for learner communication + +Negative +~~~~~~~~ + +* Existing clients using legacy enrollment endpoints need to be updated +* Dual maintenance during transition period +* Developers familiar with legacy endpoints need to learn new patterns +* Optional fields via ``requested_fields`` add complexity to serialization logic +* Async operations require additional task monitoring implementation + +Alternatives Considered +======================= + +#. **Separate Endpoints for Enroll/Unenroll** + + Considered ``POST /enrollments`` for enroll and ``POST /unenrollments`` for unenroll, + but using ``DELETE /enrollments/{id}`` is more RESTful and follows HTTP verb semantics. + +#. **Nested Pagination Format** + + Considered nesting pagination metadata under a ``pagination`` key (per Cliff Dyer's + proposal), but chose DRF standard flat format (``next``, ``previous``, ``count``, + ``num_pages``, ``results`` at top level) as it's the established convention + documented in Open edX REST API Conventions. + +#. **Expand Parameter Instead of requested_fields** + + Considered using ``expand`` parameter for related objects, but ``requested_fields`` + is more appropriate for optional fields that are not separate resources. Using + ``expand`` would imply these are related resources with their own endpoints, + which is not the case for beta tester status or profile images in this context. + +References +========== + +* OpenAPI Specification: ``../references/instructor-v2-enrollment-api-spec.yaml`` +* Live API Documentation: ``/api-docs/`` +* Legacy Implementation: ``lms/djangoapps/instructor/enrollment.py`` +* Open edX REST API Conventions: https://openedx.atlassian.net/wiki/spaces/AC/pages/18350757/Open+edX+REST+API+Conventions +* Optional Fields and API Versioning: https://openedx.atlassian.net/wiki/spaces/AC/pages/40862782/Optional+Fields+and+API+Versioning diff --git a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml new file mode 100644 index 000000000000..c8848fbd984b --- /dev/null +++ b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml @@ -0,0 +1,621 @@ +swagger: '2.0' +info: + title: Instructor Enrollment API + version: 2.0.0 + description: | + Modern REST API for instructor enrollment management operations. + + **Design Principles:** + - RESTful resource-oriented URLs + - Query parameters for filtering operations + - Clear separation between read and write operations + - Consistent error handling + - Follows Open edX REST API Conventions + + **Execution Model:** + - Operations that affect a single learner execute synchronously (< 5s typical) + - Operations that affect multiple learners queue a background task + - Use the task status endpoint to monitor background tasks + + **Authentication:** + - OAuth2 for mobile clients and micro-services + - Session-based authentication for mobile webviews and browser clients + + **Serialization:** + - Dates and timestamps are serialized to ISO 8601 format with explicit timezone offsets + - UTC timestamps are preferred + +host: courses.example.com +basePath: / +schemes: + - https + +securityDefinitions: + OAuth2: + type: oauth2 + flow: accessCode + authorizationUrl: https://courses.example.com/oauth2/authorize + tokenUrl: https://courses.example.com/oauth2/token + scopes: + instructor: Instructor-level access to course data + SessionAuth: + type: apiKey + in: header + name: Cookie + description: Session-based authentication using Django session cookies + +security: + - OAuth2: [instructor] + - SessionAuth: [] + +tags: + - name: Enrollments + description: Course enrollment operations + +paths: + # ==================== ENROLLMENT ENDPOINTS ==================== + + /api/instructor/v2/courses/{course_key}/enrollments: + get: + tags: + - Enrollments + summary: List course enrollments + description: | + Retrieve a paginated list of all enrollments for a course. + + **Performance:** Returns basic enrollment data by default. Use `requested_fields` + parameter to include additional data such as profile images or beta tester status. + + **Pagination:** Uses DRF standard pagination format with `next`, `previous`, + `count`, `num_pages`, and `results` fields. + operationId: listEnrollments + produces: + - application/json + parameters: + - $ref: '#/parameters/CourseKey' + - name: page + in: query + description: Page number (1-indexed) + required: false + type: integer + minimum: 1 + default: 1 + - name: page_size + in: query + description: Number of results per page + required: false + type: integer + minimum: 1 + maximum: 100 + default: 25 + - name: requested_fields + in: query + description: | + Comma-delimited list of optional fields to include in response. + Available fields: `beta_tester`, `profile_image` + required: false + type: string + x-example: "beta_tester,profile_image" + responses: + 200: + description: Enrollments retrieved successfully + schema: + $ref: '#/definitions/EnrollmentList' + examples: + application/json: + count: 1035 + num_pages: 42 + next: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" + previous: null + results: + - username: "bjohnson" + email: "bela.j@example.com" + first_name: "Bela" + last_name: "Johnson" + mode: "audit" + is_active: true + created: "2024-01-15T10:30:00Z" + - username: "cpatel" + email: "cyrus.patel@example.com" + first_name: "Cyrus" + last_name: "Patel" + mode: "audit" + is_active: true + created: "2024-01-16T14:22:00Z" + 400: + $ref: '#/responses/BadRequest' + 401: + $ref: '#/responses/Unauthorized' + 403: + $ref: '#/responses/Forbidden' + 404: + $ref: '#/responses/NotFound' + + post: + tags: + - Enrollments + summary: Enroll learners in course + description: | + Enroll one or more learners in a course by email or username. + + **Behavior:** + - If user exists and is active: Enrolls immediately in specified mode (or default) + - If user does not exist: Creates CourseEnrollmentAllowed record + - When the user registers, they will be auto-enrolled if `auto_enroll` is true + + **Scope:** + - Single learner: Synchronous operation (~100-500ms) + - Multiple learners: Asynchronous task queued + + **Email Notifications:** + - If user is already registered: Uses "enrolled_enroll" message type + - If user is not registered: Uses "allowed_enroll" message type + operationId: enrollLearners + consumes: + - application/json + produces: + - application/json + parameters: + - $ref: '#/parameters/CourseKey' + - name: body + in: body + required: true + schema: + type: object + required: + - identifiers + properties: + identifiers: + type: array + description: List of email addresses or usernames to enroll + minItems: 1 + items: + type: string + example: ["john@example.com", "jane_doe"] + auto_enroll: + type: boolean + description: Auto-enroll user when they register (for non-registered users) + default: false + email_students: + type: boolean + description: Send email notification to learners + default: false + reason: + type: string + description: Reason for enrollment (included in email if email_students is true) + x-nullable: true + mode: + type: string + description: Enrollment mode (audit, honor, verified, professional, etc.) + x-nullable: true + example: "audit" + responses: + 200: + description: Single learner enrolled successfully (synchronous) + schema: + $ref: '#/definitions/EnrollmentOperationResult' + examples: + application/json: + action: "enroll" + results: + - identifier: "john@example.com" + before: + enrolled: false + allowed: false + after: + enrolled: true + allowed: false + mode: "audit" + 202: + description: Multiple learner enrollment task queued (asynchronous) + schema: + $ref: '#/definitions/AsyncOperationResult' + examples: + application/json: + task_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + status_url: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" + action: "enroll" + count: 150 + 400: + $ref: '#/responses/BadRequest' + 401: + $ref: '#/responses/Unauthorized' + 403: + $ref: '#/responses/Forbidden' + 404: + $ref: '#/responses/NotFound' + + /api/instructor/v2/courses/{course_key}/enrollments/{email_or_username}: + get: + tags: + - Enrollments + summary: Get learner enrollment details + description: | + Retrieve detailed enrollment information for a specific learner. + + **Returns:** + - Current enrollment status + - CourseEnrollmentAllowed status (if applicable) + - Enrollment mode + - User profile information + operationId: getEnrollment + produces: + - application/json + parameters: + - $ref: '#/parameters/CourseKey' + - $ref: '#/parameters/LearnerIdentifierPath' + responses: + 200: + description: Enrollment information retrieved successfully + schema: + $ref: '#/definitions/Enrollment' + examples: + application/json: + username: "john_harvard" + email: "john@example.com" + first_name: "John" + last_name: "Harvard" + mode: "audit" + is_active: true + created: "2024-01-15T10:30:00Z" + enrollment_allowed: false + auto_enroll: false + 400: + $ref: '#/responses/BadRequest' + 401: + $ref: '#/responses/Unauthorized' + 403: + $ref: '#/responses/Forbidden' + 404: + $ref: '#/responses/NotFound' + + delete: + tags: + - Enrollments + summary: Unenroll learner from course + description: | + Unenroll a learner from a course. + + **Behavior:** + - If user is enrolled: Unenrolls from course + - If user has CourseEnrollmentAllowed: Deletes the allowed enrollment + - Both operations are performed if both conditions exist + - Optionally sends notification email to learner + + **Email Notifications:** + - If user was enrolled: Uses "enrolled_unenroll" message type + - If user had allowed enrollment: Uses "allowed_unenroll" message type + operationId: unenrollLearner + produces: + - application/json + parameters: + - $ref: '#/parameters/CourseKey' + - $ref: '#/parameters/LearnerIdentifierPath' + - name: email_student + in: query + description: Send email notification to learner + required: false + type: boolean + default: false + responses: + 200: + description: Learner unenrolled successfully + schema: + $ref: '#/definitions/EnrollmentOperationResult' + examples: + application/json: + action: "unenroll" + results: + - identifier: "john@example.com" + before: + enrolled: true + allowed: false + mode: "audit" + after: + enrolled: false + allowed: false + 400: + $ref: '#/responses/BadRequest' + 401: + $ref: '#/responses/Unauthorized' + 403: + $ref: '#/responses/Forbidden' + 404: + $ref: '#/responses/NotFound' + +# ==================== COMPONENTS ==================== + +parameters: + CourseKey: + name: course_key + in: path + required: true + description: Course identifier in format `course-v1:{org}+{course}+{run}` + type: string + pattern: '^course-v1:[^/+]+(\\+[^/+]+)+(\\+[^/]+)$' + x-example: "course-v1:edX+DemoX+Demo_Course" + + LearnerIdentifierPath: + name: email_or_username + in: path + required: true + description: Learner's username or email address + type: string + minLength: 1 + +responses: + BadRequest: + description: Bad request - Invalid parameters or malformed request + schema: + $ref: '#/definitions/Error' + examples: + application/json: + error_code: "INVALID_PARAMETER" + developer_message: "Invalid course key format" + user_message: "The course identifier is not valid" + status_code: 400 + + Unauthorized: + description: Unauthorized - Authentication required + schema: + $ref: '#/definitions/Error' + examples: + application/json: + error_code: "AUTHENTICATION_REQUIRED" + developer_message: "You must be authenticated to access this endpoint" + user_message: "Please log in to continue" + status_code: 401 + + Forbidden: + description: Forbidden - Insufficient permissions + schema: + $ref: '#/definitions/Error' + examples: + application/json: + error_code: "PERMISSION_DENIED" + developer_message: "You do not have instructor permissions for this course" + user_message: "You do not have permission to perform this action" + status_code: 403 + + NotFound: + description: Not found - Resource does not exist + schema: + $ref: '#/definitions/Error' + examples: + application/json: + error_code: "RESOURCE_NOT_FOUND" + developer_message: "The specified resource does not exist" + user_message: "The requested item could not be found" + status_code: 404 + +definitions: + EnrollmentList: + type: object + description: Paginated list of enrollments + required: + - count + - results + properties: + count: + type: integer + description: Total number of enrollments across all pages + example: 1035 + num_pages: + type: integer + description: Total number of pages + example: 42 + next: + type: string + format: uri + description: URL to the next page of results + x-nullable: true + example: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" + previous: + type: string + format: uri + description: URL to the previous page of results + x-nullable: true + results: + type: array + description: List of enrollments on this page + items: + $ref: '#/definitions/Enrollment' + + Enrollment: + type: object + description: Learner enrollment information + required: + - username + - email + - first_name + - last_name + - is_active + properties: + username: + type: string + description: Learner's username + example: "john_harvard" + email: + type: string + format: email + description: Learner's email address + example: "john@example.com" + first_name: + type: string + description: Learner's first name + example: "John" + last_name: + type: string + description: Learner's last name + example: "Harvard" + mode: + type: string + description: Enrollment mode (audit, honor, verified, professional, etc.) + x-nullable: true + example: "audit" + is_active: + type: boolean + description: Whether the enrollment is active + example: true + created: + type: string + format: date-time + description: Enrollment creation timestamp (ISO 8601 format with timezone) + x-nullable: true + example: "2024-01-15T10:30:00Z" + enrollment_allowed: + type: boolean + description: Whether user has a CourseEnrollmentAllowed record + example: false + auto_enroll: + type: boolean + description: Whether user will be auto-enrolled upon registration + example: false + beta_tester: + type: boolean + description: Whether learner is a beta tester (only present if requested_fields includes beta_tester) + x-nullable: true + example: false + profile_image: + type: object + description: Learner's profile image URLs (only present if requested_fields includes profile_image) + x-nullable: true + properties: + has_image: + type: boolean + description: Whether the user has uploaded a profile image + image_url_full: + type: string + format: uri + description: Full size image URL + image_url_large: + type: string + format: uri + description: Large thumbnail URL + image_url_medium: + type: string + format: uri + description: Medium thumbnail URL + image_url_small: + type: string + format: uri + description: Small thumbnail URL + + EnrollmentOperationResult: + type: object + description: Result from an enrollment operation (enroll/unenroll) + required: + - action + - results + properties: + action: + type: string + enum: ["enroll", "unenroll"] + description: The action that was performed + results: + type: array + description: Results for each identifier + items: + type: object + required: + - identifier + - before + - after + properties: + identifier: + type: string + description: Email or username that was processed + before: + $ref: '#/definitions/EnrollmentState' + after: + $ref: '#/definitions/EnrollmentState' + error: + type: string + description: Error message if operation failed for this identifier + x-nullable: true + + EnrollmentState: + type: object + description: Enrollment state snapshot + required: + - enrolled + - allowed + properties: + enrolled: + type: boolean + description: Whether user is enrolled + allowed: + type: boolean + description: Whether user has CourseEnrollmentAllowed record + mode: + type: string + description: Enrollment mode (if enrolled) + x-nullable: true + auto_enroll: + type: boolean + description: Auto-enroll setting (if allowed) + x-nullable: true + + AsyncOperationResult: + type: object + description: Task information for an asynchronous operation + required: + - task_id + - status_url + properties: + task_id: + type: string + description: Unique task identifier + example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + status_url: + type: string + format: uri + description: URL to poll for task status (see Task API for details) + example: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" + action: + type: string + description: The action being performed + example: "enroll" + count: + type: integer + description: Number of learners being processed + example: 150 + + Error: + type: object + description: Error response + required: + - error_code + - developer_message + - user_message + - status_code + properties: + error_code: + type: string + description: Machine-readable error code + example: "RESOURCE_NOT_FOUND" + developer_message: + type: string + description: Verbose, plain language description of the problem for developers + example: "The specified course does not exist in the modulestore" + user_message: + type: string + description: User-friendly error message (internationalized) + example: "The requested course could not be found" + status_code: + type: integer + description: HTTP status code + example: 404 + field_errors: + type: object + description: Field-specific validation errors (if applicable) + x-nullable: true + additionalProperties: + type: object + properties: + developer_message: + type: string + description: Technical error details + user_message: + type: string + description: User-friendly error message From 1f9d5bc9784471835db06ca3862bfbabe4e33b30 Mon Sep 17 00:00:00 2001 From: Jesse Stewart Date: Wed, 7 Jan 2026 12:49:56 -0500 Subject: [PATCH 02/10] docs: pr feedback --- .../docs/decisions/0003-instructor-enrollment-api-spec.rst | 4 +++- .../docs/references/instructor-v2-enrollment-api-spec.yaml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst index d43cf4dbb3db..cbf042b8114c 100644 --- a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst +++ b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst @@ -4,7 +4,9 @@ Instructor Enrollment API Specification Status ====== -**Draft** (=> **Provisional**) +**Draft** + +This ADR will move to **Provisional** status once the OpenAPI specification is approved and implementation begins. It will move to **Accepted** status once the API is fully implemented and deployed. Context ======= diff --git a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml index c8848fbd984b..3c43286f69bc 100644 --- a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml +++ b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml @@ -332,7 +332,7 @@ parameters: required: true description: Course identifier in format `course-v1:{org}+{course}+{run}` type: string - pattern: '^course-v1:[^/+]+(\\+[^/+]+)+(\\+[^/]+)$' + pattern: '^course-v1:[^/+]+(\+[^/+]+)+(\+[^/]+)$' x-example: "course-v1:edX+DemoX+Demo_Course" LearnerIdentifierPath: From 82c5ef8fd1563a5c9bc7b75b0fe868ccf06a6c42 Mon Sep 17 00:00:00 2001 From: Jesse Stewart Date: Mon, 12 Jan 2026 15:25:13 -0500 Subject: [PATCH 03/10] fix: pr feedback. rename to enrollment api --- .../0003-instructor-enrollment-api-spec.rst | 22 +++++++++------ ...-spec.yaml => enrollment-v2-api-spec.yaml} | 28 +++++++++++-------- 2 files changed, 31 insertions(+), 19 deletions(-) rename lms/djangoapps/instructor/docs/references/{instructor-v2-enrollment-api-spec.yaml => enrollment-v2-api-spec.yaml} (95%) diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst index cbf042b8114c..a1d03ed08a71 100644 --- a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst +++ b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst @@ -1,5 +1,5 @@ -Instructor Enrollment API Specification ----------------------------------------- +Enrollment API v2 Specification +-------------------------------- Status ====== @@ -11,14 +11,14 @@ This ADR will move to **Provisional** status once the OpenAPI specification is a Context ======= -The instructor dashboard is being migrated to a Micro-Frontend (MFE) architecture, which requires RESTful API endpoints. The current implementation provides enrollment operations (enroll, unenroll, list enrollments) through legacy endpoints in ``lms/djangoapps/instructor/enrollment.py``. +The existing enrollment API (v1) has several limitations that make it difficult to use in modern applications. A new v2 enrollment API is needed to support the instructor dashboard MFE migration and other enrollment management use cases across the platform. The current implementation provides enrollment operations (enroll, unenroll, list enrollments) through legacy endpoints in ``lms/djangoapps/instructor/enrollment.py`` and the v1 enrollment API at ``/api/enrollment/v1/``. Decisions ========= #. **RESTful Resource-Oriented Design** - Use resource-oriented URLs: ``/api/instructor/v2/courses/{course_key}/enrollments`` + Use resource-oriented URLs: ``/api/enrollment/v2/courses/{course_key}/enrollments`` Use appropriate HTTP methods per Open edX REST API Conventions: @@ -33,7 +33,7 @@ Decisions * Operations targeting multiple learners queue a background task and return ``202 Accepted`` with task tracking information * Task monitoring uses shared Task API endpoint: - ``GET /api/instructor/v2/courses/{course_key}/tasks/{task_id}`` + ``GET /api/enrollment/v2/courses/{course_key}/tasks/{task_id}`` (defined in separate Task API specification) #. **Enrollment State Model** @@ -64,7 +64,12 @@ Decisions * Support both OAuth2 (for mobile clients and micro-services) and Session-based authentication (for mobile webviews and browser clients) - * Require instructor-level permissions for all enrollment operations + * Require appropriate permissions based on operation scope: + + * Course staff or instructor: Can manage enrollments within their courses + * Global staff: Can manage enrollments across all courses + * Self-enrollment: Learners can enroll/unenroll themselves (future consideration) + * Follow separation of filtering and authorization (explicit filtering in URLs) #. **Error Handling** @@ -95,7 +100,7 @@ Decisions #. **OpenAPI Specification** - Maintain an OpenAPI specification at ``../references/instructor-v2-enrollment-api-spec.yaml`` + Maintain an OpenAPI specification at ``../references/enrollment-v2-api-spec.yaml`` to guide implementation. This static specification serves as a reference during development, but ``/api-docs/`` is the source of truth for what is actually deployed. Once implementation is complete and the endpoints are live in ``/api-docs/``, the static spec file will be @@ -151,8 +156,9 @@ Alternatives Considered References ========== -* OpenAPI Specification: ``../references/instructor-v2-enrollment-api-spec.yaml`` +* OpenAPI Specification: ``../references/enrollment-v2-api-spec.yaml`` * Live API Documentation: ``/api-docs/`` +* Existing v1 Enrollment API: ``https://master.openedx.io/api-docs/#/enrollment`` * Legacy Implementation: ``lms/djangoapps/instructor/enrollment.py`` * Open edX REST API Conventions: https://openedx.atlassian.net/wiki/spaces/AC/pages/18350757/Open+edX+REST+API+Conventions * Optional Fields and API Versioning: https://openedx.atlassian.net/wiki/spaces/AC/pages/40862782/Optional+Fields+and+API+Versioning diff --git a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml b/lms/djangoapps/instructor/docs/references/enrollment-v2-api-spec.yaml similarity index 95% rename from lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml rename to lms/djangoapps/instructor/docs/references/enrollment-v2-api-spec.yaml index 3c43286f69bc..2d3973595263 100644 --- a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml +++ b/lms/djangoapps/instructor/docs/references/enrollment-v2-api-spec.yaml @@ -1,9 +1,10 @@ swagger: '2.0' info: - title: Instructor Enrollment API + title: Enrollment API v2 version: 2.0.0 description: | - Modern REST API for instructor enrollment management operations. + Modern REST API for enrollment management operations. This API supersedes the v1 enrollment API + and provides enhanced functionality for managing course enrollments across the Open edX platform. **Design Principles:** - RESTful resource-oriented URLs @@ -21,6 +22,10 @@ info: - OAuth2 for mobile clients and micro-services - Session-based authentication for mobile webviews and browser clients + **Authorization:** + - Course staff and instructors can manage enrollments within their courses + - Global staff can manage enrollments across all courses + **Serialization:** - Dates and timestamps are serialized to ISO 8601 format with explicit timezone offsets - UTC timestamps are preferred @@ -37,7 +42,8 @@ securityDefinitions: authorizationUrl: https://courses.example.com/oauth2/authorize tokenUrl: https://courses.example.com/oauth2/token scopes: - instructor: Instructor-level access to course data + read: Read access to enrollment data + write: Write access to manage enrollments SessionAuth: type: apiKey in: header @@ -45,7 +51,7 @@ securityDefinitions: description: Session-based authentication using Django session cookies security: - - OAuth2: [instructor] + - OAuth2: [read, write] - SessionAuth: [] tags: @@ -55,7 +61,7 @@ tags: paths: # ==================== ENROLLMENT ENDPOINTS ==================== - /api/instructor/v2/courses/{course_key}/enrollments: + /api/enrollment/v2/courses/{course_key}/enrollments: get: tags: - Enrollments @@ -105,7 +111,7 @@ paths: application/json: count: 1035 num_pages: 42 - next: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" + next: "/api/enrollment/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" previous: null results: - username: "bjohnson" @@ -213,7 +219,7 @@ paths: examples: application/json: task_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" - status_url: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" + status_url: "/api/enrollment/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" action: "enroll" count: 150 400: @@ -225,7 +231,7 @@ paths: 404: $ref: '#/responses/NotFound' - /api/instructor/v2/courses/{course_key}/enrollments/{email_or_username}: + /api/enrollment/v2/courses/{course_key}/enrollments/{email_or_username}: get: tags: - Enrollments @@ -373,7 +379,7 @@ responses: examples: application/json: error_code: "PERMISSION_DENIED" - developer_message: "You do not have instructor permissions for this course" + developer_message: "You do not have the required permissions for this course" user_message: "You do not have permission to perform this action" status_code: 403 @@ -409,7 +415,7 @@ definitions: format: uri description: URL to the next page of results x-nullable: true - example: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" + example: "/api/enrollment/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" previous: type: string format: uri @@ -571,7 +577,7 @@ definitions: type: string format: uri description: URL to poll for task status (see Task API for details) - example: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" + example: "/api/enrollment/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" action: type: string description: The action being performed From b73c30b9fcd48d14a0ee062638e470ee12820de4 Mon Sep 17 00:00:00 2001 From: Jesse Stewart Date: Tue, 13 Jan 2026 14:15:54 -0500 Subject: [PATCH 04/10] fix: pr feedback --- ...> 0004-instructor-enrollment-api-spec.rst} | 0 .../references/enrollment-v2-api-spec.yaml | 25 ++++++++----------- 2 files changed, 10 insertions(+), 15 deletions(-) rename lms/djangoapps/instructor/docs/decisions/{0003-instructor-enrollment-api-spec.rst => 0004-instructor-enrollment-api-spec.rst} (100%) diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0004-instructor-enrollment-api-spec.rst similarity index 100% rename from lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst rename to lms/djangoapps/instructor/docs/decisions/0004-instructor-enrollment-api-spec.rst diff --git a/lms/djangoapps/instructor/docs/references/enrollment-v2-api-spec.yaml b/lms/djangoapps/instructor/docs/references/enrollment-v2-api-spec.yaml index 2d3973595263..1aafc8f4d810 100644 --- a/lms/djangoapps/instructor/docs/references/enrollment-v2-api-spec.yaml +++ b/lms/djangoapps/instructor/docs/references/enrollment-v2-api-spec.yaml @@ -116,18 +116,18 @@ paths: results: - username: "bjohnson" email: "bela.j@example.com" - first_name: "Bela" - last_name: "Johnson" + full_name: "Bela Johnson" mode: "audit" is_active: true created: "2024-01-15T10:30:00Z" + beta_tester: false - username: "cpatel" email: "cyrus.patel@example.com" - first_name: "Cyrus" - last_name: "Patel" + full_name: "Cyrus Patel" mode: "audit" is_active: true created: "2024-01-16T14:22:00Z" + beta_tester: false 400: $ref: '#/responses/BadRequest' 401: @@ -259,13 +259,13 @@ paths: application/json: username: "john_harvard" email: "john@example.com" - first_name: "John" - last_name: "Harvard" + full_name: "John Harvard" mode: "audit" is_active: true created: "2024-01-15T10:30:00Z" enrollment_allowed: false auto_enroll: false + beta_tester: false 400: $ref: '#/responses/BadRequest' 401: @@ -433,8 +433,7 @@ definitions: required: - username - email - - first_name - - last_name + - full_name - is_active properties: username: @@ -446,14 +445,10 @@ definitions: format: email description: Learner's email address example: "john@example.com" - first_name: + full_name: type: string - description: Learner's first name - example: "John" - last_name: - type: string - description: Learner's last name - example: "Harvard" + description: Learner's full name + example: "John Harvard" mode: type: string description: Enrollment mode (audit, honor, verified, professional, etc.) From 430bf00f8ce56556905dc090661317bf4a39798e Mon Sep 17 00:00:00 2001 From: Jesse Stewart Date: Wed, 7 Jan 2026 12:07:59 -0500 Subject: [PATCH 05/10] feat: create schema for enrollment api --- .../0003-instructor-enrollment-api-spec.rst | 156 +++++ .../instructor-v2-enrollment-api-spec.yaml | 621 ++++++++++++++++++ 2 files changed, 777 insertions(+) create mode 100644 lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst create mode 100644 lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst new file mode 100644 index 000000000000..d43cf4dbb3db --- /dev/null +++ b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst @@ -0,0 +1,156 @@ +Instructor Enrollment API Specification +---------------------------------------- + +Status +====== + +**Draft** (=> **Provisional**) + +Context +======= + +The instructor dashboard is being migrated to a Micro-Frontend (MFE) architecture, which requires RESTful API endpoints. The current implementation provides enrollment operations (enroll, unenroll, list enrollments) through legacy endpoints in ``lms/djangoapps/instructor/enrollment.py``. + +Decisions +========= + +#. **RESTful Resource-Oriented Design** + + Use resource-oriented URLs: ``/api/instructor/v2/courses/{course_key}/enrollments`` + + Use appropriate HTTP methods per Open edX REST API Conventions: + + * ``GET`` for read operations (list enrollments, get enrollment details) + * ``POST`` for enrollments (enroll one or more learners) + * ``DELETE`` for unenrollments (unenroll a single learner) + +#. **Synchronous vs Asynchronous Execution** + + * Operations targeting a single learner execute synchronously and return ``200 OK`` + with immediate results (< 5s typical, typically 100-500ms) + * Operations targeting multiple learners queue a background task and return + ``202 Accepted`` with task tracking information + * Task monitoring uses shared Task API endpoint: + ``GET /api/instructor/v2/courses/{course_key}/tasks/{task_id}`` + (defined in separate Task API specification) + +#. **Enrollment State Model** + + * Support both active enrollments (``CourseEnrollment``) and pre-enrollments + (``CourseEnrollmentAllowed``) + * Track enrollment state transitions with before/after snapshots + * Handle cases where user doesn't exist yet (creates CourseEnrollmentAllowed) + * Support auto-enrollment upon user registration + * Support multiple enrollment modes (audit, honor, verified, professional, etc.) + +#. **Pagination and Performance** + + * Use DRF standard pagination format with ``next``, ``previous``, ``count``, + ``num_pages``, and ``results`` fields (not nested pagination) + * Default page size of 25, maximum of 100 per page + * 1-indexed page numbers for consistency with DRF defaults + * Return basic enrollment data by default to optimize performance + +#. **Optional Fields via requested_fields Parameter** + + * Support ``requested_fields`` query parameter per Open edX conventions + * Available optional fields: ``beta_tester``, ``profile_image`` + * Comma-delimited list format: ``?requested_fields=beta_tester,profile_image`` + * Reduces database queries and improves performance when optional data not needed + +#. **Authentication and Authorization** + + * Support both OAuth2 (for mobile clients and micro-services) and + Session-based authentication (for mobile webviews and browser clients) + * Require instructor-level permissions for all enrollment operations + * Follow separation of filtering and authorization (explicit filtering in URLs) + +#. **Error Handling** + + * Follow Open edX REST API Conventions error format + * Include ``error_code`` (machine-readable), ``developer_message``, + ``user_message`` (internationalized), and ``status_code`` + * Support ``field_errors`` object for field-specific validation errors + * Use appropriate HTTP status codes: 200, 202, 400, 401, 403, 404 + +#. **Date/Time Serialization** + + * Serialize all dates and timestamps to ISO 8601 format with explicit timezone offsets + * Prefer UTC timestamps + * Example format: ``2024-01-15T10:30:00Z`` + +#. **Email Notifications** + + * Support optional email notifications via ``email_students`` parameter + * Use different message types based on user state: + + * ``enrolled_enroll``: User already registered, being enrolled + * ``allowed_enroll``: User not yet registered, pre-enrollment created + * ``enrolled_unenroll``: User being unenrolled + * ``allowed_unenroll``: Pre-enrollment being removed + + * Support optional ``reason`` parameter included in notification emails + +#. **OpenAPI Specification** + + Maintain an OpenAPI specification at ``../references/instructor-v2-enrollment-api-spec.yaml`` + to guide implementation. This static specification serves as a reference during development, + but ``/api-docs/`` is the source of truth for what is actually deployed. Once implementation + is complete and the endpoints are live in ``/api-docs/``, the static spec file will be + deleted to avoid maintaining outdated documentation. + +Consequences +============ + +Positive +~~~~~~~~ + +* Consistent URL patterns following Open edX conventions make the API predictable +* Explicit sync/async behavior based on operation scope allows proper UI feedback +* Pagination support efficiently handles courses with thousands of enrollments +* Optional fields optimize performance by avoiding unnecessary database queries +* OpenAPI specification enables automated validation, testing, and type-safe client generation +* Resource-oriented design makes it easy to add new operations +* Support for both enrollments and pre-enrollments handles all use cases +* Before/after state tracking provides clear audit trail of changes +* Email notification support maintains current functionality for learner communication + +Negative +~~~~~~~~ + +* Existing clients using legacy enrollment endpoints need to be updated +* Dual maintenance during transition period +* Developers familiar with legacy endpoints need to learn new patterns +* Optional fields via ``requested_fields`` add complexity to serialization logic +* Async operations require additional task monitoring implementation + +Alternatives Considered +======================= + +#. **Separate Endpoints for Enroll/Unenroll** + + Considered ``POST /enrollments`` for enroll and ``POST /unenrollments`` for unenroll, + but using ``DELETE /enrollments/{id}`` is more RESTful and follows HTTP verb semantics. + +#. **Nested Pagination Format** + + Considered nesting pagination metadata under a ``pagination`` key (per Cliff Dyer's + proposal), but chose DRF standard flat format (``next``, ``previous``, ``count``, + ``num_pages``, ``results`` at top level) as it's the established convention + documented in Open edX REST API Conventions. + +#. **Expand Parameter Instead of requested_fields** + + Considered using ``expand`` parameter for related objects, but ``requested_fields`` + is more appropriate for optional fields that are not separate resources. Using + ``expand`` would imply these are related resources with their own endpoints, + which is not the case for beta tester status or profile images in this context. + +References +========== + +* OpenAPI Specification: ``../references/instructor-v2-enrollment-api-spec.yaml`` +* Live API Documentation: ``/api-docs/`` +* Legacy Implementation: ``lms/djangoapps/instructor/enrollment.py`` +* Open edX REST API Conventions: https://openedx.atlassian.net/wiki/spaces/AC/pages/18350757/Open+edX+REST+API+Conventions +* Optional Fields and API Versioning: https://openedx.atlassian.net/wiki/spaces/AC/pages/40862782/Optional+Fields+and+API+Versioning diff --git a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml new file mode 100644 index 000000000000..c8848fbd984b --- /dev/null +++ b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml @@ -0,0 +1,621 @@ +swagger: '2.0' +info: + title: Instructor Enrollment API + version: 2.0.0 + description: | + Modern REST API for instructor enrollment management operations. + + **Design Principles:** + - RESTful resource-oriented URLs + - Query parameters for filtering operations + - Clear separation between read and write operations + - Consistent error handling + - Follows Open edX REST API Conventions + + **Execution Model:** + - Operations that affect a single learner execute synchronously (< 5s typical) + - Operations that affect multiple learners queue a background task + - Use the task status endpoint to monitor background tasks + + **Authentication:** + - OAuth2 for mobile clients and micro-services + - Session-based authentication for mobile webviews and browser clients + + **Serialization:** + - Dates and timestamps are serialized to ISO 8601 format with explicit timezone offsets + - UTC timestamps are preferred + +host: courses.example.com +basePath: / +schemes: + - https + +securityDefinitions: + OAuth2: + type: oauth2 + flow: accessCode + authorizationUrl: https://courses.example.com/oauth2/authorize + tokenUrl: https://courses.example.com/oauth2/token + scopes: + instructor: Instructor-level access to course data + SessionAuth: + type: apiKey + in: header + name: Cookie + description: Session-based authentication using Django session cookies + +security: + - OAuth2: [instructor] + - SessionAuth: [] + +tags: + - name: Enrollments + description: Course enrollment operations + +paths: + # ==================== ENROLLMENT ENDPOINTS ==================== + + /api/instructor/v2/courses/{course_key}/enrollments: + get: + tags: + - Enrollments + summary: List course enrollments + description: | + Retrieve a paginated list of all enrollments for a course. + + **Performance:** Returns basic enrollment data by default. Use `requested_fields` + parameter to include additional data such as profile images or beta tester status. + + **Pagination:** Uses DRF standard pagination format with `next`, `previous`, + `count`, `num_pages`, and `results` fields. + operationId: listEnrollments + produces: + - application/json + parameters: + - $ref: '#/parameters/CourseKey' + - name: page + in: query + description: Page number (1-indexed) + required: false + type: integer + minimum: 1 + default: 1 + - name: page_size + in: query + description: Number of results per page + required: false + type: integer + minimum: 1 + maximum: 100 + default: 25 + - name: requested_fields + in: query + description: | + Comma-delimited list of optional fields to include in response. + Available fields: `beta_tester`, `profile_image` + required: false + type: string + x-example: "beta_tester,profile_image" + responses: + 200: + description: Enrollments retrieved successfully + schema: + $ref: '#/definitions/EnrollmentList' + examples: + application/json: + count: 1035 + num_pages: 42 + next: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" + previous: null + results: + - username: "bjohnson" + email: "bela.j@example.com" + first_name: "Bela" + last_name: "Johnson" + mode: "audit" + is_active: true + created: "2024-01-15T10:30:00Z" + - username: "cpatel" + email: "cyrus.patel@example.com" + first_name: "Cyrus" + last_name: "Patel" + mode: "audit" + is_active: true + created: "2024-01-16T14:22:00Z" + 400: + $ref: '#/responses/BadRequest' + 401: + $ref: '#/responses/Unauthorized' + 403: + $ref: '#/responses/Forbidden' + 404: + $ref: '#/responses/NotFound' + + post: + tags: + - Enrollments + summary: Enroll learners in course + description: | + Enroll one or more learners in a course by email or username. + + **Behavior:** + - If user exists and is active: Enrolls immediately in specified mode (or default) + - If user does not exist: Creates CourseEnrollmentAllowed record + - When the user registers, they will be auto-enrolled if `auto_enroll` is true + + **Scope:** + - Single learner: Synchronous operation (~100-500ms) + - Multiple learners: Asynchronous task queued + + **Email Notifications:** + - If user is already registered: Uses "enrolled_enroll" message type + - If user is not registered: Uses "allowed_enroll" message type + operationId: enrollLearners + consumes: + - application/json + produces: + - application/json + parameters: + - $ref: '#/parameters/CourseKey' + - name: body + in: body + required: true + schema: + type: object + required: + - identifiers + properties: + identifiers: + type: array + description: List of email addresses or usernames to enroll + minItems: 1 + items: + type: string + example: ["john@example.com", "jane_doe"] + auto_enroll: + type: boolean + description: Auto-enroll user when they register (for non-registered users) + default: false + email_students: + type: boolean + description: Send email notification to learners + default: false + reason: + type: string + description: Reason for enrollment (included in email if email_students is true) + x-nullable: true + mode: + type: string + description: Enrollment mode (audit, honor, verified, professional, etc.) + x-nullable: true + example: "audit" + responses: + 200: + description: Single learner enrolled successfully (synchronous) + schema: + $ref: '#/definitions/EnrollmentOperationResult' + examples: + application/json: + action: "enroll" + results: + - identifier: "john@example.com" + before: + enrolled: false + allowed: false + after: + enrolled: true + allowed: false + mode: "audit" + 202: + description: Multiple learner enrollment task queued (asynchronous) + schema: + $ref: '#/definitions/AsyncOperationResult' + examples: + application/json: + task_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + status_url: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" + action: "enroll" + count: 150 + 400: + $ref: '#/responses/BadRequest' + 401: + $ref: '#/responses/Unauthorized' + 403: + $ref: '#/responses/Forbidden' + 404: + $ref: '#/responses/NotFound' + + /api/instructor/v2/courses/{course_key}/enrollments/{email_or_username}: + get: + tags: + - Enrollments + summary: Get learner enrollment details + description: | + Retrieve detailed enrollment information for a specific learner. + + **Returns:** + - Current enrollment status + - CourseEnrollmentAllowed status (if applicable) + - Enrollment mode + - User profile information + operationId: getEnrollment + produces: + - application/json + parameters: + - $ref: '#/parameters/CourseKey' + - $ref: '#/parameters/LearnerIdentifierPath' + responses: + 200: + description: Enrollment information retrieved successfully + schema: + $ref: '#/definitions/Enrollment' + examples: + application/json: + username: "john_harvard" + email: "john@example.com" + first_name: "John" + last_name: "Harvard" + mode: "audit" + is_active: true + created: "2024-01-15T10:30:00Z" + enrollment_allowed: false + auto_enroll: false + 400: + $ref: '#/responses/BadRequest' + 401: + $ref: '#/responses/Unauthorized' + 403: + $ref: '#/responses/Forbidden' + 404: + $ref: '#/responses/NotFound' + + delete: + tags: + - Enrollments + summary: Unenroll learner from course + description: | + Unenroll a learner from a course. + + **Behavior:** + - If user is enrolled: Unenrolls from course + - If user has CourseEnrollmentAllowed: Deletes the allowed enrollment + - Both operations are performed if both conditions exist + - Optionally sends notification email to learner + + **Email Notifications:** + - If user was enrolled: Uses "enrolled_unenroll" message type + - If user had allowed enrollment: Uses "allowed_unenroll" message type + operationId: unenrollLearner + produces: + - application/json + parameters: + - $ref: '#/parameters/CourseKey' + - $ref: '#/parameters/LearnerIdentifierPath' + - name: email_student + in: query + description: Send email notification to learner + required: false + type: boolean + default: false + responses: + 200: + description: Learner unenrolled successfully + schema: + $ref: '#/definitions/EnrollmentOperationResult' + examples: + application/json: + action: "unenroll" + results: + - identifier: "john@example.com" + before: + enrolled: true + allowed: false + mode: "audit" + after: + enrolled: false + allowed: false + 400: + $ref: '#/responses/BadRequest' + 401: + $ref: '#/responses/Unauthorized' + 403: + $ref: '#/responses/Forbidden' + 404: + $ref: '#/responses/NotFound' + +# ==================== COMPONENTS ==================== + +parameters: + CourseKey: + name: course_key + in: path + required: true + description: Course identifier in format `course-v1:{org}+{course}+{run}` + type: string + pattern: '^course-v1:[^/+]+(\\+[^/+]+)+(\\+[^/]+)$' + x-example: "course-v1:edX+DemoX+Demo_Course" + + LearnerIdentifierPath: + name: email_or_username + in: path + required: true + description: Learner's username or email address + type: string + minLength: 1 + +responses: + BadRequest: + description: Bad request - Invalid parameters or malformed request + schema: + $ref: '#/definitions/Error' + examples: + application/json: + error_code: "INVALID_PARAMETER" + developer_message: "Invalid course key format" + user_message: "The course identifier is not valid" + status_code: 400 + + Unauthorized: + description: Unauthorized - Authentication required + schema: + $ref: '#/definitions/Error' + examples: + application/json: + error_code: "AUTHENTICATION_REQUIRED" + developer_message: "You must be authenticated to access this endpoint" + user_message: "Please log in to continue" + status_code: 401 + + Forbidden: + description: Forbidden - Insufficient permissions + schema: + $ref: '#/definitions/Error' + examples: + application/json: + error_code: "PERMISSION_DENIED" + developer_message: "You do not have instructor permissions for this course" + user_message: "You do not have permission to perform this action" + status_code: 403 + + NotFound: + description: Not found - Resource does not exist + schema: + $ref: '#/definitions/Error' + examples: + application/json: + error_code: "RESOURCE_NOT_FOUND" + developer_message: "The specified resource does not exist" + user_message: "The requested item could not be found" + status_code: 404 + +definitions: + EnrollmentList: + type: object + description: Paginated list of enrollments + required: + - count + - results + properties: + count: + type: integer + description: Total number of enrollments across all pages + example: 1035 + num_pages: + type: integer + description: Total number of pages + example: 42 + next: + type: string + format: uri + description: URL to the next page of results + x-nullable: true + example: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" + previous: + type: string + format: uri + description: URL to the previous page of results + x-nullable: true + results: + type: array + description: List of enrollments on this page + items: + $ref: '#/definitions/Enrollment' + + Enrollment: + type: object + description: Learner enrollment information + required: + - username + - email + - first_name + - last_name + - is_active + properties: + username: + type: string + description: Learner's username + example: "john_harvard" + email: + type: string + format: email + description: Learner's email address + example: "john@example.com" + first_name: + type: string + description: Learner's first name + example: "John" + last_name: + type: string + description: Learner's last name + example: "Harvard" + mode: + type: string + description: Enrollment mode (audit, honor, verified, professional, etc.) + x-nullable: true + example: "audit" + is_active: + type: boolean + description: Whether the enrollment is active + example: true + created: + type: string + format: date-time + description: Enrollment creation timestamp (ISO 8601 format with timezone) + x-nullable: true + example: "2024-01-15T10:30:00Z" + enrollment_allowed: + type: boolean + description: Whether user has a CourseEnrollmentAllowed record + example: false + auto_enroll: + type: boolean + description: Whether user will be auto-enrolled upon registration + example: false + beta_tester: + type: boolean + description: Whether learner is a beta tester (only present if requested_fields includes beta_tester) + x-nullable: true + example: false + profile_image: + type: object + description: Learner's profile image URLs (only present if requested_fields includes profile_image) + x-nullable: true + properties: + has_image: + type: boolean + description: Whether the user has uploaded a profile image + image_url_full: + type: string + format: uri + description: Full size image URL + image_url_large: + type: string + format: uri + description: Large thumbnail URL + image_url_medium: + type: string + format: uri + description: Medium thumbnail URL + image_url_small: + type: string + format: uri + description: Small thumbnail URL + + EnrollmentOperationResult: + type: object + description: Result from an enrollment operation (enroll/unenroll) + required: + - action + - results + properties: + action: + type: string + enum: ["enroll", "unenroll"] + description: The action that was performed + results: + type: array + description: Results for each identifier + items: + type: object + required: + - identifier + - before + - after + properties: + identifier: + type: string + description: Email or username that was processed + before: + $ref: '#/definitions/EnrollmentState' + after: + $ref: '#/definitions/EnrollmentState' + error: + type: string + description: Error message if operation failed for this identifier + x-nullable: true + + EnrollmentState: + type: object + description: Enrollment state snapshot + required: + - enrolled + - allowed + properties: + enrolled: + type: boolean + description: Whether user is enrolled + allowed: + type: boolean + description: Whether user has CourseEnrollmentAllowed record + mode: + type: string + description: Enrollment mode (if enrolled) + x-nullable: true + auto_enroll: + type: boolean + description: Auto-enroll setting (if allowed) + x-nullable: true + + AsyncOperationResult: + type: object + description: Task information for an asynchronous operation + required: + - task_id + - status_url + properties: + task_id: + type: string + description: Unique task identifier + example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + status_url: + type: string + format: uri + description: URL to poll for task status (see Task API for details) + example: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" + action: + type: string + description: The action being performed + example: "enroll" + count: + type: integer + description: Number of learners being processed + example: 150 + + Error: + type: object + description: Error response + required: + - error_code + - developer_message + - user_message + - status_code + properties: + error_code: + type: string + description: Machine-readable error code + example: "RESOURCE_NOT_FOUND" + developer_message: + type: string + description: Verbose, plain language description of the problem for developers + example: "The specified course does not exist in the modulestore" + user_message: + type: string + description: User-friendly error message (internationalized) + example: "The requested course could not be found" + status_code: + type: integer + description: HTTP status code + example: 404 + field_errors: + type: object + description: Field-specific validation errors (if applicable) + x-nullable: true + additionalProperties: + type: object + properties: + developer_message: + type: string + description: Technical error details + user_message: + type: string + description: User-friendly error message From 3de03a9db2cdfa2783bc3029490a4e81e232b466 Mon Sep 17 00:00:00 2001 From: Jesse Stewart Date: Wed, 7 Jan 2026 12:49:56 -0500 Subject: [PATCH 06/10] docs: pr feedback --- .../docs/decisions/0003-instructor-enrollment-api-spec.rst | 4 +++- .../docs/references/instructor-v2-enrollment-api-spec.yaml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst index d43cf4dbb3db..cbf042b8114c 100644 --- a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst +++ b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst @@ -4,7 +4,9 @@ Instructor Enrollment API Specification Status ====== -**Draft** (=> **Provisional**) +**Draft** + +This ADR will move to **Provisional** status once the OpenAPI specification is approved and implementation begins. It will move to **Accepted** status once the API is fully implemented and deployed. Context ======= diff --git a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml index c8848fbd984b..3c43286f69bc 100644 --- a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml +++ b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml @@ -332,7 +332,7 @@ parameters: required: true description: Course identifier in format `course-v1:{org}+{course}+{run}` type: string - pattern: '^course-v1:[^/+]+(\\+[^/+]+)+(\\+[^/]+)$' + pattern: '^course-v1:[^/+]+(\+[^/+]+)+(\+[^/]+)$' x-example: "course-v1:edX+DemoX+Demo_Course" LearnerIdentifierPath: From 35bae41a11b8a32c9ee71c7844037702e95a9271 Mon Sep 17 00:00:00 2001 From: Jesse Stewart Date: Mon, 12 Jan 2026 15:25:13 -0500 Subject: [PATCH 07/10] fix: pr feedback. rename to enrollment api --- .../0003-instructor-enrollment-api-spec.rst | 22 +- .../instructor-v2-enrollment-api-spec.yaml | 621 ------------------ 2 files changed, 14 insertions(+), 629 deletions(-) delete mode 100644 lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst index cbf042b8114c..a1d03ed08a71 100644 --- a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst +++ b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst @@ -1,5 +1,5 @@ -Instructor Enrollment API Specification ----------------------------------------- +Enrollment API v2 Specification +-------------------------------- Status ====== @@ -11,14 +11,14 @@ This ADR will move to **Provisional** status once the OpenAPI specification is a Context ======= -The instructor dashboard is being migrated to a Micro-Frontend (MFE) architecture, which requires RESTful API endpoints. The current implementation provides enrollment operations (enroll, unenroll, list enrollments) through legacy endpoints in ``lms/djangoapps/instructor/enrollment.py``. +The existing enrollment API (v1) has several limitations that make it difficult to use in modern applications. A new v2 enrollment API is needed to support the instructor dashboard MFE migration and other enrollment management use cases across the platform. The current implementation provides enrollment operations (enroll, unenroll, list enrollments) through legacy endpoints in ``lms/djangoapps/instructor/enrollment.py`` and the v1 enrollment API at ``/api/enrollment/v1/``. Decisions ========= #. **RESTful Resource-Oriented Design** - Use resource-oriented URLs: ``/api/instructor/v2/courses/{course_key}/enrollments`` + Use resource-oriented URLs: ``/api/enrollment/v2/courses/{course_key}/enrollments`` Use appropriate HTTP methods per Open edX REST API Conventions: @@ -33,7 +33,7 @@ Decisions * Operations targeting multiple learners queue a background task and return ``202 Accepted`` with task tracking information * Task monitoring uses shared Task API endpoint: - ``GET /api/instructor/v2/courses/{course_key}/tasks/{task_id}`` + ``GET /api/enrollment/v2/courses/{course_key}/tasks/{task_id}`` (defined in separate Task API specification) #. **Enrollment State Model** @@ -64,7 +64,12 @@ Decisions * Support both OAuth2 (for mobile clients and micro-services) and Session-based authentication (for mobile webviews and browser clients) - * Require instructor-level permissions for all enrollment operations + * Require appropriate permissions based on operation scope: + + * Course staff or instructor: Can manage enrollments within their courses + * Global staff: Can manage enrollments across all courses + * Self-enrollment: Learners can enroll/unenroll themselves (future consideration) + * Follow separation of filtering and authorization (explicit filtering in URLs) #. **Error Handling** @@ -95,7 +100,7 @@ Decisions #. **OpenAPI Specification** - Maintain an OpenAPI specification at ``../references/instructor-v2-enrollment-api-spec.yaml`` + Maintain an OpenAPI specification at ``../references/enrollment-v2-api-spec.yaml`` to guide implementation. This static specification serves as a reference during development, but ``/api-docs/`` is the source of truth for what is actually deployed. Once implementation is complete and the endpoints are live in ``/api-docs/``, the static spec file will be @@ -151,8 +156,9 @@ Alternatives Considered References ========== -* OpenAPI Specification: ``../references/instructor-v2-enrollment-api-spec.yaml`` +* OpenAPI Specification: ``../references/enrollment-v2-api-spec.yaml`` * Live API Documentation: ``/api-docs/`` +* Existing v1 Enrollment API: ``https://master.openedx.io/api-docs/#/enrollment`` * Legacy Implementation: ``lms/djangoapps/instructor/enrollment.py`` * Open edX REST API Conventions: https://openedx.atlassian.net/wiki/spaces/AC/pages/18350757/Open+edX+REST+API+Conventions * Optional Fields and API Versioning: https://openedx.atlassian.net/wiki/spaces/AC/pages/40862782/Optional+Fields+and+API+Versioning diff --git a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml b/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml deleted file mode 100644 index 3c43286f69bc..000000000000 --- a/lms/djangoapps/instructor/docs/references/instructor-v2-enrollment-api-spec.yaml +++ /dev/null @@ -1,621 +0,0 @@ -swagger: '2.0' -info: - title: Instructor Enrollment API - version: 2.0.0 - description: | - Modern REST API for instructor enrollment management operations. - - **Design Principles:** - - RESTful resource-oriented URLs - - Query parameters for filtering operations - - Clear separation between read and write operations - - Consistent error handling - - Follows Open edX REST API Conventions - - **Execution Model:** - - Operations that affect a single learner execute synchronously (< 5s typical) - - Operations that affect multiple learners queue a background task - - Use the task status endpoint to monitor background tasks - - **Authentication:** - - OAuth2 for mobile clients and micro-services - - Session-based authentication for mobile webviews and browser clients - - **Serialization:** - - Dates and timestamps are serialized to ISO 8601 format with explicit timezone offsets - - UTC timestamps are preferred - -host: courses.example.com -basePath: / -schemes: - - https - -securityDefinitions: - OAuth2: - type: oauth2 - flow: accessCode - authorizationUrl: https://courses.example.com/oauth2/authorize - tokenUrl: https://courses.example.com/oauth2/token - scopes: - instructor: Instructor-level access to course data - SessionAuth: - type: apiKey - in: header - name: Cookie - description: Session-based authentication using Django session cookies - -security: - - OAuth2: [instructor] - - SessionAuth: [] - -tags: - - name: Enrollments - description: Course enrollment operations - -paths: - # ==================== ENROLLMENT ENDPOINTS ==================== - - /api/instructor/v2/courses/{course_key}/enrollments: - get: - tags: - - Enrollments - summary: List course enrollments - description: | - Retrieve a paginated list of all enrollments for a course. - - **Performance:** Returns basic enrollment data by default. Use `requested_fields` - parameter to include additional data such as profile images or beta tester status. - - **Pagination:** Uses DRF standard pagination format with `next`, `previous`, - `count`, `num_pages`, and `results` fields. - operationId: listEnrollments - produces: - - application/json - parameters: - - $ref: '#/parameters/CourseKey' - - name: page - in: query - description: Page number (1-indexed) - required: false - type: integer - minimum: 1 - default: 1 - - name: page_size - in: query - description: Number of results per page - required: false - type: integer - minimum: 1 - maximum: 100 - default: 25 - - name: requested_fields - in: query - description: | - Comma-delimited list of optional fields to include in response. - Available fields: `beta_tester`, `profile_image` - required: false - type: string - x-example: "beta_tester,profile_image" - responses: - 200: - description: Enrollments retrieved successfully - schema: - $ref: '#/definitions/EnrollmentList' - examples: - application/json: - count: 1035 - num_pages: 42 - next: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" - previous: null - results: - - username: "bjohnson" - email: "bela.j@example.com" - first_name: "Bela" - last_name: "Johnson" - mode: "audit" - is_active: true - created: "2024-01-15T10:30:00Z" - - username: "cpatel" - email: "cyrus.patel@example.com" - first_name: "Cyrus" - last_name: "Patel" - mode: "audit" - is_active: true - created: "2024-01-16T14:22:00Z" - 400: - $ref: '#/responses/BadRequest' - 401: - $ref: '#/responses/Unauthorized' - 403: - $ref: '#/responses/Forbidden' - 404: - $ref: '#/responses/NotFound' - - post: - tags: - - Enrollments - summary: Enroll learners in course - description: | - Enroll one or more learners in a course by email or username. - - **Behavior:** - - If user exists and is active: Enrolls immediately in specified mode (or default) - - If user does not exist: Creates CourseEnrollmentAllowed record - - When the user registers, they will be auto-enrolled if `auto_enroll` is true - - **Scope:** - - Single learner: Synchronous operation (~100-500ms) - - Multiple learners: Asynchronous task queued - - **Email Notifications:** - - If user is already registered: Uses "enrolled_enroll" message type - - If user is not registered: Uses "allowed_enroll" message type - operationId: enrollLearners - consumes: - - application/json - produces: - - application/json - parameters: - - $ref: '#/parameters/CourseKey' - - name: body - in: body - required: true - schema: - type: object - required: - - identifiers - properties: - identifiers: - type: array - description: List of email addresses or usernames to enroll - minItems: 1 - items: - type: string - example: ["john@example.com", "jane_doe"] - auto_enroll: - type: boolean - description: Auto-enroll user when they register (for non-registered users) - default: false - email_students: - type: boolean - description: Send email notification to learners - default: false - reason: - type: string - description: Reason for enrollment (included in email if email_students is true) - x-nullable: true - mode: - type: string - description: Enrollment mode (audit, honor, verified, professional, etc.) - x-nullable: true - example: "audit" - responses: - 200: - description: Single learner enrolled successfully (synchronous) - schema: - $ref: '#/definitions/EnrollmentOperationResult' - examples: - application/json: - action: "enroll" - results: - - identifier: "john@example.com" - before: - enrolled: false - allowed: false - after: - enrolled: true - allowed: false - mode: "audit" - 202: - description: Multiple learner enrollment task queued (asynchronous) - schema: - $ref: '#/definitions/AsyncOperationResult' - examples: - application/json: - task_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" - status_url: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" - action: "enroll" - count: 150 - 400: - $ref: '#/responses/BadRequest' - 401: - $ref: '#/responses/Unauthorized' - 403: - $ref: '#/responses/Forbidden' - 404: - $ref: '#/responses/NotFound' - - /api/instructor/v2/courses/{course_key}/enrollments/{email_or_username}: - get: - tags: - - Enrollments - summary: Get learner enrollment details - description: | - Retrieve detailed enrollment information for a specific learner. - - **Returns:** - - Current enrollment status - - CourseEnrollmentAllowed status (if applicable) - - Enrollment mode - - User profile information - operationId: getEnrollment - produces: - - application/json - parameters: - - $ref: '#/parameters/CourseKey' - - $ref: '#/parameters/LearnerIdentifierPath' - responses: - 200: - description: Enrollment information retrieved successfully - schema: - $ref: '#/definitions/Enrollment' - examples: - application/json: - username: "john_harvard" - email: "john@example.com" - first_name: "John" - last_name: "Harvard" - mode: "audit" - is_active: true - created: "2024-01-15T10:30:00Z" - enrollment_allowed: false - auto_enroll: false - 400: - $ref: '#/responses/BadRequest' - 401: - $ref: '#/responses/Unauthorized' - 403: - $ref: '#/responses/Forbidden' - 404: - $ref: '#/responses/NotFound' - - delete: - tags: - - Enrollments - summary: Unenroll learner from course - description: | - Unenroll a learner from a course. - - **Behavior:** - - If user is enrolled: Unenrolls from course - - If user has CourseEnrollmentAllowed: Deletes the allowed enrollment - - Both operations are performed if both conditions exist - - Optionally sends notification email to learner - - **Email Notifications:** - - If user was enrolled: Uses "enrolled_unenroll" message type - - If user had allowed enrollment: Uses "allowed_unenroll" message type - operationId: unenrollLearner - produces: - - application/json - parameters: - - $ref: '#/parameters/CourseKey' - - $ref: '#/parameters/LearnerIdentifierPath' - - name: email_student - in: query - description: Send email notification to learner - required: false - type: boolean - default: false - responses: - 200: - description: Learner unenrolled successfully - schema: - $ref: '#/definitions/EnrollmentOperationResult' - examples: - application/json: - action: "unenroll" - results: - - identifier: "john@example.com" - before: - enrolled: true - allowed: false - mode: "audit" - after: - enrolled: false - allowed: false - 400: - $ref: '#/responses/BadRequest' - 401: - $ref: '#/responses/Unauthorized' - 403: - $ref: '#/responses/Forbidden' - 404: - $ref: '#/responses/NotFound' - -# ==================== COMPONENTS ==================== - -parameters: - CourseKey: - name: course_key - in: path - required: true - description: Course identifier in format `course-v1:{org}+{course}+{run}` - type: string - pattern: '^course-v1:[^/+]+(\+[^/+]+)+(\+[^/]+)$' - x-example: "course-v1:edX+DemoX+Demo_Course" - - LearnerIdentifierPath: - name: email_or_username - in: path - required: true - description: Learner's username or email address - type: string - minLength: 1 - -responses: - BadRequest: - description: Bad request - Invalid parameters or malformed request - schema: - $ref: '#/definitions/Error' - examples: - application/json: - error_code: "INVALID_PARAMETER" - developer_message: "Invalid course key format" - user_message: "The course identifier is not valid" - status_code: 400 - - Unauthorized: - description: Unauthorized - Authentication required - schema: - $ref: '#/definitions/Error' - examples: - application/json: - error_code: "AUTHENTICATION_REQUIRED" - developer_message: "You must be authenticated to access this endpoint" - user_message: "Please log in to continue" - status_code: 401 - - Forbidden: - description: Forbidden - Insufficient permissions - schema: - $ref: '#/definitions/Error' - examples: - application/json: - error_code: "PERMISSION_DENIED" - developer_message: "You do not have instructor permissions for this course" - user_message: "You do not have permission to perform this action" - status_code: 403 - - NotFound: - description: Not found - Resource does not exist - schema: - $ref: '#/definitions/Error' - examples: - application/json: - error_code: "RESOURCE_NOT_FOUND" - developer_message: "The specified resource does not exist" - user_message: "The requested item could not be found" - status_code: 404 - -definitions: - EnrollmentList: - type: object - description: Paginated list of enrollments - required: - - count - - results - properties: - count: - type: integer - description: Total number of enrollments across all pages - example: 1035 - num_pages: - type: integer - description: Total number of pages - example: 42 - next: - type: string - format: uri - description: URL to the next page of results - x-nullable: true - example: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2" - previous: - type: string - format: uri - description: URL to the previous page of results - x-nullable: true - results: - type: array - description: List of enrollments on this page - items: - $ref: '#/definitions/Enrollment' - - Enrollment: - type: object - description: Learner enrollment information - required: - - username - - email - - first_name - - last_name - - is_active - properties: - username: - type: string - description: Learner's username - example: "john_harvard" - email: - type: string - format: email - description: Learner's email address - example: "john@example.com" - first_name: - type: string - description: Learner's first name - example: "John" - last_name: - type: string - description: Learner's last name - example: "Harvard" - mode: - type: string - description: Enrollment mode (audit, honor, verified, professional, etc.) - x-nullable: true - example: "audit" - is_active: - type: boolean - description: Whether the enrollment is active - example: true - created: - type: string - format: date-time - description: Enrollment creation timestamp (ISO 8601 format with timezone) - x-nullable: true - example: "2024-01-15T10:30:00Z" - enrollment_allowed: - type: boolean - description: Whether user has a CourseEnrollmentAllowed record - example: false - auto_enroll: - type: boolean - description: Whether user will be auto-enrolled upon registration - example: false - beta_tester: - type: boolean - description: Whether learner is a beta tester (only present if requested_fields includes beta_tester) - x-nullable: true - example: false - profile_image: - type: object - description: Learner's profile image URLs (only present if requested_fields includes profile_image) - x-nullable: true - properties: - has_image: - type: boolean - description: Whether the user has uploaded a profile image - image_url_full: - type: string - format: uri - description: Full size image URL - image_url_large: - type: string - format: uri - description: Large thumbnail URL - image_url_medium: - type: string - format: uri - description: Medium thumbnail URL - image_url_small: - type: string - format: uri - description: Small thumbnail URL - - EnrollmentOperationResult: - type: object - description: Result from an enrollment operation (enroll/unenroll) - required: - - action - - results - properties: - action: - type: string - enum: ["enroll", "unenroll"] - description: The action that was performed - results: - type: array - description: Results for each identifier - items: - type: object - required: - - identifier - - before - - after - properties: - identifier: - type: string - description: Email or username that was processed - before: - $ref: '#/definitions/EnrollmentState' - after: - $ref: '#/definitions/EnrollmentState' - error: - type: string - description: Error message if operation failed for this identifier - x-nullable: true - - EnrollmentState: - type: object - description: Enrollment state snapshot - required: - - enrolled - - allowed - properties: - enrolled: - type: boolean - description: Whether user is enrolled - allowed: - type: boolean - description: Whether user has CourseEnrollmentAllowed record - mode: - type: string - description: Enrollment mode (if enrolled) - x-nullable: true - auto_enroll: - type: boolean - description: Auto-enroll setting (if allowed) - x-nullable: true - - AsyncOperationResult: - type: object - description: Task information for an asynchronous operation - required: - - task_id - - status_url - properties: - task_id: - type: string - description: Unique task identifier - example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" - status_url: - type: string - format: uri - description: URL to poll for task status (see Task API for details) - example: "/api/instructor/v2/courses/course-v1:edX+DemoX+Demo_Course/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890" - action: - type: string - description: The action being performed - example: "enroll" - count: - type: integer - description: Number of learners being processed - example: 150 - - Error: - type: object - description: Error response - required: - - error_code - - developer_message - - user_message - - status_code - properties: - error_code: - type: string - description: Machine-readable error code - example: "RESOURCE_NOT_FOUND" - developer_message: - type: string - description: Verbose, plain language description of the problem for developers - example: "The specified course does not exist in the modulestore" - user_message: - type: string - description: User-friendly error message (internationalized) - example: "The requested course could not be found" - status_code: - type: integer - description: HTTP status code - example: 404 - field_errors: - type: object - description: Field-specific validation errors (if applicable) - x-nullable: true - additionalProperties: - type: object - properties: - developer_message: - type: string - description: Technical error details - user_message: - type: string - description: User-friendly error message From 6bb741973bcf0af32c6c643ec169e982f9546061 Mon Sep 17 00:00:00 2001 From: wgu-jesse-stewart Date: Tue, 13 Jan 2026 13:49:48 -0500 Subject: [PATCH 08/10] docs: update enrollment API spec with full_name and beta_tester fields Replace first_name/last_name with full_name in enrollment response examples and schema. Add beta_tester field to response examples. Co-authored-by: Sarina Canelake --- .../docs/decisions/0003-instructor-enrollment-api-spec.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst index a1d03ed08a71..7dfbb850deb9 100644 --- a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst +++ b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst @@ -160,5 +160,5 @@ References * Live API Documentation: ``/api-docs/`` * Existing v1 Enrollment API: ``https://master.openedx.io/api-docs/#/enrollment`` * Legacy Implementation: ``lms/djangoapps/instructor/enrollment.py`` -* Open edX REST API Conventions: https://openedx.atlassian.net/wiki/spaces/AC/pages/18350757/Open+edX+REST+API+Conventions +* `Open edX REST API Conventions `_ * Optional Fields and API Versioning: https://openedx.atlassian.net/wiki/spaces/AC/pages/40862782/Optional+Fields+and+API+Versioning From a98605c143c64b7066517054e3ddbb925061268d Mon Sep 17 00:00:00 2001 From: Jesse Stewart Date: Tue, 13 Jan 2026 16:53:20 -0500 Subject: [PATCH 09/10] Merge branch '37813/v2-enrollment-api-schema' of https://github.com/WGU-Open-edX/edx-platform into 37813/v2-enrollment-api-schema --- .../docs/decisions/0004-instructor-enrollment-api-spec.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/docs/decisions/0004-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0004-instructor-enrollment-api-spec.rst index a1d03ed08a71..075440b9870a 100644 --- a/lms/djangoapps/instructor/docs/decisions/0004-instructor-enrollment-api-spec.rst +++ b/lms/djangoapps/instructor/docs/decisions/0004-instructor-enrollment-api-spec.rst @@ -160,5 +160,5 @@ References * Live API Documentation: ``/api-docs/`` * Existing v1 Enrollment API: ``https://master.openedx.io/api-docs/#/enrollment`` * Legacy Implementation: ``lms/djangoapps/instructor/enrollment.py`` -* Open edX REST API Conventions: https://openedx.atlassian.net/wiki/spaces/AC/pages/18350757/Open+edX+REST+API+Conventions -* Optional Fields and API Versioning: https://openedx.atlassian.net/wiki/spaces/AC/pages/40862782/Optional+Fields+and+API+Versioning +* `Open edX REST API Conventions ` +* `Optional Fields and API Versioning: https://openedx.atlassian.net/wiki/spaces/AC/pages/40862782/Optional+Fields+and+API+Versioning` From e4cdfde8e56c147f5aaf41db20801aa7e9b38f48 Mon Sep 17 00:00:00 2001 From: wgu-jesse-stewart Date: Wed, 14 Jan 2026 09:48:23 -0500 Subject: [PATCH 10/10] docs: delete lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst --- .../0003-instructor-enrollment-api-spec.rst | 164 ------------------ 1 file changed, 164 deletions(-) delete mode 100644 lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst diff --git a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst b/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst deleted file mode 100644 index 7dfbb850deb9..000000000000 --- a/lms/djangoapps/instructor/docs/decisions/0003-instructor-enrollment-api-spec.rst +++ /dev/null @@ -1,164 +0,0 @@ -Enrollment API v2 Specification --------------------------------- - -Status -====== - -**Draft** - -This ADR will move to **Provisional** status once the OpenAPI specification is approved and implementation begins. It will move to **Accepted** status once the API is fully implemented and deployed. - -Context -======= - -The existing enrollment API (v1) has several limitations that make it difficult to use in modern applications. A new v2 enrollment API is needed to support the instructor dashboard MFE migration and other enrollment management use cases across the platform. The current implementation provides enrollment operations (enroll, unenroll, list enrollments) through legacy endpoints in ``lms/djangoapps/instructor/enrollment.py`` and the v1 enrollment API at ``/api/enrollment/v1/``. - -Decisions -========= - -#. **RESTful Resource-Oriented Design** - - Use resource-oriented URLs: ``/api/enrollment/v2/courses/{course_key}/enrollments`` - - Use appropriate HTTP methods per Open edX REST API Conventions: - - * ``GET`` for read operations (list enrollments, get enrollment details) - * ``POST`` for enrollments (enroll one or more learners) - * ``DELETE`` for unenrollments (unenroll a single learner) - -#. **Synchronous vs Asynchronous Execution** - - * Operations targeting a single learner execute synchronously and return ``200 OK`` - with immediate results (< 5s typical, typically 100-500ms) - * Operations targeting multiple learners queue a background task and return - ``202 Accepted`` with task tracking information - * Task monitoring uses shared Task API endpoint: - ``GET /api/enrollment/v2/courses/{course_key}/tasks/{task_id}`` - (defined in separate Task API specification) - -#. **Enrollment State Model** - - * Support both active enrollments (``CourseEnrollment``) and pre-enrollments - (``CourseEnrollmentAllowed``) - * Track enrollment state transitions with before/after snapshots - * Handle cases where user doesn't exist yet (creates CourseEnrollmentAllowed) - * Support auto-enrollment upon user registration - * Support multiple enrollment modes (audit, honor, verified, professional, etc.) - -#. **Pagination and Performance** - - * Use DRF standard pagination format with ``next``, ``previous``, ``count``, - ``num_pages``, and ``results`` fields (not nested pagination) - * Default page size of 25, maximum of 100 per page - * 1-indexed page numbers for consistency with DRF defaults - * Return basic enrollment data by default to optimize performance - -#. **Optional Fields via requested_fields Parameter** - - * Support ``requested_fields`` query parameter per Open edX conventions - * Available optional fields: ``beta_tester``, ``profile_image`` - * Comma-delimited list format: ``?requested_fields=beta_tester,profile_image`` - * Reduces database queries and improves performance when optional data not needed - -#. **Authentication and Authorization** - - * Support both OAuth2 (for mobile clients and micro-services) and - Session-based authentication (for mobile webviews and browser clients) - * Require appropriate permissions based on operation scope: - - * Course staff or instructor: Can manage enrollments within their courses - * Global staff: Can manage enrollments across all courses - * Self-enrollment: Learners can enroll/unenroll themselves (future consideration) - - * Follow separation of filtering and authorization (explicit filtering in URLs) - -#. **Error Handling** - - * Follow Open edX REST API Conventions error format - * Include ``error_code`` (machine-readable), ``developer_message``, - ``user_message`` (internationalized), and ``status_code`` - * Support ``field_errors`` object for field-specific validation errors - * Use appropriate HTTP status codes: 200, 202, 400, 401, 403, 404 - -#. **Date/Time Serialization** - - * Serialize all dates and timestamps to ISO 8601 format with explicit timezone offsets - * Prefer UTC timestamps - * Example format: ``2024-01-15T10:30:00Z`` - -#. **Email Notifications** - - * Support optional email notifications via ``email_students`` parameter - * Use different message types based on user state: - - * ``enrolled_enroll``: User already registered, being enrolled - * ``allowed_enroll``: User not yet registered, pre-enrollment created - * ``enrolled_unenroll``: User being unenrolled - * ``allowed_unenroll``: Pre-enrollment being removed - - * Support optional ``reason`` parameter included in notification emails - -#. **OpenAPI Specification** - - Maintain an OpenAPI specification at ``../references/enrollment-v2-api-spec.yaml`` - to guide implementation. This static specification serves as a reference during development, - but ``/api-docs/`` is the source of truth for what is actually deployed. Once implementation - is complete and the endpoints are live in ``/api-docs/``, the static spec file will be - deleted to avoid maintaining outdated documentation. - -Consequences -============ - -Positive -~~~~~~~~ - -* Consistent URL patterns following Open edX conventions make the API predictable -* Explicit sync/async behavior based on operation scope allows proper UI feedback -* Pagination support efficiently handles courses with thousands of enrollments -* Optional fields optimize performance by avoiding unnecessary database queries -* OpenAPI specification enables automated validation, testing, and type-safe client generation -* Resource-oriented design makes it easy to add new operations -* Support for both enrollments and pre-enrollments handles all use cases -* Before/after state tracking provides clear audit trail of changes -* Email notification support maintains current functionality for learner communication - -Negative -~~~~~~~~ - -* Existing clients using legacy enrollment endpoints need to be updated -* Dual maintenance during transition period -* Developers familiar with legacy endpoints need to learn new patterns -* Optional fields via ``requested_fields`` add complexity to serialization logic -* Async operations require additional task monitoring implementation - -Alternatives Considered -======================= - -#. **Separate Endpoints for Enroll/Unenroll** - - Considered ``POST /enrollments`` for enroll and ``POST /unenrollments`` for unenroll, - but using ``DELETE /enrollments/{id}`` is more RESTful and follows HTTP verb semantics. - -#. **Nested Pagination Format** - - Considered nesting pagination metadata under a ``pagination`` key (per Cliff Dyer's - proposal), but chose DRF standard flat format (``next``, ``previous``, ``count``, - ``num_pages``, ``results`` at top level) as it's the established convention - documented in Open edX REST API Conventions. - -#. **Expand Parameter Instead of requested_fields** - - Considered using ``expand`` parameter for related objects, but ``requested_fields`` - is more appropriate for optional fields that are not separate resources. Using - ``expand`` would imply these are related resources with their own endpoints, - which is not the case for beta tester status or profile images in this context. - -References -========== - -* OpenAPI Specification: ``../references/enrollment-v2-api-spec.yaml`` -* Live API Documentation: ``/api-docs/`` -* Existing v1 Enrollment API: ``https://master.openedx.io/api-docs/#/enrollment`` -* Legacy Implementation: ``lms/djangoapps/instructor/enrollment.py`` -* `Open edX REST API Conventions `_ -* Optional Fields and API Versioning: https://openedx.atlassian.net/wiki/spaces/AC/pages/40862782/Optional+Fields+and+API+Versioning