Skip to content

feat: Add course team management v2 API for instructor dashboard#38205

Draft
brianjbuck-wgu wants to merge 1 commit intoopenedx:masterfrom
WGU-Open-edX:feature/37559-course-team-api-schema
Draft

feat: Add course team management v2 API for instructor dashboard#38205
brianjbuck-wgu wants to merge 1 commit intoopenedx:masterfrom
WGU-Open-edX:feature/37559-course-team-api-schema

Conversation

@brianjbuck-wgu
Copy link
Copy Markdown
Contributor

@brianjbuck-wgu brianjbuck-wgu commented Mar 23, 2026

Description

Adds v2 REST API for managing course team members (instructors, staff, beta testers, CCX coaches, etc.) on the LMS instructor dashboard. This includes an OpenAPI schema specification, serializers, views, URL routing, and tests.

The schema defines RESTful endpoints that will replace the legacy POST-based course team management endpoints on the LMS instructor dashboard.

Supporting information

Testing instructions

Manual API testing (optional):

  • Authenticate as an instructor for a course
  • GET /api/instructor/v2/courses/{course_id}/team?role=staff — list staff members
  • POST /api/instructor/v2/courses/{course_id}/team with {"identifier": "username", "role": "staff"} — grant role
  • DELETE /api/instructor/v2/courses/{course_id}/team/username?role=staff — revoke role
  • POST /api/instructor/v2/courses/{course_id}/team/beta-testers with {"identifiers": ["user1", "user2"]} — bulk add
  • DELETE /api/instructor/v2/courses/{course_id}/team/beta-testers with {"identifiers": ["user1"]} — bulk remove

Deadline

None

Other information

  • No database migrations, no runtime changes, no deprecations.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Mar 23, 2026
@openedx-webhooks
Copy link
Copy Markdown

Thanks for the pull request, @brianjbuck-wgu!

This repository is currently maintained by @openedx/wg-maintenance-openedx-platform.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

🔘 Update the status of your PR

Your PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate.


Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@github-project-automation github-project-automation bot moved this to Needs Triage in Contributions Mar 23, 2026
@mphilbrick211 mphilbrick211 added the mao-onboarding Reviewing this will help onboard devs from an Axim mission-aligned organization (MAO). label Mar 27, 2026
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Mar 27, 2026
@brianjbuck-wgu brianjbuck-wgu force-pushed the feature/37559-course-team-api-schema branch from 20c7491 to 1bc035e Compare April 8, 2026 22:12
@brianjbuck-wgu brianjbuck-wgu changed the title feat: Add schema file for instructor dashboard v2 API feat: Add course team management v2 API for instructor dashboard Apr 8, 2026
@brianjbuck-wgu brianjbuck-wgu requested a review from Copilot April 8, 2026 22:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Swagger 2.0 (OpenAPI) specification documenting an Instructor Dashboard “course team management” v2 REST API (list/grant/revoke roles + bulk beta tester operations), intended to replace legacy POST-based instructor endpoints.

Changes:

  • Introduces a new OpenAPI spec file for course team role management v2 endpoints.
  • Documents single-user role grant/revoke operations and bulk beta tester add/remove operations.
  • Defines shared request/response schemas (including a standardized error model).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1878 to +1908
for identifier in identifiers:
try:
error = False
user_does_not_exist = False
user = get_student_from_identifier(identifier)
user_active = user.is_active

if action == 'add':
allow_access(course, user, 'beta')
else:
revoke_access(course, user, 'beta')
except User.DoesNotExist:
error = True
user_does_not_exist = True
user_active = None
except Exception: # pylint: disable=broad-except
log.exception("Error while %sing beta tester %s", action, identifier)
error = True
else:
if email_students:
send_beta_role_email(action, user, email_params)
if auto_enroll and action == 'add':
if not is_user_enrolled_in_course(user, course_key):
CourseEnrollment.enroll(user, course_key)
finally:
results.append({
'identifier': identifier,
'error': error,
'user_does_not_exist': user_does_not_exist,
'is_active': user_active, # pylint: disable=used-before-assignment
})
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _process_beta_testers, user_active is only set after get_student_from_identifier succeeds. If any unexpected exception is raised before user_active is assigned (e.g., non-DoesNotExist from user lookup), the finally block will raise UnboundLocalError when building the result item. Initialize user_active (and ideally user_does_not_exist) to safe defaults before the try, and set user_active = None in the broad except as well.

Copilot uses AI. Check for mistakes.
Comment on lines +396 to +437
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
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenAPI spec defines a structured error object (error_code, developer_message, user_message, status_code, etc.) and uses it in 400/401/403/404/409 responses, but the actual implementation in api_v2.py returns simpler payloads like {'error': '...'} (and sometimes raw serializer.errors). Please either update the implementation to match this error schema or adjust the spec examples/definitions to reflect the real response shape so clients can rely on it.

Suggested change
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
description: Bad request - Invalid parameters or malformed request. The current implementation
returns either a simple `{"error": "..."}` payload or a serializer validation error object.
schema:
type: object
description: Error response returned by the current implementation.
properties:
error:
type: string
description: Human-readable error message when the endpoint returns a simple error payload.
additionalProperties: true
examples:
application/json:
error: "Invalid course key format"
Unauthorized:
description: Unauthorized - Authentication required
schema:
type: object
description: Error response returned by the current implementation.
properties:
error:
type: string
description: Human-readable error message when the endpoint returns a simple error payload.
additionalProperties: true
examples:
application/json:
error: "Authentication credentials were not provided."
Forbidden:
description: Forbidden - Insufficient permissions
schema:
type: object
description: Error response returned by the current implementation.
properties:
error:
type: string
description: Human-readable error message when the endpoint returns a simple error payload.
additionalProperties: true
examples:
application/json:
error: "You do not have permission to perform this action"
NotFound:
description: Not found - Resource does not exist
schema:
type: object
description: Error response returned by the current implementation.
properties:
error:
type: string
description: Human-readable error message when the endpoint returns a simple error payload.
additionalProperties: true
examples:
application/json:
error: "The requested item could not be found"

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +244
$ref: '#/definitions/Error'
examples:
application/json:
error_code: "SELF_REMOVAL_NOT_ALLOWED"
developer_message: "Instructors cannot remove their own instructor access"
user_message: "You cannot remove your own instructor access"
status_code: 409
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 409 response example is documented as a structured error with error_code/developer_message/user_message, but CourseTeamMemberView.delete currently returns {'error': _('Instructors cannot remove their own instructor access.')}. Align the spec and implementation so the documented error format matches what clients receive.

Suggested change
$ref: '#/definitions/Error'
examples:
application/json:
error_code: "SELF_REMOVAL_NOT_ALLOWED"
developer_message: "Instructors cannot remove their own instructor access"
user_message: "You cannot remove your own instructor access"
status_code: 409
type: object
required:
- error
properties:
error:
type: string
description: Error message explaining why the instructor role cannot be revoked
examples:
application/json:
error: "Instructors cannot remove their own instructor access."

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +27
from common.djangoapps.student.roles import (
CourseBetaTesterRole,
CourseCcxCoachRole,
CourseDataResearcherRole,
CourseInstructorRole,
CourseStaffRole,
)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CourseCcxCoachRole is imported here but not used anywhere in this test module. Please remove it to avoid unused-import lint failures.

Copilot uses AI. Check for mistakes.
Comment on lines +1668 to +1670
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.EDIT_COURSE_ACCESS

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These v2 endpoints use permissions.InstructorPermission, whose has_permission currently does CourseKey.from_string(view.kwargs['course_id']) without handling InvalidKeyError (see lms/djangoapps/instructor/permissions.py:86). If a malformed course_id matches the (fairly permissive) COURSE_ID_PATTERN, the permission check can raise and return a 500 before the view method runs. Please ensure invalid course keys are handled gracefully (e.g., catch InvalidKeyError/Http404 inside InstructorPermission and deny with a 404/400) so clients get a predictable response.

Copilot uses AI. Check for mistakes.
Comment on lines +1835 to +1847
**Response Values** (same for both POST and DELETE)

{
"action": "add",
"results": [
{
"identifier": "beta_user1",
"error": false,
"user_does_not_exist": false,
"is_active": true
}
]
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says the response values are the same for POST and DELETE, but the example hard-codes "action": "add". Since DELETE returns action: "remove", consider updating the example (or add a second example) to avoid confusing API consumers.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mao-onboarding Reviewing this will help onboard devs from an Axim mission-aligned organization (MAO). open-source-contribution PR author is not from Axim or 2U

Projects

Status: Waiting on Author

Development

Successfully merging this pull request may close these issues.

Instructor Dashboard - Course Team Management API - GET - List by Rolename - SCHEMA

4 participants