feat: course optimizer extended report - #426
Conversation
Studio proxies both kicking off a Course Optimizer extended-analysis run (CourseAnalysisReportView, generating the course export server-side via the existing create_export_tarball) and polling its status (CourseAnalysisReportStatusView) to the xpert-ai-workflows backend, keyed by a shared secret -- the browser only ever talks to Studio.
…lows Reached via Docker's host-gateway alias, since it runs on the host machine rather than in the devstack docker network.
Gates the Course Optimizer extended-report section in the authoring MFE, matching enable_course_optimizer's existing CourseWaffleFlag pattern. Without this, the frontend's default for this flag would apply unconditionally everywhere, since it's absent from every course waffle flags API response until a real flag exists to answer for it.
There was a problem hiding this comment.
Pull request overview
Adds Studio-side support for the Course Re-Run Assistant (CORA) / Course Optimizer “extended report” by introducing a feature flag, new API endpoints that proxy to the xpert-ai-workflows backend, and the required configuration/settings.
Changes:
- Adds a course-scoped waffle flag
contentstore.enable_course_optimizer_extended_reportand exposes it via the course waffle flags API. - Introduces two new v0 REST endpoints to start an extended analysis run and to fetch the latest run status (server-to-server proxy to
xpert-ai-workflows). - Adds CMS settings for the backend base URL, API key, and request timeout (plus devstack defaults) and adds unit tests for the new endpoints.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
cms/envs/devstack.py |
Adds devstack defaults for the xpert-ai-workflows URL and shared secret. |
cms/envs/common.py |
Defines documented CMS settings for workflow URL/API key/timeout. |
cms/djangoapps/contentstore/toggles.py |
Adds the new course-level waffle flag and helper function. |
cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py |
Updates expected waffle-flag response to include the new flag. |
cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py |
Exposes the new flag via the serializer method field. |
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py |
Implements the two new proxy endpoints for extended report run + status. |
cms/djangoapps/contentstore/rest_api/v0/views/__init__.py |
Re-exports new views for v0 API. |
cms/djangoapps/contentstore/rest_api/v0/urls.py |
Registers the new v0 routes. |
cms/djangoapps/contentstore/rest_api/v0/tests/test_course_optimizer.py |
Adds unit tests covering authz and proxy behavior for the new endpoints. |
Suppressed comments (2)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:540
_COURSE_ANALYSIS_REPORT_REQUEST_TIMEOUT_SECONDSis not defined here, so this endpoint will raise aNameErrorbefore making the request. Use the existingCOURSE_ANALYSIS_WORKFLOW_REQUEST_TIMEOUT_SECONDSsetting (as the POST endpoint does) or define the constant.
response = requests.get(
f'{settings.COURSE_ANALYSIS_WORKFLOW_URL}/courses/{course_id}/runs/latest',
headers={'X-Api-Key': settings.COURSE_ANALYSIS_WORKFLOW_API_KEY},
timeout=_COURSE_ANALYSIS_REPORT_REQUEST_TIMEOUT_SECONDS,
)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:536
- Same as the POST endpoint: the status proxy should also be gated by
contentstore.enable_course_optimizer_extended_reportso the extended-report feature is truly off by default at the API layer.
course_key = CourseKey.from_string(course_id)
if not has_course_author_access(request.user, course_key):
self.permission_denied(request)
try:
response = requests.get(
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
CourseAnalysisReportStatusView.get parsed course_id without @verify_course_exists(), so an invalid or unknown course key produced a 500 instead of the documented 404. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CourseAnalysisReportView.post and CourseAnalysisReportStatusView.get triggered/proxied analysis runs regardless of the enable_course_optimizer_extended_report flag state, so the backend call happened even when the flag was off. Also fixes a NameError in the status view from a leftover reference to an undefined _COURSE_ANALYSIS_REPORT_REQUEST_TIMEOUT_SECONDS constant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (4)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:556
- If COURSE_ANALYSIS_WORKFLOW_URL / COURSE_ANALYSIS_WORKFLOW_API_KEY are unset, this will attempt a request to an invalid URL and return 502. It’s better to fail fast with a clear configuration error (and avoid an unnecessary network attempt).
response = requests.get(
f'{settings.COURSE_ANALYSIS_WORKFLOW_URL}/courses/{course_id}/runs/latest',
headers={'X-Api-Key': settings.COURSE_ANALYSIS_WORKFLOW_API_KEY},
timeout=settings.COURSE_ANALYSIS_WORKFLOW_REQUEST_TIMEOUT_SECONDS,
)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:560
- As with the POST view, response.json() can raise ValueError if the backend returns non-JSON (or no body), causing a 500 in Studio.
return Response(response.json(), status=response.status_code)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:485
- This generates a full course export before confirming the analysis backend is configured. If COURSE_ANALYSIS_WORKFLOW_URL / COURSE_ANALYSIS_WORKFLOW_API_KEY are unset (defaults are empty in cms/envs/common.py), Studio will do expensive work only to fail with a 502 due to an invalid URL.
This issue also appears on line 552 of the same file.
course_block = modulestore().get_course(course_key)
tarball = create_export_tarball(course_block, course_key, {})
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:500
- response.json() will raise ValueError if the backend returns non-JSON (e.g., HTML error page, empty body). That would turn a backend failure into a 500 in Studio instead of cleanly proxying/handling it.
This issue also appears on line 560 of the same file.
return Response(response.json(), status=response.status_code)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (8)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:500
response.json()can raise (e.g., empty body or non-JSON error response) which would crash the view and return an unstructured 500. Guard JSON decoding and return a structured error when the upstream response isn't valid JSON.
return Response(response.json(), status=response.status_code)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:558
- On request failures, this returns a bare 502 with an empty body. Since this view uses
DeveloperErrorViewMixin, it should ideally return the standard structured error payload (developer_message/error_code) to conform to API conventions and aid debugging.
except requests.RequestException:
return Response(status=status.HTTP_502_BAD_GATEWAY)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:560
response.json()can raise (e.g., empty body or non-JSON error response) which would crash the view and return an unstructured 500. Guard JSON decoding and return a structured error when the upstream response isn't valid JSON.
return Response(response.json(), status=response.status_code)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:522
- The schema for this endpoint doesn't document the 400 response that is actually returned when the waffle flag is disabled. This makes the generated API docs inaccurate for clients.
200: "OK",
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The course has no analysis runs yet.",
502: "The Course Optimizer extended-report backend is unreachable.",
cms/djangoapps/contentstore/toggles.py:723
- The PR description references a waffle flag named
contentstore.enable_course_optimizer_extended_report, but the implementation introducescontentstore.enable_course_optimizer_extended_checks. This mismatch will make rollout/debugging harder (operators will look for a different flag name than what the code uses). Align the code and documentation by renaming the toggle (and all references) or updating the PR description/related docs to match the implemented name.
# .. toggle_name: contentstore.enable_course_optimizer_extended_checks
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: When enabled, replaces the Course Optimizer page
# with its extended analysis report (time-on-task, learning balance, and
# LLM-driven accessibility/content-quality/pacing findings) in the
# authoring MFE.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2026-08-13
# .. toggle_target_removal_date: 2027-02-13
ENABLE_COURSE_OPTIMIZER_EXTENDED_CHECKS = CourseWaffleFlag(
f'{CONTENTSTORE_NAMESPACE}.enable_course_optimizer_extended_checks',
__name__,
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:486
create_export_tarball(...)can raise (e.g., serialization/export errors). In this request-thread view, an exception will bypassDeveloperErrorViewMixin.handle_exception(it re-raises unknown exceptions) and turn into an unstructured 500. Consider catching export failures and returning a structured API error response.
This issue also appears on line 557 of the same file.
course_block = modulestore().get_course(course_key)
tarball = create_export_tarball(course_block, course_key, {})
try:
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:496
- On request failures, this returns a bare 502 with an empty body. Since this view uses
DeveloperErrorViewMixin, it should ideally return the standard structured error payload (developer_message/error_code) to conform to API conventions and aid debugging.
This issue also appears in the following locations of the same file:
- line 500
- line 560
except requests.RequestException:
return Response(status=status.HTTP_502_BAD_GATEWAY)
cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py:453
- The schema for this endpoint doesn't document the 400 response that is actually returned when the waffle flag is disabled. This makes the generated API docs inaccurate for clients.
This issue also appears on line 518 of the same file.
202: "Analysis run queued.",
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The requested course does not exist.",
502: "The Course Optimizer extended-report backend is unreachable.",
Splits the new xpert-ai-workflows-driven extended report endpoints
(CourseAnalysisReportView, CourseAnalysisReportStatusView) out to v1,
nested under a course_optimizer/analysis/{course_id} prefix, while
leaving the original link-checking/rerun-link-update endpoints on v0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Suppressed comments (2)
cms/djangoapps/contentstore/rest_api/v1/views/course_optimizer.py:149
- Same issue here:
response.json()can raise if the backend response isn't valid JSON, which will surface as an unhandled 500 from Studio instead of proxying the backend status cleanly.
return Response(response.json(), status=response.status_code)
cms/djangoapps/contentstore/rest_api/v1/views/course_optimizer.py:74
- Generating a full course export tarball inside a request/response cycle can be very slow for large courses and can tie up a Studio web worker for a long time. Consider moving the export+submit flow to an async task (returning 202 with a task/run identifier) so Studio remains responsive under load.
course_block = modulestore().get_course(course_key)
tarball = create_export_tarball(course_block, course_key, {})
…evstack overrides Addresses PR review feedback: response.json() on the extended-report proxy views now returns 502 instead of an unhandled 500 when the backend returns a non-JSON body, and the devstack URL/API key default to ENV_TOKENS overrides like other devstack secrets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
cms/djangoapps/contentstore/rest_api/v1/views/course_optimizer.py:41
- The view returns HTTP 400 when the waffle flag is disabled, but the API schema doesn't document a 400 response. This makes the generated API docs incomplete/misleading for clients.
This issue also appears on line 112 of the same file.
202: "Analysis run queued.",
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The requested course does not exist.",
502: "The Course Optimizer extended-report backend is unreachable.",
cms/djangoapps/contentstore/rest_api/v1/views/course_optimizer.py:116
- The status view can return HTTP 400 when the waffle flag is disabled, but the API schema doesn't document it. Also, this endpoint can return 404 both for "no runs" and for a nonexistent course (via verify_course_exists), so the 404 response description should reflect both cases.
200: "OK",
401: "The requester is not authenticated.",
403: "The requester cannot access the specified course.",
404: "The course has no analysis runs yet.",
502: "The Course Optimizer extended-report backend is unreachable.",
Description
Provides support for the Course Re-Run Assistant / CORA / extended course optimizer report project. Specifically:
contentstore.enable_course_optimizer_extended_checksfor enabling this feature. Off by default.POST /api/contentstore/v1/course_optimizer/analysis/{course_id}to post course material to our xpert-ai-workflows endpoint for analysis.GET /api/contentstore/v1/course_optimizer/analysis/{course_id}/statusto check the status of a given analysis.COURSE_ANALYSIS_WORKFLOW_API_KEYfor authentication to thexpert-ai-workflowsservice.These two routes live under v1 (nested under a
course_optimizer/prefix) rather than v0, keeping the original link-checking/rerun-link-update routes on v0 as-is.Supporting information
Tickets:
Supporting PRs:
Testing instructions
contentstore.enable_course_optimizer_extended_checkswaffle flag.course-optimizer-reportplugin infrontend-app-learningfrom the supporting PR.Deadline
ASAP