Skip to content

feat: course optimizer extended report - #426

Open
nsprenkle wants to merge 9 commits into
release-ulmofrom
nsprenkle/course-optimizer-extended-report
Open

feat: course optimizer extended report#426
nsprenkle wants to merge 9 commits into
release-ulmofrom
nsprenkle/course-optimizer-extended-report

Conversation

@nsprenkle

@nsprenkle nsprenkle commented Aug 13, 2026

Copy link
Copy Markdown
Member

Description

Provides support for the Course Re-Run Assistant / CORA / extended course optimizer report project. Specifically:

  1. Creates a waffle flag contentstore.enable_course_optimizer_extended_checks for enabling this feature. Off by default.
  2. Creates 2 new routes:
    1. POST /api/contentstore/v1/course_optimizer/analysis/{course_id} to post course material to our xpert-ai-workflows endpoint for analysis.
    2. GET /api/contentstore/v1/course_optimizer/analysis/{course_id}/status to check the status of a given analysis.
  3. Establishes a shared secret, COURSE_ANALYSIS_WORKFLOW_API_KEY for authentication to the xpert-ai-workflows service.

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

  1. Enable the contentstore.enable_course_optimizer_extended_checks waffle flag.
  2. Install the course-optimizer-report plugin in frontend-app-learning from the supporting PR.
  3. Run xpert-ai-workflows/content/cora from the supporting PR.
  4. Navigate to course optimizer for an enabled course / user (e.g. http://localhost:2001/course/course-v1:edX+DemoX+Demo_Course/optimizer)
  5. Click the new "scan now" button.
  6. Verify that the report completes.

Deadline

ASAP

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.
@nsprenkle
nsprenkle marked this pull request as ready for review August 14, 2026 14:51
Copilot AI lite review requested due to automatic review settings August 14, 2026 14:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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_report and 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_SECONDS is not defined here, so this endpoint will raise a NameError before making the request. Use the existing COURSE_ANALYSIS_WORKFLOW_REQUEST_TIMEOUT_SECONDS setting (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_report so 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.

Comment thread cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py Outdated
Comment thread cms/djangoapps/contentstore/rest_api/v0/views/course_optimizer.py Outdated
nsprenkle and others added 2 commits August 17, 2026 09:16
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>
Copilot AI review requested due to automatic review settings August 17, 2026 13:17

Copilot AI left a comment

Copy link
Copy Markdown

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 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)

Copilot AI review requested due to automatic review settings August 17, 2026 21:18

Copilot AI left a comment

Copy link
Copy Markdown

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 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 introduces contentstore.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 bypass DeveloperErrorViewMixin.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>
Copilot AI review requested due to automatic review settings August 27, 2026 16:12

Copilot AI left a comment

Copy link
Copy Markdown

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 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, {})

Comment thread cms/envs/devstack.py Outdated
Comment thread cms/djangoapps/contentstore/toggles.py
Comment thread cms/djangoapps/contentstore/rest_api/v1/urls.py
Comment thread cms/djangoapps/contentstore/rest_api/v1/views/course_optimizer.py
Comment thread cms/djangoapps/contentstore/rest_api/v1/views/course_optimizer.py Outdated
…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>
Copilot AI review requested due to automatic review settings August 27, 2026 19:05

Copilot AI left a comment

Copy link
Copy Markdown

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 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.",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants