Add an XBlock Aside#
Add an XBlock Aside to attach behavior, UI, or stored data to one or more existing XBlock types without modifying those XBlocks. Use this recipe when you want a single, installable Python package that decorates the views of XBlocks in your platform.
For background on what asides are and when to use them, read About XBlock Asides. For a complete API reference, see XBlock Asides Reference.
Prerequisites#
Before you start, make sure you have:
A working Open edX development environment in which you can install a Python package and restart the LMS and Studio services. Tutor devstack is the recommended environment.
The installed Python version used by the target Open edX release.
Familiarity with writing a basic XBlock view that returns a
Fragment. If you have never written one, complete Quickstart: Build Your First XBlock Aside first.
This recipe builds a feedback-badge Aside that adds a “Report an issue” link to Problem and Video blocks, with a course-author setting to enable or disable it per block. Substitute your own block types and behavior as needed.
Step 1: Scaffold a Python package#
Create a new directory for the Aside package, with the layout below:
feedback_badge_aside/
├── pyproject.toml
├── feedback_badge_aside/
│ ├── __init__.py
│ ├── Aside.py
│ └── static/
│ ├── html/
│ │ └── studio_view.html
│ ├── css/
│ │ └── studio.css
│ └── js/
│ └── studio.js
└── README.rst
The package name (feedback_badge_aside) and the module name
(Aside.py) are conventions; pick names that describe your Aside.
The static/html and static/js directories hold the template and
script for the author-facing toggle built in later steps — this
mirrors how production asides such as ol-openedx-chat lay out their
static assets.
Populate pyproject.toml with the package metadata and a placeholder
for the entry point you will add in Step 8.
[project]
name = "feedback-badge-Aside"
version = "0.1.0"
description = "An XBlock Aside that adds a feedback link to Problem and Video blocks."
requires-python = ">=X.Y" # set to the minimum Python version for the target Open edX release
dependencies = [
"XBlock",
"web-fragments",
]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
Step 2: Define the Aside class#
In feedback_badge_aside/Aside.py, define a subclass of
XBlockAside.
from xblock.core import XBlockAside
class FeedbackBadgeAside(XBlockAside):
"""Adds a feedback link to learner-facing views of supported blocks."""
The class name does not need to match the entry point name, but keeping them consistent makes debugging easier.
Step 4: Decorate the views you want to inject into#
Add one method per XBlock view you want to decorate, using
aside_for(). The method takes self,
the host block, and an optional context dictionary, and returns
a Fragment.
The learner-facing view is straightforward — a link, gated on the field:
from web_fragments.fragment import Fragment
from xblock.core import XBlockAside
from xblock.fields import Boolean, Scope
class FeedbackBadgeAside(XBlockAside):
"""Adds a feedback link to learner-facing views of supported blocks."""
is_feedback_badge_enabled = Boolean(
display_name="Show feedback link",
default=True,
scope=Scope.settings,
help="Whether to show a 'Report an issue' link on this block.",
)
@XBlockAside.aside_for("student_view")
def student_view_aside(self, block, context=None):
"""Render the feedback link for the learner view."""
if not self.is_feedback_badge_enabled:
return Fragment("")
block_id = block.scope_ids.usage_id.block_id
html = (
f'<a class="feedback-badge" '
f'href="/feedback?block={block_id}">Report an issue</a>'
)
return Fragment(html)
The author-facing toggle is where it’s worth taking more care. Decorate
author_view, not studio_view. This isn’t a style preference:
Studio’s own aside-rendering machinery only splices an Aside’s fragment
into the views it treats as preview views — student_view,
public_view, and author_view — and studio_view isn’t one of
them, so an Aside decorating studio_view never actually appears in
Studio’s unit editor. author_view is also what production asides
such as ol-openedx-chat use for exactly this purpose.
Add a small helper for loading the package’s static files, and use it to render the checkbox from a template instead of building HTML inline:
import pkg_resources
def resource_string(path):
"""Load a static resource from this package as a decoded string."""
return pkg_resources.resource_string(__name__, path).decode("utf8")
class FeedbackBadgeAside(XBlockAside):
# ... fields and student_view_aside from above ...
@XBlockAside.aside_for("author_view")
def author_view_aside(self, block, context=None):
"""Render the author-facing toggle in Studio's unit preview."""
html = resource_string("static/html/studio_view.html").format(
checked="checked" if self.is_feedback_badge_enabled else "",
)
fragment = Fragment(html)
fragment.add_css(resource_string("static/css/studio.css"))
fragment.add_javascript(resource_string("static/js/studio.js"))
fragment.initialize_js("FeedbackBadgeStudioInit")
return fragment
static/html/studio_view.html — note the {checked} placeholder,
filled in by the .format() call in author_view_aside above, so
this is shown as plain text rather than as HTML:
<label class="feedback-badge-toggle-label">
<input type="checkbox" {checked} class="feedback-badge-toggle">
Show feedback link
</label>
Note what this method does not do: it doesn’t pass the checkbox state
through initialize_js’s json_args parameter. That parameter
exists and is useful — student_view_aside could use it to hand
learner-facing JavaScript some initial state — but author_view_aside
here gets its state a different way, by rendering it directly into the
HTML template’s checked attribute. Keep the two mechanisms distinct
in your own Asides: use template context for what the initial markup
should look like, and json_args for values your JavaScript needs
after the page has already loaded.
The checkbox doesn’t do anything yet — clicking it doesn’t persist the change. That’s Step 5.
Step 5: Add a handler to persist the toggle#
Add an AJAX handler, using the standard @XBlock.handler decorator,
that reads the posted value and saves it to the field:
import json
from webob import Response
from xblock.core import XBlock
class FeedbackBadgeAside(XBlockAside):
# ... fields and view methods from above ...
@XBlock.handler
def update_config(self, request, suffix=""):
"""Persist the course author's toggle setting."""
data = json.loads(request.body)
self.is_feedback_badge_enabled = bool(data.get("is_enabled", True))
return Response(json_body={"is_enabled": self.is_feedback_badge_enabled})
This is a normal XBlock handler — an Aside’s handlers work exactly like an XBlock’s, and the runtime generates a handler URL for the Aside the same way it does for the host block.
Step 6: Add the JavaScript that wires up the checkbox#
The checkbox needs client-side code to listen for changes and call the
handler. static/js/studio.js:
function FeedbackBadgeStudioInit(runtime, element) {
var studioRuntime = new window.StudioRuntime.v1();
var handlerUrl = studioRuntime.handlerUrl(element, "update_config");
var checkbox = element.querySelector(".feedback-badge-toggle");
checkbox.addEventListener("change", function () {
$.ajax({
type: "POST",
url: handlerUrl,
data: JSON.stringify({is_enabled: checkbox.checked}),
}).done(function () {
runtime.notify("save", {state: "end"});
});
});
}
Two things worth noting, both taken from how ol-openedx-chat does this in production:
Use
new window.StudioRuntime.v1()to get a runtime capable of building the handler URL, rather than theruntimeargumentinitialize_jspasses in directly — this is what Studio’s JavaScript environment expects for saving Aside/XBlock data.Don’t add a CSRF header to the request yourself. Studio already attaches one globally for every
$.ajax/$.postcall (cms/static/cms/js/main.jscalls$.ajaxSetupwith the CSRF token once, at page load), and the platform’s own Aside JavaScript (cms/static/js/xblock_asides/structured_tags.js) relies on this without adding its own header. Adding one yourself is redundant at best.
If your package also needs to tell the Authoring MFE that the embedded
unit page’s content changed — for example, because the surrounding UI
needs to react to the save — post a saveEditedXBlockData message
to document.referrer after a successful save, the same message
ol-openedx-chat’s own studio.js sends:
}).done(function () {
runtime.notify("save", {state: "end"});
window.parent.postMessage(
{type: "saveEditedXBlockData"},
document.referrer
);
});
frontend-app-authoring listens for exactly this message type to
know the embedded unit iframe changed. This is a deliberate integration
point that already exists between Studio’s Aside JavaScript and the
Authoring MFE, not incidental boilerplate — include it if your Aside’s
save should be reflected in the surrounding MFE UI. This is also used to
enable the “Publish” button in the Authoring MFE when an Aside’s checkbox
is toggled, otherwise, reloading the page will enable the “Publish” button.
Step 7: Filter to specific block types#
By default, an Aside applies to every block. Override
should_apply_to_block() to restrict the
Aside to the block types you support.
@classmethod
def should_apply_to_block(cls, block):
"""Apply this Aside to Problem and Video blocks only."""
block_type = getattr(block, "category", None)
return block_type in {"problem", "video"}
Add this classmethod to FeedbackBadgeAside. Without it, the Aside
would attempt to render on every block in every course, including blocks
where the markup makes no sense.
For more sophisticated filtering, should_apply_to_block can also
inspect:
block.scope_ids.usage_id.context_keyto gate on a course or library.Platform feature flags such as Waffle flags.
Course-level settings retrieved through a runtime service.
If your filter consults course settings or feature flags, guard against the import and export paths where these may not be available; see About XBlock Asides for the relevant limitations.
Step 8: Register the Aside as an entry point#
In pyproject.toml, add an entry point in the xblock_asides.v1
group. The entry point name on the left side of the equals sign becomes
the Aside’s type name and is used as the XML tag during OLX
serialization.
[project.entry-points."xblock_asides.v1"]
feedback_badge = "feedback_badge_aside.Aside:FeedbackBadgeAside"
Choose a type name that is unlikely to collide with other asides on the same deployment. Treat the name as a stable public identifier; renaming it later breaks OLX round-trips of any course that has used the Aside.
Step 9: Install the package and restart services#
Install the package into the LMS and Studio Python environments. With Tutor:
tutor mounts add lms,cms:/path/to/feedback_badge_aside:/openedx/feedback_badge_aside
tutor dev exec lms bash
pip install -e /openedx/feedback_badge_aside
exit
tutor dev restart lms
tutor dev exec cms bash
pip install -e /openedx/feedback_badge_aside
exit
tutor dev restart cms
Step 10: Enable asides in the LMS and Studio#
The LMS and Studio each gate Aside rendering on their own, separate Django configuration model. Enabling one does not enable the other.
LMS. XBlockAsidesConfig, defined in
lms/djangoapps/lms_xblock/models.py. Until this model has an
enabled revision, no Aside renders in the LMS regardless of
installation or registration. Open the LMS Django admin and create a
new configuration revision:
http://<your-lms-host>/admin/lms_xblock/xblockasidesconfig/
Click Add, check Enabled, and save. The model is
a ConfigurationModel (revision-based), so each save creates a new
revision and the most recent enabled revision is treated as current.
The same form has a Disabled blocks field, a space-separated list of
block types on which asides will never render in the LMS. The
default value is about course_info static_tab. If your Aside should
apply to one of these block types, remove that type from the list.
There is no per-course allowlist and no per-Aside-type allowlist. Once
XBlockAsidesConfig is enabled and your Aside’s host block type is
not in disabled_blocks, the runtime offers your Aside to every
matching block in every course. Per-Aside filtering happens through
your Aside’s own should_apply_to_block classmethod, which you wrote
in Step 7.
Studio. A separate model, StudioConfig, defined in
cms/djangoapps/xblock_config/models.py, gates Aside rendering in
Studio — the LMS model above has no effect there. It has the same
shape (an enabled flag and a disabled_blocks field, same
default) and the same admin workflow, at a different URL:
http://<your-studio-host>/admin/xblock_config/studioconfig/
Both models are ConfigurationModel rows that default to
enabled=False, so out of the box your Aside renders in neither the
LMS nor Studio until you explicitly enable the model for each.
The Authoring MFE doesn’t need a separate switch either: its unit
editor embeds the same Studio unit page this StudioConfig setting
controls, inside an iframe. Once StudioConfig is enabled, the
author_view checkbox from Step 4 appears inside that embedded page
when authors edit a unit.
Step 11: Verify the Aside is rendering#
Open a course that contains a Problem or Video block, view it as a learner, and confirm the feedback link appears at the bottom of the block. To verify the author-side UI, open the same block’s unit page in Studio and confirm the checkbox appears in the author view. Click it, reload the page, and confirm the checkbox keeps its new state — that confirms the handler from Step 5 is actually persisting the value, not just rendering it once.
If the Aside does not appear at all:
Check that the entry point is registered. Run:
from xblock.core import XBlockAside print(list(XBlockAside.load_classes()))
in a Django shell. Your Aside’s type name should be in the list. If it’s missing, check the LMS/Studio logs for a
log.warningabout failing to load your Aside’s entry point — a broken import in your package is dropped silently at this stage, with the Aside simply absent from the list above and no other error anywhere.Check that
should_apply_to_blockreturnsTruefor the block you are testing.
If the Aside’s fragment shows an error instead of your content, that’s
a different situation: an exception was raised inside a view method
(student_view_aside or author_view_aside) or inside
should_apply_to_block itself, after the Aside was already found and
loaded. None of these fail silently, in either environment: an
exception in author_view_aside or in should_apply_to_block
renders as an error block on the unit page in Studio, and an exception
in student_view_aside or in should_apply_to_block renders as an
error block in the Learning MFE. Check the corresponding logs for the
underlying exception either way.
Step 12: Talk to the Learning MFE via postMessage (optional)#
If your Aside needs to reach beyond its own iframe — to trigger
something in the surrounding Learning MFE page — use postMessage.
This step is optional; skip it if your Aside’s UI is self-contained.
For a built-in message type, no extra setup is needed on either side. For example, to open a modal from your Aside’s learner-facing JavaScript:
window.parent.postMessage(
{type: "plugin.modal", payload: {open: true}},
learningMfeBaseUrl
);
Get learningMfeBaseUrl from the server side and pass it into your
fragment’s JavaScript through initialize_js’s json_args, rather
than guessing at a URL client-side — a mismatched target origin drops
the message with no console error on either end. ol-openedx-chat
sources this value from settings.LEARNING_MICROFRONTEND_URL.
For a custom message type, you’ll also need a listener registered
through the Learning MFE’s own env.config.jsx. See XBlock Asides Reference for the full set of built-in types, why
plugin.resize collides with the host page’s own use of it, the
listener recipe, and a complete worked example from MIT Open
Learning’s ol-openedx-chat.
Next Steps#
Once the basic Aside is working, common follow-ups include:
Persist user-specific state. Add fields with
Scope.user_stateto store per-learner data alongside the Aside — but only save them from a handler invoked in the LMS. Saving aScope.user_statefield from a handler while the Aside runs under Studio raisesxblock.exceptions.InvalidScopeError; see XBlock Asides Reference for why.Customize layout. If you need the Aside to render somewhere other than after the host block, override the runtime’s
layout_asidesin your platform integration.Study more real-world Asides. ol-openedx-chat (MIT Open Learning) and the platform’s own StructuredTagsAside show two more points on the spectrum of author-facing configuration — see About XBlock Asides for a tour of all of them.
For the complete API surface, see XBlock Asides Reference. For the conceptual background, including known limitations of the Aside mechanism, see About XBlock Asides.
See also
- About XBlock Asides (concept)
Why asides exist, what problem they solve, and current limitations.
- XBlock Asides Reference (reference)
The complete API surface for
XBlockAsideand its runtime hooks.- Quickstart: Build Your First XBlock Aside (quickstart)
A beginner-friendly walkthrough from zero to a running Aside.
Maintenance chart
Review Date |
Working Group Reviewer |
Release |
Test situation |