Skip to content

feat: call filters for learner block binds and course API responses - #39269

Draft
asadali145 wants to merge 7 commits into
openedx:masterfrom
mitodl:asad/xblock-handler-started
Draft

asadali145 wants to merge 7 commits into
openedx:masterfrom
mitodl:asad/xblock-handler-started

Conversation

@asadali145

@asadali145 asadali145 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

Calls nine new openedx-filters so plugins can adjust how course content is presented to a learner on each request. Render-time filters alone do not reach handler requests, sequence metadata, course pages or the cached course structure and outline, so changes made there are lost on the next interaction.

User roles affected: Developer / Operator (new extension points). No behavior change unless a pipeline step is configured.

Changes:

  • lms/djangoapps/courseware/block_render.py — get_block_for_descriptor returns the block through LearnerXBlockBound.run_filter(block, user, course_key) after binding and the access check. This covers unit children, library and conditional children, XBlock handler calls, sequence metadata, static tabs, course info and about sections. It runs once each time a block is bound to a user, including anonymous users, and again when the same instance is bound to a different user. It does not run for unknown users (user.known is False): block structure collection and no-auth callbacks.
  • lms/djangoapps/course_blocks/api.py — get_course_blocks passes the transformed structure through CourseBlocksTransformCompleted (per-request copy; feeds outline, sidebar, blocks API, dates, progress).
  • openedx/core/djangoapps/content/learning_sequences/views.py — CourseOutlineAPIRenderStarted on the serialized outline (breadcrumbs, next up). The cached outline objects are not passed.
  • lms/djangoapps/course_home_api/course_metadata/views.py — CourseHomeMetadataAPIRenderStarted on the serialized response (course title, tabs).
  • openedx/core/djangoapps/courseware_api/views.py — CoursewareInformation.retrieve runs CoursewareMetadataAPIRenderStarted on the serialized response.
  • lms/djangoapps/course_home_api/{progress,dates,outline}/views.py — ProgressTabView, DatesTabView, OutlineTabView and CourseNavigationBlocksView pass their serialized responses through CourseHomeProgressAPIRenderStarted (with the learner whose progress is shown), CourseHomeDatesAPIRenderStarted, CourseHomeOutlineAPIRenderStarted and CourseNavigationAPIRenderStarted. The navigation filter runs after the sidebar cache, so the cached blocks stay as built.
  • xmodule/modulestore — new fresh_block_reads(course_key) context manager. It is a no-op by default and delegated by the mixed store and the CCX wrapper. In split it sets aside the active bulk operation's cached block instances and the request's runtimes, then restores them. BlockStructureManager._update_collected and CourseOverview.load_from_module_store use it, so data shared by every user is always built from fresh, unbound block instances. Before this, a cache miss in a request that had already bound blocks could collect those (possibly changed) instances, or load children through a runtime prepared for that user. user.known is now restored in a finally after collection.
  • lms/djangoapps/course_home_api/outline/views.py — the navigation sidebar cache key now includes the active request language. The sidebar caches the output of CourseBlocksTransformCompleted for an hour (shared by anonymous users), and block structure filter steps can vary block data by the request's active language.

Supporting information

Testing instructions

  1. Install the openedx-filters branch: pip install git+https://github.com/mitodl/openedx-filters@asad/xblock-handler-started.
  2. Run the affected tests:
    pytest lms/djangoapps/course_home_api/outline/tests \
      openedx/core/djangoapps/content/block_structure/tests \
      openedx/core/djangoapps/content/course_overviews/tests \
      lms/djangoapps/ccx/tests/test_ccx_modulestore.py \
      openedx/core/djangoapps/content/learning_sequences/tests/test_views.py \
      lms/djangoapps/course_home_api/course_metadata/tests \
      openedx/core/djangoapps/courseware_api/tests \
      lms/djangoapps/course_blocks/tests/test_api.py \
      lms/djangoapps/courseware/tests/test_block_render.py
    
    (Locally: 1249 passed across block_structure, course_overviews, course_blocks, ccx, courseware block_render, course_home_api, learning_sequences, courseware_api, course_api blocks, split/mixed modulestore and grades tests. The blocks API query-count tests only change when a filter pipeline is configured.)
  3. End to end: register a pipeline step for each filter type in OPEN_EDX_FILTERS_CONFIG, for example:
    • org.openedx.learning.xblock.bound.v1: prefix block.display_name in memory;
    • org.openedx.learning.course_blocks.transform.completed.v1: block_structure.override_xblock_field(key, "display_name", ...);
    • the three API filters: change data["title"] / data["name"].
      Then load a unit, submit a problem, and open the course outline, sidebar and breadcrumbs in the learning MFE, and check the changed values show everywhere.

Deadline

None.

Other information

  • Depends on the openedx-filters PR above; requirements will need the new openedx-filters version.
  • LearnerXBlockBound runs on every block bind. With no pipeline configured, run_filter returns immediately; steps should return early when they have nothing to do.
  • Open to narrower call sites instead of the single bind filter if reviewers prefer.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Oct 21, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @asadali145!

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

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 Oct 21, 2026
@asadali145
asadali145 force-pushed the asad/xblock-handler-started branch from bc9f157 to da63b4a Compare October 21, 2026 10:16
@asadali145 asadali145 changed the title feat: run learner content translation filters feat: call filters for learner block binds and course API responses Oct 21, 2026
@asadali145
asadali145 requested a balanced review from Copilot October 21, 2026 10:24
@asadali145
asadali145 force-pushed the asad/xblock-handler-started branch from a5c06c6 to da63b4a Compare October 21, 2026 10:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Dependency compatibility, shared-cache isolation, and repeated block-filter mutations remain unresolved.

3 open issues
What changed in this PR

Adds plugin extension points for learner-specific course presentation beyond render-time filtering.

Changes:

  • Adds filters for bound XBlocks and transformed course structures.
  • Filters serialized outline and metadata API responses.
  • Separates navigation sidebar cache entries by active language.
File Description
openedx/​core/​djangoapps/​courseware_api/​views.py Filters serialized courseware metadata.
openedx/​core/​djangoapps/​content/​learning_sequences/​views.py Filters serialized learner outlines.
lms/​djangoapps/​courseware/​block_render.py Filters blocks after binding and access handling.
lms/​djangoapps/​course_home_api/​outline/​views.py Adds language to sidebar cache keys.
lms/​djangoapps/​course_home_api/​course_metadata/​views.py Filters course-home metadata responses.
lms/​djangoapps/​course_blocks/​api.py Filters transformed course structures.

Review effort: Balanced

Comment thread lms/djangoapps/course_blocks/api.py
Comment thread lms/djangoapps/courseware/block_render.py Outdated
Comment thread lms/djangoapps/courseware/block_render.py Outdated
Run new openedx-filters hooks so plugins can adjust how course content is
presented to a learner on each request:

- LearnerXBlockBound at the end of get_block_for_descriptor, after the
  block is bound and its access checked (renders, children, handlers,
  sequence metadata, course pages).
- CourseBlocksTransformCompleted on the transformed block structure in
  get_course_blocks (a per-request copy).
- CourseOutlineAPIRenderStarted, CourseHomeMetadataAPIRenderStarted and
  CoursewareMetadataAPIRenderStarted on the serialized API responses.

The navigation sidebar cache key now includes the active request
language, since block structure filter steps can vary block data by it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@asadali145
asadali145 force-pushed the asad/xblock-handler-started branch from da63b4a to 5365d88 Compare October 21, 2026 10:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread lms/djangoapps/courseware/block_render.py
Comment thread lms/djangoapps/courseware/block_render.py Outdated
Comment thread lms/djangoapps/courseware/block_render.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Collection-time bindings can prevent customization on subsequent learner-facing lookups of the same block.

3 open issues
1 resolved since last review

Review effort: Balanced

Comment thread lms/djangoapps/courseware/block_render.py Outdated
… first bound while unknown

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The binding guard incorrectly skips first-time filtering for anonymous page requests.

3 open issues
1 resolved since last review

Review effort: Balanced

Comment thread lms/djangoapps/courseware/block_render.py Outdated
Block structure collection and course overviews are shared by every user, but
they could read block instances that the current request had already loaded
and bound to a user (and that LearnerXBlockBound steps may have changed in
memory), or load children through a runtime prepared for that user.

- Add a fresh_block_reads(course_key) modulestore context manager: a no-op by
  default, delegated by the mixed and CCX stores, and in split it sets aside
  the bulk operation's cached blocks and the request's runtimes.
- Use it in BlockStructureManager._update_collected and
  CourseOverview.load_from_module_store.
- Restore user.known in a finally block after collection.
- Run LearnerXBlockBound for anonymous users too (their id is None).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Cross-cutting runtime and shared-cache isolation needs maintainer validation, with configured-filter and language-cache regression coverage still missing.

6 open issues
2 resolved since last review

Review effort: Balanced

Comment thread lms/djangoapps/course_blocks/api.py
Comment thread lms/djangoapps/course_home_api/course_metadata/views.py
Comment thread lms/djangoapps/course_home_api/outline/views.py
Comment thread openedx/core/djangoapps/content/learning_sequences/views.py
Comment thread openedx/core/djangoapps/courseware_api/views.py
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@asadali145
asadali145 requested a balanced review from Copilot October 21, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread xmodule/modulestore/__init__.py Outdated
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

…lters

Each view passes its serialized response through its new filter. The navigation
sidebar runs it after the cache, so the cached blocks stay as built.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The progress filter receives a shared mutable colors list, allowing in-place customization to affect later learners’ responses.

1 open issue

Review effort: Balanced

Comment thread lms/djangoapps/course_home_api/progress/views.py
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Cross-cutting binding and shared-cache changes warrant maintainer validation with the companion filters release and updated dependencies.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

This branch has not been deployed

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

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Needs Triage

Development

Successfully merging this pull request may close these issues.

3 participants