docs: add adr and schema for enrollment api (#37846)

This commit is contained in:
wgu-jesse-stewart
2026-01-29 18:36:31 -05:00
committed by GitHub
parent 3df2ba4592
commit 6cb2ea3cf1
2 changed files with 786 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
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 <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`

View File

@@ -0,0 +1,622 @@
swagger: '2.0'
info:
title: Enrollment API v2
version: 2.0.0
description: |
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
- 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
**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
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:
read: Read access to enrollment data
write: Write access to manage enrollments
SessionAuth:
type: apiKey
in: header
name: Cookie
description: Session-based authentication using Django session cookies
security:
- OAuth2: [read, write]
- SessionAuth: []
tags:
- name: Enrollments
description: Course enrollment operations
paths:
# ==================== ENROLLMENT ENDPOINTS ====================
/api/enrollment/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/enrollment/v2/courses/course-v1:edX+DemoX+Demo_Course/enrollments?page=2"
previous: null
results:
- username: "bjohnson"
email: "bela.j@example.com"
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"
full_name: "Cyrus Patel"
mode: "audit"
is_active: true
created: "2024-01-16T14:22:00Z"
beta_tester: false
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/enrollment/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/enrollment/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"
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:
$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 the required 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/enrollment/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
- full_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"
full_name:
type: string
description: Learner's full name
example: "John 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/enrollment/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