feat: Add course team management v2 API for instructor dashboard#38205
feat: Add course team management v2 API for instructor dashboard#38205brianjbuck-wgu wants to merge 1 commit intoopenedx:masterfrom
Conversation
|
Thanks for the pull request, @brianjbuck-wgu! This repository is currently maintained by 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 approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo 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:
🔘 Get a green buildIf 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 PRYour 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:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
20c7491 to
1bc035e
Compare
There was a problem hiding this comment.
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.
lms/djangoapps/instructor/docs/references/instructor-v2-course-team-api-spec.yaml
Show resolved
Hide resolved
lms/djangoapps/instructor/docs/references/instructor-v2-course-team-api-spec.yaml
Outdated
Show resolved
Hide resolved
lms/djangoapps/instructor/docs/references/instructor-v2-course-team-api-spec.yaml
Outdated
Show resolved
Hide resolved
lms/djangoapps/instructor/docs/references/instructor-v2-course-team-api-spec.yaml
Outdated
Show resolved
Hide resolved
lms/djangoapps/instructor/docs/references/instructor-v2-course-team-api-spec.yaml
Outdated
Show resolved
Hide resolved
lms/djangoapps/instructor/docs/references/instructor-v2-course-team-api-spec.yaml
Outdated
Show resolved
Hide resolved
lms/djangoapps/instructor/docs/references/instructor-v2-course-team-api-spec.yaml
Outdated
Show resolved
Hide resolved
1bc035e to
accd3bf
Compare
There was a problem hiding this comment.
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.
| 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 | ||
| }) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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" |
| $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 |
There was a problem hiding this comment.
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.
| $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." |
| from common.djangoapps.student.roles import ( | ||
| CourseBetaTesterRole, | ||
| CourseCcxCoachRole, | ||
| CourseDataResearcherRole, | ||
| CourseInstructorRole, | ||
| CourseStaffRole, | ||
| ) |
There was a problem hiding this comment.
CourseCcxCoachRole is imported here but not used anywhere in this test module. Please remove it to avoid unused-import lint failures.
| permission_classes = (IsAuthenticated, permissions.InstructorPermission) | ||
| permission_name = permissions.EDIT_COURSE_ACCESS | ||
|
|
There was a problem hiding this comment.
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.
| **Response Values** (same for both POST and DELETE) | ||
|
|
||
| { | ||
| "action": "add", | ||
| "results": [ | ||
| { | ||
| "identifier": "beta_user1", | ||
| "error": false, | ||
| "user_does_not_exist": false, | ||
| "is_active": true | ||
| } | ||
| ] | ||
| } |
There was a problem hiding this comment.
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.
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):
Deadline
None
Other information