From 9416fbba7e1917fdd3624da4fc4a0e6c7a278adb Mon Sep 17 00:00:00 2001 From: sarina Date: Sat, 2 May 2026 01:22:15 -0400 Subject: [PATCH 1/4] Add developer's docs on XBlock Asides Co-authored-by: Claude --- .../concepts/about_xblock_asides.rst | 280 ++++++++++++ .../how-tos/add-an-xblock-aside.rst | 337 +++++++++++++++ .../quickstarts/quickstart_xblock_aside.rst | 267 ++++++++++++ .../extending_platform/index.rst | 1 + .../extending_platform/xblock_asides.rst | 401 ++++++++++++++++++ 5 files changed, 1286 insertions(+) create mode 100644 source/developers/concepts/about_xblock_asides.rst create mode 100644 source/developers/how-tos/add-an-xblock-aside.rst create mode 100644 source/developers/quickstarts/quickstart_xblock_aside.rst create mode 100644 source/developers/references/developer_guide/extending_platform/xblock_asides.rst diff --git a/source/developers/concepts/about_xblock_asides.rst b/source/developers/concepts/about_xblock_asides.rst new file mode 100644 index 000000000..d8398b415 --- /dev/null +++ b/source/developers/concepts/about_xblock_asides.rst @@ -0,0 +1,280 @@ +.. _About XBlock Asides: + +################### +About XBlock Asides +################### + +.. tags:: developer, concept + +An XBlock aside is a class that injects content into the rendered views of +existing XBlocks without modifying those XBlocks. Asides let you add behavior, +data, and UI elements to many XBlock instances at once, across XBlock types you +do not own, while preserving the host XBlock's code, fields, and Open Learning +XML (OLX) representation. + +.. contents:: Contents + :local: + :depth: 1 + +What an Aside Is +**************** + +An aside is a Python class that subclasses :class:`~xblock.core.XBlockAside`, +declares one or more view-injection methods using the +:func:`~xblock.core.XBlockAside.aside_for` decorator, and is registered with +the platform through a Python entry point in the ``xblock_asides.v1`` group. +When the platform renders an XBlock view, the runtime collects every +applicable aside, invokes its matching aside view, and appends the resulting +fragments to the host XBlock's rendered fragment. + +An aside is **not** a child XBlock. It does not appear in the course outline, +it does not have its own URL, and it cannot be added to a course like a +regular block. It exists only in relation to a host block, and its lifecycle +is bound to that host block's lifecycle. + +For the precise API surface, see :ref:`XBlock Asides Reference`. + +The Problem Asides Solve +************************ + +When you want to enhance the behavior of an XBlock that you did not write, +you have three options: + +#. Fork the XBlock and modify it directly. +#. Replace the XBlock with a new XBlock that wraps the original. +#. Attach an aside to the existing XBlock. + +The first two options carry significant costs. Forking creates a parallel +codebase that must be maintained against upstream changes. Replacing the +XBlock requires every existing course that uses the original to migrate, and +it does not scale when you want to enhance many different XBlock types in the +same way. + +Asides solve this by externalizing the enhancement. The host XBlock is not +modified. The same aside can apply to a Video block, a Problem block, or any +other block type, by overriding a single classmethod. Course authors keep +control over whether the enhancement is active for a given block, because +asides expose their own scoped fields. The enhancement travels with the +course in OLX export and import. + +Reach for an aside when all of the following are true: + +* You want to enhance one or more existing XBlock types without forking them. +* The enhancement is conceptually layered on top of the block, not a + replacement for any of its behavior. +* The enhancement should apply to many block instances, possibly across + block types, without per-instance configuration in the course outline. +* The enhancement may need its own settings or stored data, scoped to the + block instance. + +Reach for something else when: + +* You are creating a brand new piece of course content. Write an XBlock. +* You need to change a behavior that is internal to a single block type and + not visible in any view. Consider a runtime service or a filter from the + Hooks Extension Framework. +* You only need to react to platform events. Consider an Open edX event + receiver. + +How an Aside Relates to Its Host Block +************************************** + +The runtime maintains a many-to-many relationship between asides and host +blocks at runtime, but each aside instance is bound to exactly one host block +during a single render. The relationship is established in three stages. + +Discovery +========= + +When the runtime renders an XBlock view, it asks the runtime for the set of +applicable aside types. The default runtime returns every aside class +registered through the ``xblock_asides.v1`` entry point. A runtime may +override this to filter the set further, for example based on the current +user or the course. + +Per-Block Filtering +=================== + +For each candidate aside type, the runtime instantiates the aside and asks +it whether it should apply to this specific block by calling its +:meth:`~xblock.core.XBlockAside.should_apply_to_block` classmethod. The +default implementation returns ``True``. Real-world asides almost always +override this method to restrict themselves to specific block types, course +contexts, or feature flags. + +Rendering and Layout +==================== + +For each aside that survives filtering, the runtime invokes the aside method +that was decorated with ``@XBlockAside.aside_for(view_name)`` for the view +being rendered. The aside method returns a ``Fragment``, the runtime wraps +that fragment with identifying markup, and the runtime appends the wrapped +fragment to the host block's rendered output. A runtime can override +:meth:`~xblock.runtime.Runtime.layout_asides` to control where and how the +aside fragments are placed. + +Why Asides Are Worth the Trouble +******************************** + +The framing above describes the trade-offs from the perspective of someone +choosing among extension mechanisms. The deeper reasons asides exist, and +remain useful, come from the production deployments that depend on them. + +Multiple Block Types, One Implementation +======================================== + +A single aside class can decorate Video blocks, Problem blocks, and any +other block type the author chooses, by checking ``block.category`` or +``block.scope_ids.block_type`` inside ``should_apply_to_block``. The MIT +Open Learning chat aside, for example, attaches an "AskTIM" chat button to +both Video and Problem blocks from a single class, with one entry point. +Without asides, the same outcome would require either two parallel forks +or replacement blocks for both types. + +Course Author Control +===================== + +An aside can declare its own scoped fields, just like an XBlock. By exposing +those fields in an author view, an aside gives course authors a UI to enable +or disable the enhancement on a per-block basis. The settings are stored +under the aside's own scope, not the host block's, so they are preserved +across exports and imports without any change to the host block's data +model. + +OLX Export and Import +===================== + +When a course is exported to OLX, the platform serializes each aside as an +XML child element under its host block, named after the aside's entry point +name. On import, the runtime reconstitutes the asides automatically. This +means an aside-enhanced course is portable, with limitations described below. + +Real-World Examples +******************* + +Three implementations in the wild illustrate the range of what asides can +do. + +Rapid Response XBlock +===================== + +The `rapid-response-xblock`_ from MIT Open Learning is a single aside that +applies to Problem blocks. It overlays an instructor-only control on the +problem in the LMS that lets a live instructor open and close response +windows during a lecture, and it renders a real-time chart of student +responses. Course authors enable it per problem in Studio. The repository +name calls it an "xblock" but the implementation is purely an aside. + +Open Learning Chat Aside +======================== + +The `ol-openedx-chat`_ aside, also from MIT Open Learning, attaches an +"AskTIM" chat button to Video and Problem blocks. The button opens a +context-aware chat drawer that streams messages to a backend large language +model, passing block-specific context such as a video transcript identifier +or a problem's siblings. A single aside class, registered as one entry +point, handles both block types and uses ``should_apply_to_block`` to gate +on a course-level waffle flag and per-course settings. + +Thumbs Sample Aside +=================== + +The `xblock-sdk`_ repository contains a ``ThumbsAside`` class in +``sample_xblocks/thumbs/thumbs.py``. It is **not functional** and is not +registered through any entry point. The class comment in the source notes: +"Asides aren't ready yet, so this is currently not being installed in +setup.py." It exists as a syntactic example of the decorator pattern, not +as a working aside. Treat it as illustrative only. + +Limitations +*********** + +Asides are a real, working feature in production deployments, but the +ecosystem around them is incomplete. The list below is drawn from the +state of the codebase as of the Sumac release and from a 2025 Open edX +Conference talk by Peter Pinch of MIT Open Learning. Read it before +committing to an aside-based design. + +No Authoring Story in the Course Authoring MFE +============================================== + +The Studio author view for an aside is rendered by the legacy course +authoring frontend. The current Course Authoring micro-frontend has no +defined location to display aside author UI. If your project depends on the +new MFE for authoring, plan to render the aside's author UI through a +different mechanism, or accept that authors will use the legacy Studio for +this part of the workflow. + +Not All XBlocks Round-Trip Through OLX +====================================== + +OLX export and import for asides depends on the host XBlock cooperating +with the export process. Some XBlocks, including ORA2, do not preserve +aside data through their export and import paths. If your aside must +survive a course export and re-import on a course that uses one of these +blocks, test the round trip end to end before depending on it. + +Multiple Asides on a Single Block Are Not Reliable +================================================== + +The runtime supports multiple aside types decorating the same block in +principle, but interactions between asides on the same block are not +well-tested. Two asides that both decorate ``student_view`` on the same +block may render correctly in isolation and break when combined. If you +need this, build a single aside that composes both behaviors rather than +relying on two independent asides to coexist. + +JavaScript Library Loading Is Limited +===================================== + +Asides use the same fragment-based JavaScript loading mechanism as XBlocks, +which assumes a single set of static assets. If your aside needs a JS +library that is not already loaded by the host page, you must add it +through the fragment, and you must handle ordering and conflicts yourself. +There is no shared aside-level mechanism for declaring library dependencies. + +Documentation Has Historically Been Sparse +========================================== + +XBlock Asides have been part of the platform for years but have had no +user-facing documentation until this set of articles. The original work was +done by Dave Ormsbee. Most of the institutional knowledge has lived in +docstrings, test code, and the implementations of a handful of asides +maintained outside the core platform. If you find this documentation lacks +detail your project needs, the test file at ``xblock/test/test_asides.py`` +in the XBlock repository is the most reliable source of behavioral truth. + +Where to Go Next +**************** + +If you are ready to build an aside, start with +:ref:`XBlock Aside Quickstart`. If you already have a target XBlock in mind +and want a step-by-step recipe, read :ref:`Add an XBlock Aside`. For the +complete list of classes, decorators, methods, and entry points, consult +:ref:`XBlock Asides Reference`. + +.. _rapid-response-xblock: https://github.com/mitodl/rapid-response-xblock +.. _ol-openedx-chat: https://github.com/mitodl/open-edx-plugins/tree/main/src/ol_openedx_chat +.. _xblock-sdk: https://github.com/openedx/xblock-sdk + +.. seealso:: + + :ref:`XBlock Asides Reference` (reference) + The complete API surface for ``XBlockAside`` and its runtime hooks. + + :ref:`Add an XBlock Aside` (how-to) + A step-by-step recipe for adding an aside to existing XBlocks. + + :ref:`XBlock Aside Quickstart` (quickstart) + A beginner-friendly walkthrough from zero to a running aside. + + :ref:`Hooks Extension Framework` (concept) + An alternative extension mechanism for non-view-based behaviors. + +**Maintenance chart** + ++--------------+-------------------------------+----------------+--------------------------------+ +| Review Date | Working Group Reviewer | Release |Test situation | ++--------------+-------------------------------+----------------+--------------------------------+ +| | | | | ++--------------+-------------------------------+----------------+--------------------------------+ diff --git a/source/developers/how-tos/add-an-xblock-aside.rst b/source/developers/how-tos/add-an-xblock-aside.rst new file mode 100644 index 000000000..3819dc9d2 --- /dev/null +++ b/source/developers/how-tos/add-an-xblock-aside.rst @@ -0,0 +1,337 @@ +.. _Add an XBlock Aside: + +#################### +Add an XBlock Aside +#################### + +.. tags:: developer, how-to + +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 +:ref:`About XBlock Asides`. For a complete API reference, see +:ref:`XBlock Asides Reference`. + +.. contents:: Contents + :local: + :depth: 1 + +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 + :class:`~web_fragments.fragment.Fragment`. If you have never written + one, complete :ref:`XBlock Aside Quickstart` 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: + +.. code-block:: text + + feedback_badge_aside/ + ├── pyproject.toml + ├── feedback_badge_aside/ + │ ├── __init__.py + │ └── aside.py + └── README.rst + +The package name (``feedback_badge_aside``) and the module name +(``aside.py``) are conventions; pick names that describe your aside. + +Populate ``pyproject.toml`` with the package metadata and a placeholder +for the entry point you will add in :ref:`Step 6 `. + +.. code-block:: toml + + [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 +:class:`~xblock.core.XBlockAside`. + +.. code-block:: python + + 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 3: Declare fields for course-author control +************************************************ + +Add a Boolean field that course authors can toggle to enable or disable +the aside on a per-block basis. Scope the field to ``Scope.settings``, +which means the value is stored per block and travels with the course in +OLX export and import. + +.. code-block:: python + + 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.""" + + enabled = Boolean( + display_name="Show feedback link", + default=True, + scope=Scope.settings, + help="Whether to show a 'Report an issue' link on this block.", + ) + +Step 4: Decorate the views you want to inject into +************************************************** + +Add one method per XBlock view you want to decorate, using +:meth:`~xblock.core.XBlockAside.aside_for`. The method takes ``self``, +the host ``block``, and an optional ``context`` dictionary, and returns +a :class:`~web_fragments.fragment.Fragment`. + +.. code-block:: python + + 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.""" + + 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.enabled: + return Fragment("") + + block_id = block.scope_ids.usage_id.block_id + html = ( + f'' + ) + return Fragment(html) + + @XBlockAside.aside_for("studio_view") + def studio_view_aside(self, block, context=None): + """Render the author-side toggle UI in Studio.""" + checked = "checked" if self.enabled else "" + html = ( + f'' + ) + return Fragment(html) + +For production code, render templates from files with the runtime's +template service rather than building HTML strings inline. The strings +above keep the example readable. + +Step 5: Filter to specific block types +************************************** + +By default, an aside applies to every block. Override +:meth:`~xblock.core.XBlockAside.should_apply_to_block` to restrict the +aside to the block types you support. + +.. code-block:: python + + @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_key`` to 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 +:ref:`About XBlock Asides` for the relevant limitations. + +.. _register entry point: + +Step 6: 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. + +.. code-block:: toml + + [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 7: Install the package and restart services +************************************************ + +Install the package into the LMS and Studio Python environments. With +Tutor: + +.. code-block:: bash + + tutor mounts add /path/to/feedback_badge_aside + tutor dev launch + +Or, if you are managing the environment manually: + +.. code-block:: bash + + pip install -e /path/to/feedback_badge_aside + +After installing, restart both the LMS and Studio. Asides are discovered +at process start, so newly installed asides do not appear until the +services restart. + +Step 8: Enable asides in the LMS +******************************** + +The edx-platform LMS gates aside rendering on a Django configuration +model, ``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: + +.. code-block:: text + + http:///admin/lms_xblock/xblockasidesconfig/ + +Click :guilabel:`Add`, check :guilabel:`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 5. + +The Studio runtime does not consult this configuration. Asides render +in Studio author views independently of ``XBlockAsidesConfig``, as soon +as they are installed and registered. + +Step 9: 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 in Studio and +confirm the toggle appears in the studio view. + +If the aside does not appear: + +#. Check that the entry point is registered. Run: + + .. code-block:: python + + from xblock.core import XBlockAside + print(list(XBlockAside.load_classes())) + + in a Django shell. Your aside's type name should be in the list. + +#. Check that ``should_apply_to_block`` returns ``True`` for the block + you are testing. +#. Check that the aside is allowlisted for the course. +#. Check the LMS and Studio logs for any exceptions raised inside your + aside view. + +Next Steps +********** + +Once the basic aside is working, common follow-ups include: + +* **Add an AJAX handler.** Decorate a method with ``@XBlock.handler`` + and call it from the rendered fragment with + ``self.runtime.handler_url(self, "handler_name")``. +* **Render from templates.** Use the runtime's template service to + render HTML from ``.html`` files in your package's static assets. +* **Persist user-specific state.** Add fields with + ``Scope.user_state`` to store per-learner data alongside the aside. +* **Customize layout.** If you need the aside to render somewhere other + than after the host block, override the runtime's ``layout_asides`` + in your platform integration. + +For the complete API surface, see :ref:`XBlock Asides Reference`. For +the conceptual background, including known limitations of the aside +mechanism, see :ref:`About XBlock Asides`. + +.. seealso:: + + :ref:`About XBlock Asides` (concept) + Why asides exist, what problem they solve, and current limitations. + + :ref:`XBlock Asides Reference` (reference) + The complete API surface for ``XBlockAside`` and its runtime hooks. + + :ref:`XBlock Aside Quickstart` (quickstart) + A beginner-friendly walkthrough from zero to a running aside. + +**Maintenance chart** + ++--------------+-------------------------------+----------------+--------------------------------+ +| Review Date | Working Group Reviewer | Release |Test situation | ++--------------+-------------------------------+----------------+--------------------------------+ +| | | | | ++--------------+-------------------------------+----------------+--------------------------------+ diff --git a/source/developers/quickstarts/quickstart_xblock_aside.rst b/source/developers/quickstarts/quickstart_xblock_aside.rst new file mode 100644 index 000000000..f3ad8ab48 --- /dev/null +++ b/source/developers/quickstarts/quickstart_xblock_aside.rst @@ -0,0 +1,267 @@ +.. _XBlock Aside Quickstart: + +########################################## +Quickstart: Build Your First XBlock Aside +########################################## + +.. tags:: developer, quickstart + +Build, install, and run a minimal XBlock aside in a Tutor development +environment. By the end of this quickstart, you will have an aside that +adds a small banner to every Problem block in a course, and you will +understand the four moving parts every aside has: the class, the +decorated views, the entry point, and the install step. + +This quickstart deliberately keeps the aside trivial. For a more +realistic recipe, work through :ref:`Add an XBlock Aside` next. For the +conceptual background, see :ref:`About XBlock Asides`. + +.. contents:: Contents + :local: + :depth: 1 + +Prerequisites +************* + +You need: + +* A running Tutor development environment with the LMS and Studio + services accessible. Tutor's `Getting started`_ guide walks through + the install if you do not have one yet. +* The installed Python version used by the target Open edX release. +* A course in your devstack with at least one Problem block. The Tutor + demo course works. +* A text editor and a terminal. + +You do **not** need: + +* Prior experience writing XBlocks. The aside in this quickstart uses + plain Python and a hardcoded HTML string. +* Familiarity with the Open Learning XML (OLX) format. + +Step 1: Create a Python package +******************************* + +In a working directory of your choice, create the following file +structure: + +.. code-block:: text + + hello_aside/ + ├── pyproject.toml + └── hello_aside/ + ├── __init__.py + └── aside.py + +Make ``__init__.py`` an empty file. The remaining steps describe what to +put in the other two files. + +Step 2: Write the aside class +***************************** + +In ``hello_aside/aside.py``, paste the following code: + +.. code-block:: python + + from web_fragments.fragment import Fragment + from xblock.core import XBlockAside + + + class HelloAside(XBlockAside): + """A trivial aside that prints a banner above every Problem block.""" + + @XBlockAside.aside_for("student_view") + def student_view_aside(self, block, context=None): + return Fragment( + '
' + 'Hello from an XBlock aside!' + '
' + ) + + @classmethod + def should_apply_to_block(cls, block): + return getattr(block, "category", None) == "problem" + +This is the entire implementation. Three things to notice: + +* The class subclasses :class:`~xblock.core.XBlockAside`. +* The :meth:`~xblock.core.XBlockAside.aside_for` decorator marks + ``student_view_aside`` as the method to call when an XBlock's + ``student_view`` is being rendered. +* :meth:`~xblock.core.XBlockAside.should_apply_to_block` restricts the + aside to Problem blocks. Without it, the banner would appear on every + block in every course. + +Step 3: Configure the package +***************************** + +In ``hello_aside/pyproject.toml``, paste: + +.. code-block:: toml + + [project] + name = "hello-aside" + version = "0.1.0" + requires-python = ">=X.Y" # set to the minimum Python version for the target Open edX release + dependencies = ["XBlock", "web-fragments"] + + [project.entry-points."xblock_asides.v1"] + hello_aside = "hello_aside.aside:HelloAside" + + [build-system] + requires = ["setuptools>=61.0"] + build-backend = "setuptools.build_meta" + +The critical line is the one under +``[project.entry-points."xblock_asides.v1"]``. It tells the Open edX +platform that a class called ``HelloAside``, in the ``hello_aside.aside`` +module, should be loaded as an aside under the type name +``hello_aside``. This entry point group is how every aside is +discovered. + +Step 4: Install the package into Tutor +************************************** + +From the directory containing ``hello_aside/``, mount the package into +Tutor and relaunch the development environment: + +.. code-block:: bash + + tutor mounts add ./hello_aside + tutor dev launch + +The ``mounts add`` command tells Tutor to install the local package into +both the LMS and Studio containers each time they start. The +``dev launch`` command rebuilds and restarts the containers so the new +aside is picked up. + +If you are not using Tutor, install the package directly into the LMS +and Studio Python environments with ``pip install -e ./hello_aside``, +then restart both services. + +Step 5: Enable the aside system in the LMS +****************************************** + +The LMS gates aside rendering on a global Django configuration model, +``XBlockAsidesConfig``. Until that model is enabled, no aside renders on +any block, regardless of whether it is registered or installed. + +Open the LMS Django admin in your browser: + +.. code-block:: text + + http:///admin/lms_xblock/xblockasidesconfig/ + +Click :guilabel:`Add` to create a new revision. Check :guilabel:`Enabled` +and click :guilabel:`Save`. The new revision becomes the current +configuration immediately. + +The same form has a ``Disabled blocks`` field that holds a +space-separated list of block types on which asides will **never** +render. The default value is ``about course_info static_tab``. The +``hello_aside`` example targets ``problem`` blocks, which are not in the +default disabled list, so no further changes are needed. + +The Studio runtime does not consult this model. Asides will render in +Studio author views as soon as they are installed, independently of +whether ``XBlockAsidesConfig`` is enabled. + +Step 6: Verify the aside is rendering +************************************* + +Open a course in the LMS, navigate to a unit that contains a Problem +block, and confirm a light blue banner reading "Hello from an XBlock +aside!" appears below the problem. If you see the banner, your aside is +working. + +If the banner does not appear, work through these checks in order: + +#. **The aside is registered.** Open a Django shell and run: + + .. code-block:: python + + from xblock.core import XBlockAside + print(list(XBlockAside.load_classes())) + + The list should include ``hello_aside``. If it does not, the entry + point is not installed; recheck Step 3 and Step 4. + +#. **The aside is allowlisted.** Confirm Step 5 was applied to the + course you are viewing. + +#. **The block type matches.** Your test block must have + ``category == "problem"``. A Video block, an HTML block, or a + Discussion block will not trigger the aside. + +#. **No exceptions are being swallowed.** Check the LMS logs for any + exception raised inside ``student_view_aside`` or + ``should_apply_to_block``. The runtime catches some aside exceptions + silently, which can make a broken aside look like a missing one. + +What You Just Built +******************* + +You have a working aside with all four required pieces: + +A class + ``HelloAside`` subclasses ``XBlockAside``. + +A decorated view + ``student_view_aside`` is decorated with + ``@XBlockAside.aside_for("student_view")``, so it is invoked + whenever the runtime renders any block's ``student_view``. + +A filter + ``should_apply_to_block`` restricts the aside to Problem blocks. + +An entry point + The ``xblock_asides.v1`` entry point in ``pyproject.toml`` makes the + aside discoverable by the platform at startup. + +Every production aside has the same four pieces, plus additional +features such as scoped fields, AJAX handlers, author-side UI, and +template-rendered HTML. + +Where to Go Next +**************** + +To turn this trivial example into something useful: + +* **Add a course-author toggle.** Declare a ``Boolean`` field with + ``Scope.settings`` and conditionally render the banner based on its + value. The :ref:`Add an XBlock Aside` how-to walks through this. +* **Render from a template.** Replace the inline HTML string with a + template loaded from your package's static assets, rendered through + the runtime's template service. +* **Add an AJAX handler.** Decorate a method with ``@XBlock.handler`` + and call it from JavaScript in your fragment to support interactive + behavior. +* **Decorate the author view.** Add a second method decorated with + ``@XBlockAside.aside_for("author_view")`` to render an author-facing + preview in Studio. + +For each of these extensions, the :ref:`XBlock Asides Reference` is the +authoritative source. For the trade-offs and current limitations of the +aside mechanism, read :ref:`About XBlock Asides` before scaling up your +design. + +.. _Getting started: https://docs.tutor.edly.io/quickstart.html + +.. seealso:: + + :ref:`About XBlock Asides` (concept) + Why asides exist, what problem they solve, and current limitations. + + :ref:`Add an XBlock Aside` (how-to) + A step-by-step recipe for adding an aside to existing XBlocks. + + :ref:`XBlock Asides Reference` (reference) + The complete API surface for ``XBlockAside`` and its runtime hooks. + +**Maintenance chart** + ++--------------+-------------------------------+----------------+--------------------------------+ +| Review Date | Working Group Reviewer | Release |Test situation | ++--------------+-------------------------------+----------------+--------------------------------+ +| | | | | ++--------------+-------------------------------+----------------+--------------------------------+ diff --git a/source/developers/references/developer_guide/extending_platform/index.rst b/source/developers/references/developer_guide/extending_platform/index.rst index 584f582cb..9e136b677 100644 --- a/source/developers/references/developer_guide/extending_platform/index.rst +++ b/source/developers/references/developer_guide/extending_platform/index.rst @@ -7,3 +7,4 @@ Extending the edX Platform extending xblocks + xblock_asides diff --git a/source/developers/references/developer_guide/extending_platform/xblock_asides.rst b/source/developers/references/developer_guide/extending_platform/xblock_asides.rst new file mode 100644 index 000000000..a2c6f3cae --- /dev/null +++ b/source/developers/references/developer_guide/extending_platform/xblock_asides.rst @@ -0,0 +1,401 @@ +.. _XBlock Asides Reference: + +####################### +XBlock Asides Reference +####################### + +.. tags:: developer, reference + +This reference describes the public API surface for XBlock Asides as +defined in the ``xblock`` package. It covers the +:class:`~xblock.core.XBlockAside` base class, the decorators and +classmethods that subclasses use, the runtime hooks that govern aside +discovery and rendering, the entry point group that registers asides as +plugins, and the OLX serialization contract. + +For an introduction to what asides are and when to use them, see +:ref:`About XBlock Asides`. For a guided walkthrough, see +:ref:`Add an XBlock Aside` or :ref:`XBlock Aside Quickstart`. + +.. contents:: Contents + :local: + :depth: 1 + +The XBlockAside Class +********************* + +The base class for all asides is ``xblock.core.XBlockAside``. It inherits +from ``Plugin`` and ``Blocklike``, so an aside can declare scoped fields, +mark methods as handlers, and be loaded as a plugin in the same way as an +XBlock. + +.. code-block:: python + + from xblock.core import XBlockAside + + class MyAside(XBlockAside): + """An aside that decorates one or more XBlock views.""" + +Class Attributes +================ + +``entry_point`` + The Python entry point group used to discover aside plugins. Set on the + base class to ``"xblock_asides.v1"``. Subclasses should not change this. + +``fields`` + The set of fields declared on the aside, automatically collected from + class-level :class:`~xblock.fields.Field` declarations. Inherited from + ``Blocklike``. + +Decorators +********** + +``XBlockAside.aside_for(view_name)`` +==================================== + +A classmethod decorator that marks a method as the aside view for the +named XBlock view. When the runtime renders an XBlock view called +``view_name``, every applicable aside whose class declares a method +decorated with ``@XBlockAside.aside_for(view_name)`` is invoked, and the +returned fragments are appended to the host block's rendered fragment. + +**Signature of the decorated method** + +.. code-block:: python + + @XBlockAside.aside_for("student_view") + def student_view_aside(self, block, context=None): + ... + return Fragment(...) + +The decorated method takes: + +* ``self`` — the aside instance. +* ``block`` — the host XBlock instance being rendered. +* ``context`` — an optional dictionary of context data passed by the + caller of ``render``. + +The method must return a :class:`~web_fragments.fragment.Fragment`. + +**Multiple views per aside** + +A single method may decorate multiple views by stacking decorators, and a +single aside class may define different methods for different views. + +.. code-block:: python + + class MyAside(XBlockAside): + + @XBlockAside.aside_for("student_view") + def student_view_aside(self, block, context=None): + return Fragment("
Student-side aside
") + + @XBlockAside.aside_for("author_view") + @XBlockAside.aside_for("studio_view") + def studio_aside(self, block, context=None): + return Fragment("
Author/Studio aside
") + +**Common view names** + +The view names commonly decorated by asides include: + ++----------------------+---------------------------------------------------+ +| View name | When the runtime renders it | ++======================+===================================================+ +| ``student_view`` | The learner-facing view of a block in the LMS. | ++----------------------+---------------------------------------------------+ +| ``author_view`` | The author-facing preview of a block in Studio. | ++----------------------+---------------------------------------------------+ +| ``studio_view`` | The author-facing edit form of a block in Studio. | ++----------------------+---------------------------------------------------+ + +XBlocks may define additional views; an aside can decorate any of them by +name. + +Classmethods +************ + +``XBlockAside.should_apply_to_block(cls, block)`` +================================================= + +A classmethod that returns ``True`` if the aside should apply to the given +``block``, and ``False`` otherwise. The default implementation returns +``True`` unconditionally. + +Override this method to restrict the aside to specific block types, +courses, or feature flags. The runtime calls this method on every +aside-block pair before rendering, and asides for which it returns +``False`` are skipped. + +.. code-block:: python + + @classmethod + def should_apply_to_block(cls, block): + block_type = getattr(block, "category", None) + return block_type in {"problem", "video"} + +The ``block`` argument is the host XBlock instance. Most filtering logic +inspects ``block.scope_ids.block_type``, ``block.category``, the course +context derived from ``block.scope_ids.usage_id``, or platform feature +flags. + +Instance Methods +**************** + +``aside_view_declaration(view_name)`` +===================================== + +Return the bound method on this aside instance that is decorated with +``@XBlockAside.aside_for(view_name)``, or ``None`` if no such method +exists. The runtime uses this method to look up the aside's view function +when rendering. Subclasses do not normally need to call it directly. + +``needs_serialization()`` +========================= + +Return ``True`` if the aside has any field whose value differs from its +default. The default implementation iterates over the aside's fields and +returns ``True`` if any field is set on this instance. The runtime calls +this method during OLX export to decide whether to serialize the aside as +an XML element. Subclasses rarely need to override this. + +``parse_xml(node, runtime, keys)`` and ``add_xml_to_node(node)`` +================================================================ + +Inherited from ``Blocklike``. Override these to customize how the aside is +read from and written to OLX. The default implementations serialize the +aside's fields as XML attributes and child elements, identical to the +default XBlock behavior. + +Fields and Scopes +***************** + +An aside declares fields the same way an XBlock does, with class-level +field declarations. Fields are scoped, and the scope determines where the +field's value is stored and which entities share it. + +.. code-block:: python + + from xblock.core import XBlockAside + from xblock.fields import Boolean, Scope, String + + class MyAside(XBlockAside): + enabled = Boolean( + display_name="Enabled", + default=False, + scope=Scope.settings, + help="Whether this aside is active for this block.", + ) + + last_message = String( + default="", + scope=Scope.user_state, + help="The most recent message for this user-block pair.", + ) + +The supported scopes are the standard XBlock scopes from +:mod:`xblock.fields`: ``Scope.content``, ``Scope.settings``, +``Scope.user_state``, ``Scope.user_state_summary``, +``Scope.preferences``, and ``Scope.user_info``. An aside's field values +are stored under the aside's own usage ID, separate from the host block's +field values. + +Handlers +******** + +An aside can define AJAX handlers using the standard +``@XBlock.handler`` decorator from ``xblock.core``. The handler URL is +generated by the runtime in the same way as for an XBlock, so an aside's +view fragment can call its own handler with +``self.runtime.handler_url(self, "handler_name")``. + +.. code-block:: python + + from xblock.core import XBlock, XBlockAside + + class MyAside(XBlockAside): + + @XBlock.handler + def submit_feedback(self, request, suffix=""): + ... + return Response(json_body={"ok": True}) + +The handler signature, request object, and response handling are +identical to XBlock handlers. + +Entry Point Registration +************************ + +An aside is discovered by the runtime through a Python entry point in the +``xblock_asides.v1`` group. Declare the entry point in the package's +``pyproject.toml``: + +.. code-block:: toml + + [project.entry-points."xblock_asides.v1"] + my_aside = "my_package.aside:MyAside" + +Or in ``setup.py``: + +.. code-block:: python + + setup( + entry_points={ + "xblock_asides.v1": [ + "my_aside = my_package.aside:MyAside", + ], + }, + ) + +The entry point name on the left of the equals sign is the aside's +**type name**. It is used as the XML tag when the aside is serialized to +OLX, and as the key in :class:`~xblock.scopes.ScopeIds` when an aside +instance is constructed. Choose a name that is unique across all +installed asides on a deployment. + +Runtime API +*********** + +The methods listed below are defined on +:class:`~xblock.runtime.Runtime` and govern aside discovery, instantiation, +and rendering. Aside subclasses do not normally call these directly. They +are documented here so runtime implementors and aside authors can +understand the lifecycle. + +Discovery +========= + +``runtime.applicable_aside_types(block)`` + Return the list of aside type names that may apply to ``block``. The + default implementation returns every aside class registered through + the ``xblock_asides.v1`` entry point. A runtime may override this to + filter by user, course, or other context. + + The edx-platform LMS runtime overrides this through + ``lms_applicable_aside_types`` in + ``lms/djangoapps/lms_xblock/runtime.py``, which gates aside rendering + on the ``XBlockAsidesConfig`` Django configuration model. When the + model's current revision has ``enabled=False``, no asides render in + the LMS. When enabled, asides do not render on block types listed in + the model's ``disabled_blocks`` field (default value: + ``"about course_info static_tab"``). The Studio runtime does not + apply this gate. See :ref:`Add an XBlock Aside` for the + administrative steps to enable the configuration. + +``runtime.load_aside_type(aside_type)`` + Return the :class:`XBlockAside` subclass corresponding to the given + ``aside_type`` string. Raises if no aside is registered under that + name. + +Instantiation +============= + +``runtime.create_aside(block_type, keys)`` + Construct an aside instance of the named ``block_type`` (the aside + type name, despite the parameter name) with the given ``ScopeIds``. + Returns an :class:`XBlockAside` instance. + +``runtime.get_aside_of_type(block, aside_type)`` + Construct an aside of the named type that is bound to the given host + ``block``. Generates the aside's definition and usage IDs from the + host block's IDs using the runtime's ``id_generator``. Returns an + :class:`XBlockAside` instance. + +``runtime.get_aside(aside_usage_id)`` + Construct an aside instance from a previously known aside usage ID. + Used during OLX import and other paths where the aside's identity is + already established. + +``runtime.get_asides(block)`` + Return the list of aside instances that should decorate the given + ``block``. This method composes ``applicable_aside_types`` and each + aside's ``should_apply_to_block`` filter, returning only the asides + for which both pass. + +Rendering +========= + +``runtime.render_asides(block, view_name, frag, context)`` + Called by the runtime's ``render`` method after the block's own view + has produced its fragment. Iterates over ``get_asides(block)``, looks + up each aside's view declaration for ``view_name``, and dispatches + layout to ``layout_asides``. Returns the augmented + :class:`Fragment`. + +``runtime.layout_asides(block, context, frag, view_name, aside_frag_fns)`` + Execute the aside view functions and combine their fragments with the + block's fragment. The default implementation appends each aside's + wrapped fragment after the block's fragment. Override this to control + the placement, ordering, or conditional inclusion of asides. Any + override must call ``wrap_aside`` around each aside fragment to + preserve client-side identification. + +``runtime.wrap_aside(block, aside, view, frag, context)`` + Wrap an aside's fragment with a ``
`` carrying the aside's usage + ID, the host block's usage ID, and any JavaScript initialization + metadata. Override this if you need a different wrapping element or + different ``data-`` attributes. + +OLX Serialization +***************** + +When a course is exported to OLX, the runtime serializes every aside +that returns ``True`` from ``needs_serialization()`` as an XML child +element of its host block. The XML tag of the aside element is the +aside's entry point name. Field values are written as attributes or +nested elements according to the aside's ``add_xml_to_node`` +implementation. + +On import, the runtime detects aside elements by looking up their tag +names in the registered ``xblock_asides.v1`` entry points. Tag names +that do not resolve to a registered aside are ignored. Field values are +then read by the aside's ``parse_xml`` implementation. + +Two practical consequences: + +* An aside's data only round-trips through OLX if both the source and + destination platforms have the same aside installed under the same + entry point name. +* Some XBlocks do not preserve aside child elements through their own + export and import paths. See :ref:`About XBlock Asides` for the + current list of known issues. + +Render Pipeline Summary +*********************** + +The full sequence of calls when a runtime renders an XBlock view is: + +#. ``runtime.render(block, view_name, context)`` is called. +#. The runtime invokes the block's view function and obtains a fragment. +#. The runtime calls ``runtime.wrap_xblock`` on that fragment. +#. The runtime calls ``runtime.render_asides(block, view_name, frag, context)``. +#. ``render_asides`` calls ``runtime.get_asides(block)``, which uses + ``applicable_aside_types(block)`` and each aside's + ``should_apply_to_block(block)`` to compute the filtered list. +#. For each surviving aside, ``render_asides`` calls + ``aside.aside_view_declaration(view_name)`` to find the matching + method. +#. ``render_asides`` calls ``layout_asides``, which invokes each aside + view function, calls ``wrap_aside`` on each result, and appends the + wrapped fragments to the block's fragment. +#. The combined fragment is returned to the original caller. + +.. seealso:: + + :ref:`About XBlock Asides` (concept) + Why asides exist, what problem they solve, and current limitations. + + :ref:`Add an XBlock Aside` (how-to) + A step-by-step recipe for adding an aside to existing XBlocks. + + :ref:`XBlock Aside Quickstart` (quickstart) + A beginner-friendly walkthrough from zero to a running aside. + +**Maintenance chart** + ++--------------+-------------------------------+----------------+--------------------------------+ +| Review Date | Working Group Reviewer | Release |Test situation | ++--------------+-------------------------------+----------------+--------------------------------+ +| | | | | ++--------------+-------------------------------+----------------+--------------------------------+ From 706507791532991cd01ba4a0fcc4873034cda593 Mon Sep 17 00:00:00 2001 From: Sarina Canelake Date: Sun, 3 May 2026 21:49:59 -0400 Subject: [PATCH 2/4] temp: Apply suggestions from code review Co-authored-by: David Ormsbee Co-authored-by: Peter Pinch --- .../concepts/about_xblock_asides.rst | 46 +++++-------------- .../how-tos/add-an-xblock-aside.rst | 13 +----- .../quickstarts/quickstart_xblock_aside.rst | 3 -- 3 files changed, 12 insertions(+), 50 deletions(-) diff --git a/source/developers/concepts/about_xblock_asides.rst b/source/developers/concepts/about_xblock_asides.rst index d8398b415..1dc4e25b3 100644 --- a/source/developers/concepts/about_xblock_asides.rst +++ b/source/developers/concepts/about_xblock_asides.rst @@ -52,10 +52,8 @@ same way. Asides solve this by externalizing the enhancement. The host XBlock is not modified. The same aside can apply to a Video block, a Problem block, or any -other block type, by overriding a single classmethod. Course authors keep -control over whether the enhancement is active for a given block, because -asides expose their own scoped fields. The enhancement travels with the -course in OLX export and import. +other block type, by overriding a single classmethod. Asides can serialize +their own scoped fields during course import and export. Reach for an aside when all of the following are true: @@ -70,9 +68,6 @@ Reach for an aside when all of the following are true: Reach for something else when: * You are creating a brand new piece of course content. Write an XBlock. -* You need to change a behavior that is internal to a single block type and - not visible in any view. Consider a runtime service or a filter from the - Hooks Extension Framework. * You only need to react to platform events. Consider an Open edX event receiver. @@ -83,15 +78,6 @@ The runtime maintains a many-to-many relationship between asides and host blocks at runtime, but each aside instance is bound to exactly one host block during a single render. The relationship is established in three stages. -Discovery -========= - -When the runtime renders an XBlock view, it asks the runtime for the set of -applicable aside types. The default runtime returns every aside class -registered through the ``xblock_asides.v1`` entry point. A runtime may -override this to filter the set further, for example based on the current -user or the course. - Per-Block Filtering =================== @@ -152,7 +138,7 @@ means an aside-enhanced course is portable, with limitations described below. Real-World Examples ******************* -Three implementations in the wild illustrate the range of what asides can +Two implementations in the wild illustrate the range of what asides can do. Rapid Response XBlock @@ -176,16 +162,6 @@ or a problem's siblings. A single aside class, registered as one entry point, handles both block types and uses ``should_apply_to_block`` to gate on a course-level waffle flag and per-course settings. -Thumbs Sample Aside -=================== - -The `xblock-sdk`_ repository contains a ``ThumbsAside`` class in -``sample_xblocks/thumbs/thumbs.py``. It is **not functional** and is not -registered through any entry point. The class comment in the source notes: -"Asides aren't ready yet, so this is currently not being installed in -setup.py." It exists as a syntactic example of the decorator pattern, not -as a working aside. Treat it as illustrative only. - Limitations *********** @@ -199,7 +175,7 @@ No Authoring Story in the Course Authoring MFE ============================================== The Studio author view for an aside is rendered by the legacy course -authoring frontend. The current Course Authoring micro-frontend has no +authoring frontend. The current Authoring micro-frontend has no defined location to display aside author UI. If your project depends on the new MFE for authoring, plan to render the aside's author UI through a different mechanism, or accept that authors will use the legacy Studio for @@ -237,12 +213,12 @@ Documentation Has Historically Been Sparse ========================================== XBlock Asides have been part of the platform for years but have had no -user-facing documentation until this set of articles. The original work was -done by Dave Ormsbee. Most of the institutional knowledge has lived in -docstrings, test code, and the implementations of a handful of asides -maintained outside the core platform. If you find this documentation lacks -detail your project needs, the test file at ``xblock/test/test_asides.py`` -in the XBlock repository is the most reliable source of behavioral truth. +user-facing documentation until this set of articles. Most of the institutional +knowledge has lived in docstrings, test code, and the implementations of a +handful of asides maintained outside the core platform. If you find this +documentation lacks detail your project needs, the test file at +``xblock/test/test_asides.py`` in the XBlock repository is the most reliable +source of behavioral truth. Where to Go Next **************** @@ -253,7 +229,7 @@ and want a step-by-step recipe, read :ref:`Add an XBlock Aside`. For the complete list of classes, decorators, methods, and entry points, consult :ref:`XBlock Asides Reference`. -.. _rapid-response-xblock: https://github.com/mitodl/rapid-response-xblock +.. _rapid-response-xblock: https://github.com/mitodl/open-edx-plugins/tree/main/src/rapid_response_xblock .. _ol-openedx-chat: https://github.com/mitodl/open-edx-plugins/tree/main/src/ol_openedx_chat .. _xblock-sdk: https://github.com/openedx/xblock-sdk diff --git a/source/developers/how-tos/add-an-xblock-aside.rst b/source/developers/how-tos/add-an-xblock-aside.rst index 3819dc9d2..0bd281b65 100644 --- a/source/developers/how-tos/add-an-xblock-aside.rst +++ b/source/developers/how-tos/add-an-xblock-aside.rst @@ -6,7 +6,7 @@ Add an XBlock Aside .. tags:: developer, how-to -Add an XBlock aside to attach behavior, UI, or stored data to one or more +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. @@ -227,16 +227,6 @@ Tutor: tutor mounts add /path/to/feedback_badge_aside tutor dev launch -Or, if you are managing the environment manually: - -.. code-block:: bash - - pip install -e /path/to/feedback_badge_aside - -After installing, restart both the LMS and Studio. Asides are discovered -at process start, so newly installed asides do not appear until the -services restart. - Step 8: Enable asides in the LMS ******************************** @@ -293,7 +283,6 @@ If the aside does not appear: #. Check that ``should_apply_to_block`` returns ``True`` for the block you are testing. -#. Check that the aside is allowlisted for the course. #. Check the LMS and Studio logs for any exceptions raised inside your aside view. diff --git a/source/developers/quickstarts/quickstart_xblock_aside.rst b/source/developers/quickstarts/quickstart_xblock_aside.rst index f3ad8ab48..b83352634 100644 --- a/source/developers/quickstarts/quickstart_xblock_aside.rst +++ b/source/developers/quickstarts/quickstart_xblock_aside.rst @@ -186,9 +186,6 @@ If the banner does not appear, work through these checks in order: The list should include ``hello_aside``. If it does not, the entry point is not installed; recheck Step 3 and Step 4. -#. **The aside is allowlisted.** Confirm Step 5 was applied to the - course you are viewing. - #. **The block type matches.** Your test block must have ``category == "problem"``. A Video block, an HTML block, or a Discussion block will not trigger the aside. From 80f0184df81d8931767df421559dcaa4e48ae227 Mon Sep 17 00:00:00 2001 From: sarina Date: Sun, 3 May 2026 21:52:54 -0400 Subject: [PATCH 3/4] temp: Remove note about sparse documentation --- source/developers/concepts/about_xblock_asides.rst | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/source/developers/concepts/about_xblock_asides.rst b/source/developers/concepts/about_xblock_asides.rst index 1dc4e25b3..54f8a5b65 100644 --- a/source/developers/concepts/about_xblock_asides.rst +++ b/source/developers/concepts/about_xblock_asides.rst @@ -209,17 +209,6 @@ library that is not already loaded by the host page, you must add it through the fragment, and you must handle ordering and conflicts yourself. There is no shared aside-level mechanism for declaring library dependencies. -Documentation Has Historically Been Sparse -========================================== - -XBlock Asides have been part of the platform for years but have had no -user-facing documentation until this set of articles. Most of the institutional -knowledge has lived in docstrings, test code, and the implementations of a -handful of asides maintained outside the core platform. If you find this -documentation lacks detail your project needs, the test file at -``xblock/test/test_asides.py`` in the XBlock repository is the most reliable -source of behavioral truth. - Where to Go Next **************** From a0d95b0caa2cd8c2dd4a17ba6b0878b8249c1619 Mon Sep 17 00:00:00 2001 From: sarina Date: Sun, 3 May 2026 21:54:40 -0400 Subject: [PATCH 4/4] Consistently capitalize 'Aside' --- .../concepts/about_xblock_asides.rst | 72 ++++----- .../how-tos/add-an-xblock-aside.rst | 68 ++++----- .../quickstarts/quickstart_xblock_aside.rst | 64 ++++---- .../extending_platform/xblock_asides.rst | 140 +++++++++--------- 4 files changed, 172 insertions(+), 172 deletions(-) diff --git a/source/developers/concepts/about_xblock_asides.rst b/source/developers/concepts/about_xblock_asides.rst index 54f8a5b65..ca518da12 100644 --- a/source/developers/concepts/about_xblock_asides.rst +++ b/source/developers/concepts/about_xblock_asides.rst @@ -6,7 +6,7 @@ About XBlock Asides .. tags:: developer, concept -An XBlock aside is a class that injects content into the rendered views of +An XBlock Aside is a class that injects content into the rendered views of existing XBlocks without modifying those XBlocks. Asides let you add behavior, data, and UI elements to many XBlock instances at once, across XBlock types you do not own, while preserving the host XBlock's code, fields, and Open Learning @@ -19,15 +19,15 @@ XML (OLX) representation. What an Aside Is **************** -An aside is a Python class that subclasses :class:`~xblock.core.XBlockAside`, +An Aside is a Python class that subclasses :class:`~xblock.core.XBlockAside`, declares one or more view-injection methods using the :func:`~xblock.core.XBlockAside.aside_for` decorator, and is registered with the platform through a Python entry point in the ``xblock_asides.v1`` group. When the platform renders an XBlock view, the runtime collects every -applicable aside, invokes its matching aside view, and appends the resulting +applicable Aside, invokes its matching Aside view, and appends the resulting fragments to the host XBlock's rendered fragment. -An aside is **not** a child XBlock. It does not appear in the course outline, +An Aside is **not** a child XBlock. It does not appear in the course outline, it does not have its own URL, and it cannot be added to a course like a regular block. It exists only in relation to a host block, and its lifecycle is bound to that host block's lifecycle. @@ -42,7 +42,7 @@ you have three options: #. Fork the XBlock and modify it directly. #. Replace the XBlock with a new XBlock that wraps the original. -#. Attach an aside to the existing XBlock. +#. Attach an Aside to the existing XBlock. The first two options carry significant costs. Forking creates a parallel codebase that must be maintained against upstream changes. Replacing the @@ -51,11 +51,11 @@ it does not scale when you want to enhance many different XBlock types in the same way. Asides solve this by externalizing the enhancement. The host XBlock is not -modified. The same aside can apply to a Video block, a Problem block, or any +modified. The same Aside can apply to a Video block, a Problem block, or any other block type, by overriding a single classmethod. Asides can serialize their own scoped fields during course import and export. -Reach for an aside when all of the following are true: +Reach for an Aside when all of the following are true: * You want to enhance one or more existing XBlock types without forking them. * The enhancement is conceptually layered on top of the block, not a @@ -75,13 +75,13 @@ How an Aside Relates to Its Host Block ************************************** The runtime maintains a many-to-many relationship between asides and host -blocks at runtime, but each aside instance is bound to exactly one host block +blocks at runtime, but each Aside instance is bound to exactly one host block during a single render. The relationship is established in three stages. Per-Block Filtering =================== -For each candidate aside type, the runtime instantiates the aside and asks +For each candidate Aside type, the runtime instantiates the Aside and asks it whether it should apply to this specific block by calling its :meth:`~xblock.core.XBlockAside.should_apply_to_block` classmethod. The default implementation returns ``True``. Real-world asides almost always @@ -91,13 +91,13 @@ contexts, or feature flags. Rendering and Layout ==================== -For each aside that survives filtering, the runtime invokes the aside method +For each Aside that survives filtering, the runtime invokes the Aside method that was decorated with ``@XBlockAside.aside_for(view_name)`` for the view -being rendered. The aside method returns a ``Fragment``, the runtime wraps +being rendered. The Aside method returns a ``Fragment``, the runtime wraps that fragment with identifying markup, and the runtime appends the wrapped fragment to the host block's rendered output. A runtime can override :meth:`~xblock.runtime.Runtime.layout_asides` to control where and how the -aside fragments are placed. +Aside fragments are placed. Why Asides Are Worth the Trouble ******************************** @@ -109,10 +109,10 @@ remain useful, come from the production deployments that depend on them. Multiple Block Types, One Implementation ======================================== -A single aside class can decorate Video blocks, Problem blocks, and any +A single Aside class can decorate Video blocks, Problem blocks, and any other block type the author chooses, by checking ``block.category`` or ``block.scope_ids.block_type`` inside ``should_apply_to_block``. The MIT -Open Learning chat aside, for example, attaches an "AskTIM" chat button to +Open Learning chat Aside, for example, attaches an "AskTIM" chat button to both Video and Problem blocks from a single class, with one entry point. Without asides, the same outcome would require either two parallel forks or replacement blocks for both types. @@ -120,20 +120,20 @@ or replacement blocks for both types. Course Author Control ===================== -An aside can declare its own scoped fields, just like an XBlock. By exposing -those fields in an author view, an aside gives course authors a UI to enable +An Aside can declare its own scoped fields, just like an XBlock. By exposing +those fields in an author view, an Aside gives course authors a UI to enable or disable the enhancement on a per-block basis. The settings are stored -under the aside's own scope, not the host block's, so they are preserved +under the Aside's own scope, not the host block's, so they are preserved across exports and imports without any change to the host block's data model. OLX Export and Import ===================== -When a course is exported to OLX, the platform serializes each aside as an -XML child element under its host block, named after the aside's entry point +When a course is exported to OLX, the platform serializes each Aside as an +XML child element under its host block, named after the Aside's entry point name. On import, the runtime reconstitutes the asides automatically. This -means an aside-enhanced course is portable, with limitations described below. +means an Aside-enhanced course is portable, with limitations described below. Real-World Examples ******************* @@ -144,21 +144,21 @@ do. Rapid Response XBlock ===================== -The `rapid-response-xblock`_ from MIT Open Learning is a single aside that +The `rapid-response-xblock`_ from MIT Open Learning is a single Aside that applies to Problem blocks. It overlays an instructor-only control on the problem in the LMS that lets a live instructor open and close response windows during a lecture, and it renders a real-time chart of student responses. Course authors enable it per problem in Studio. The repository -name calls it an "xblock" but the implementation is purely an aside. +name calls it an "xblock" but the implementation is purely an Aside. Open Learning Chat Aside ======================== -The `ol-openedx-chat`_ aside, also from MIT Open Learning, attaches an +The `ol-openedx-chat`_ Aside, also from MIT Open Learning, attaches an "AskTIM" chat button to Video and Problem blocks. The button opens a context-aware chat drawer that streams messages to a backend large language model, passing block-specific context such as a video transcript identifier -or a problem's siblings. A single aside class, registered as one entry +or a problem's siblings. A single Aside class, registered as one entry point, handles both block types and uses ``should_apply_to_block`` to gate on a course-level waffle flag and per-course settings. @@ -169,15 +169,15 @@ Asides are a real, working feature in production deployments, but the ecosystem around them is incomplete. The list below is drawn from the state of the codebase as of the Sumac release and from a 2025 Open edX Conference talk by Peter Pinch of MIT Open Learning. Read it before -committing to an aside-based design. +committing to an Aside-based design. No Authoring Story in the Course Authoring MFE ============================================== -The Studio author view for an aside is rendered by the legacy course +The Studio author view for an Aside is rendered by the legacy course authoring frontend. The current Authoring micro-frontend has no -defined location to display aside author UI. If your project depends on the -new MFE for authoring, plan to render the aside's author UI through a +defined location to display Aside author UI. If your project depends on the +new MFE for authoring, plan to render the Aside's author UI through a different mechanism, or accept that authors will use the legacy Studio for this part of the workflow. @@ -186,33 +186,33 @@ Not All XBlocks Round-Trip Through OLX OLX export and import for asides depends on the host XBlock cooperating with the export process. Some XBlocks, including ORA2, do not preserve -aside data through their export and import paths. If your aside must +Aside data through their export and import paths. If your Aside must survive a course export and re-import on a course that uses one of these blocks, test the round trip end to end before depending on it. Multiple Asides on a Single Block Are Not Reliable ================================================== -The runtime supports multiple aside types decorating the same block in +The runtime supports multiple Aside types decorating the same block in principle, but interactions between asides on the same block are not well-tested. Two asides that both decorate ``student_view`` on the same block may render correctly in isolation and break when combined. If you -need this, build a single aside that composes both behaviors rather than +need this, build a single Aside that composes both behaviors rather than relying on two independent asides to coexist. JavaScript Library Loading Is Limited ===================================== Asides use the same fragment-based JavaScript loading mechanism as XBlocks, -which assumes a single set of static assets. If your aside needs a JS +which assumes a single set of static assets. If your Aside needs a JS library that is not already loaded by the host page, you must add it through the fragment, and you must handle ordering and conflicts yourself. -There is no shared aside-level mechanism for declaring library dependencies. +There is no shared Aside-level mechanism for declaring library dependencies. Where to Go Next **************** -If you are ready to build an aside, start with +If you are ready to build an Aside, start with :ref:`XBlock Aside Quickstart`. If you already have a target XBlock in mind and want a step-by-step recipe, read :ref:`Add an XBlock Aside`. For the complete list of classes, decorators, methods, and entry points, consult @@ -228,10 +228,10 @@ complete list of classes, decorators, methods, and entry points, consult The complete API surface for ``XBlockAside`` and its runtime hooks. :ref:`Add an XBlock Aside` (how-to) - A step-by-step recipe for adding an aside to existing XBlocks. + A step-by-step recipe for adding an Aside to existing XBlocks. :ref:`XBlock Aside Quickstart` (quickstart) - A beginner-friendly walkthrough from zero to a running aside. + A beginner-friendly walkthrough from zero to a running Aside. :ref:`Hooks Extension Framework` (concept) An alternative extension mechanism for non-view-based behaviors. diff --git a/source/developers/how-tos/add-an-xblock-aside.rst b/source/developers/how-tos/add-an-xblock-aside.rst index 0bd281b65..79ac0ec8e 100644 --- a/source/developers/how-tos/add-an-xblock-aside.rst +++ b/source/developers/how-tos/add-an-xblock-aside.rst @@ -32,7 +32,7 @@ Before you start, make sure you have: :class:`~web_fragments.fragment.Fragment`. If you have never written one, complete :ref:`XBlock Aside Quickstart` first. -This recipe builds a feedback-badge aside that adds a "Report an issue" +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. @@ -40,7 +40,7 @@ needed. Step 1: Scaffold a Python package ********************************* -Create a new directory for the aside package, with the layout below: +Create a new directory for the Aside package, with the layout below: .. code-block:: text @@ -48,11 +48,11 @@ Create a new directory for the aside package, with the layout below: ├── pyproject.toml ├── feedback_badge_aside/ │ ├── __init__.py - │ └── aside.py + │ └── Aside.py └── README.rst The package name (``feedback_badge_aside``) and the module name -(``aside.py``) are conventions; pick names that describe your aside. +(``Aside.py``) are conventions; pick names that describe your Aside. Populate ``pyproject.toml`` with the package metadata and a placeholder for the entry point you will add in :ref:`Step 6 `. @@ -60,9 +60,9 @@ for the entry point you will add in :ref:`Step 6 `. .. code-block:: toml [project] - name = "feedback-badge-aside" + name = "feedback-badge-Aside" version = "0.1.0" - description = "An XBlock aside that adds a feedback link to Problem and Video blocks." + 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", @@ -73,10 +73,10 @@ for the entry point you will add in :ref:`Step 6 `. requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" -Step 2: Define the aside class +Step 2: Define the Aside class ****************************** -In ``feedback_badge_aside/aside.py``, define a subclass of +In ``feedback_badge_aside/Aside.py``, define a subclass of :class:`~xblock.core.XBlockAside`. .. code-block:: python @@ -94,7 +94,7 @@ Step 3: Declare fields for course-author control ************************************************ Add a Boolean field that course authors can toggle to enable or disable -the aside on a per-block basis. Scope the field to ``Scope.settings``, +the Aside on a per-block basis. Scope the field to ``Scope.settings``, which means the value is stored per block and travels with the course in OLX export and import. @@ -169,19 +169,19 @@ above keep the example readable. Step 5: Filter to specific block types ************************************** -By default, an aside applies to every block. Override +By default, an Aside applies to every block. Override :meth:`~xblock.core.XBlockAside.should_apply_to_block` to restrict the -aside to the block types you support. +Aside to the block types you support. .. code-block:: python @classmethod def should_apply_to_block(cls, block): - """Apply this aside to Problem and Video blocks only.""" + """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 +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. @@ -199,22 +199,22 @@ the import and export paths where these may not be available; see .. _register entry point: -Step 6: Register the aside as an entry point +Step 6: 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 +the Aside's type name and is used as the XML tag during OLX serialization. .. code-block:: toml [project.entry-points."xblock_asides.v1"] - feedback_badge = "feedback_badge_aside.aside:FeedbackBadgeAside" + 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. +it later breaks OLX round-trips of any course that has used the Aside. Step 7: Install the package and restart services ************************************************ @@ -230,10 +230,10 @@ Tutor: Step 8: Enable asides in the LMS ******************************** -The edx-platform LMS gates aside rendering on a Django configuration +The edx-platform LMS gates Aside rendering on a Django configuration model, ``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 +revision, no Aside renders in the LMS regardless of installation or registration. Open the LMS Django admin and create a new configuration revision: @@ -248,21 +248,21 @@ 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 +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 +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 5. The Studio runtime does not consult this configuration. Asides render in Studio author views independently of ``XBlockAsidesConfig``, as soon as they are installed and registered. -Step 9: Verify the aside is rendering +Step 9: Verify the Aside is rendering ************************************* Open a course that contains a Problem or Video block, view it as a @@ -270,7 +270,7 @@ learner, and confirm the feedback link appears at the bottom of the block. To verify the author-side UI, open the same block in Studio and confirm the toggle appears in the studio view. -If the aside does not appear: +If the Aside does not appear: #. Check that the entry point is registered. Run: @@ -279,17 +279,17 @@ If the aside does not appear: from xblock.core import XBlockAside print(list(XBlockAside.load_classes())) - in a Django shell. Your aside's type name should be in the list. + in a Django shell. Your Aside's type name should be in the list. #. Check that ``should_apply_to_block`` returns ``True`` for the block you are testing. #. Check the LMS and Studio logs for any exceptions raised inside your - aside view. + Aside view. Next Steps ********** -Once the basic aside is working, common follow-ups include: +Once the basic Aside is working, common follow-ups include: * **Add an AJAX handler.** Decorate a method with ``@XBlock.handler`` and call it from the rendered fragment with @@ -297,13 +297,13 @@ Once the basic aside is working, common follow-ups include: * **Render from templates.** Use the runtime's template service to render HTML from ``.html`` files in your package's static assets. * **Persist user-specific state.** Add fields with - ``Scope.user_state`` to store per-learner data alongside the aside. -* **Customize layout.** If you need the aside to render somewhere other + ``Scope.user_state`` to store per-learner data alongside the Aside. +* **Customize layout.** If you need the Aside to render somewhere other than after the host block, override the runtime's ``layout_asides`` in your platform integration. For the complete API surface, see :ref:`XBlock Asides Reference`. For -the conceptual background, including known limitations of the aside +the conceptual background, including known limitations of the Aside mechanism, see :ref:`About XBlock Asides`. .. seealso:: @@ -315,7 +315,7 @@ mechanism, see :ref:`About XBlock Asides`. The complete API surface for ``XBlockAside`` and its runtime hooks. :ref:`XBlock Aside Quickstart` (quickstart) - A beginner-friendly walkthrough from zero to a running aside. + A beginner-friendly walkthrough from zero to a running Aside. **Maintenance chart** diff --git a/source/developers/quickstarts/quickstart_xblock_aside.rst b/source/developers/quickstarts/quickstart_xblock_aside.rst index b83352634..3ae66b2ca 100644 --- a/source/developers/quickstarts/quickstart_xblock_aside.rst +++ b/source/developers/quickstarts/quickstart_xblock_aside.rst @@ -6,13 +6,13 @@ Quickstart: Build Your First XBlock Aside .. tags:: developer, quickstart -Build, install, and run a minimal XBlock aside in a Tutor development -environment. By the end of this quickstart, you will have an aside that +Build, install, and run a minimal XBlock Aside in a Tutor development +environment. By the end of this quickstart, you will have an Aside that adds a small banner to every Problem block in a course, and you will -understand the four moving parts every aside has: the class, the +understand the four moving parts every Aside has: the class, the decorated views, the entry point, and the install step. -This quickstart deliberately keeps the aside trivial. For a more +This quickstart deliberately keeps the Aside trivial. For a more realistic recipe, work through :ref:`Add an XBlock Aside` next. For the conceptual background, see :ref:`About XBlock Asides`. @@ -35,7 +35,7 @@ You need: You do **not** need: -* Prior experience writing XBlocks. The aside in this quickstart uses +* Prior experience writing XBlocks. The Aside in this quickstart uses plain Python and a hardcoded HTML string. * Familiarity with the Open Learning XML (OLX) format. @@ -51,15 +51,15 @@ structure: ├── pyproject.toml └── hello_aside/ ├── __init__.py - └── aside.py + └── Aside.py Make ``__init__.py`` an empty file. The remaining steps describe what to put in the other two files. -Step 2: Write the aside class +Step 2: Write the Aside class ***************************** -In ``hello_aside/aside.py``, paste the following code: +In ``hello_aside/Aside.py``, paste the following code: .. code-block:: python @@ -68,13 +68,13 @@ In ``hello_aside/aside.py``, paste the following code: class HelloAside(XBlockAside): - """A trivial aside that prints a banner above every Problem block.""" + """A trivial Aside that prints a banner above every Problem block.""" @XBlockAside.aside_for("student_view") def student_view_aside(self, block, context=None): return Fragment( '
' - 'Hello from an XBlock aside!' + 'Hello from an XBlock Aside!' '
' ) @@ -89,7 +89,7 @@ This is the entire implementation. Three things to notice: ``student_view_aside`` as the method to call when an XBlock's ``student_view`` is being rendered. * :meth:`~xblock.core.XBlockAside.should_apply_to_block` restricts the - aside to Problem blocks. Without it, the banner would appear on every + Aside to Problem blocks. Without it, the banner would appear on every block in every course. Step 3: Configure the package @@ -100,13 +100,13 @@ In ``hello_aside/pyproject.toml``, paste: .. code-block:: toml [project] - name = "hello-aside" + name = "hello-Aside" version = "0.1.0" requires-python = ">=X.Y" # set to the minimum Python version for the target Open edX release dependencies = ["XBlock", "web-fragments"] [project.entry-points."xblock_asides.v1"] - hello_aside = "hello_aside.aside:HelloAside" + hello_aside = "hello_aside.Aside:HelloAside" [build-system] requires = ["setuptools>=61.0"] @@ -114,9 +114,9 @@ In ``hello_aside/pyproject.toml``, paste: The critical line is the one under ``[project.entry-points."xblock_asides.v1"]``. It tells the Open edX -platform that a class called ``HelloAside``, in the ``hello_aside.aside`` -module, should be loaded as an aside under the type name -``hello_aside``. This entry point group is how every aside is +platform that a class called ``HelloAside``, in the ``hello_aside.Aside`` +module, should be loaded as an Aside under the type name +``hello_aside``. This entry point group is how every Aside is discovered. Step 4: Install the package into Tutor @@ -133,17 +133,17 @@ Tutor and relaunch the development environment: The ``mounts add`` command tells Tutor to install the local package into both the LMS and Studio containers each time they start. The ``dev launch`` command rebuilds and restarts the containers so the new -aside is picked up. +Aside is picked up. If you are not using Tutor, install the package directly into the LMS and Studio Python environments with ``pip install -e ./hello_aside``, then restart both services. -Step 5: Enable the aside system in the LMS +Step 5: Enable the Aside system in the LMS ****************************************** -The LMS gates aside rendering on a global Django configuration model, -``XBlockAsidesConfig``. Until that model is enabled, no aside renders on +The LMS gates Aside rendering on a global Django configuration model, +``XBlockAsidesConfig``. Until that model is enabled, no Aside renders on any block, regardless of whether it is registered or installed. Open the LMS Django admin in your browser: @@ -166,17 +166,17 @@ The Studio runtime does not consult this model. Asides will render in Studio author views as soon as they are installed, independently of whether ``XBlockAsidesConfig`` is enabled. -Step 6: Verify the aside is rendering +Step 6: Verify the Aside is rendering ************************************* Open a course in the LMS, navigate to a unit that contains a Problem block, and confirm a light blue banner reading "Hello from an XBlock -aside!" appears below the problem. If you see the banner, your aside is +Aside!" appears below the problem. If you see the banner, your Aside is working. If the banner does not appear, work through these checks in order: -#. **The aside is registered.** Open a Django shell and run: +#. **The Aside is registered.** Open a Django shell and run: .. code-block:: python @@ -188,17 +188,17 @@ If the banner does not appear, work through these checks in order: #. **The block type matches.** Your test block must have ``category == "problem"``. A Video block, an HTML block, or a - Discussion block will not trigger the aside. + Discussion block will not trigger the Aside. #. **No exceptions are being swallowed.** Check the LMS logs for any exception raised inside ``student_view_aside`` or - ``should_apply_to_block``. The runtime catches some aside exceptions - silently, which can make a broken aside look like a missing one. + ``should_apply_to_block``. The runtime catches some Aside exceptions + silently, which can make a broken Aside look like a missing one. What You Just Built ******************* -You have a working aside with all four required pieces: +You have a working Aside with all four required pieces: A class ``HelloAside`` subclasses ``XBlockAside``. @@ -209,13 +209,13 @@ A decorated view whenever the runtime renders any block's ``student_view``. A filter - ``should_apply_to_block`` restricts the aside to Problem blocks. + ``should_apply_to_block`` restricts the Aside to Problem blocks. An entry point The ``xblock_asides.v1`` entry point in ``pyproject.toml`` makes the - aside discoverable by the platform at startup. + Aside discoverable by the platform at startup. -Every production aside has the same four pieces, plus additional +Every production Aside has the same four pieces, plus additional features such as scoped fields, AJAX handlers, author-side UI, and template-rendered HTML. @@ -239,7 +239,7 @@ To turn this trivial example into something useful: For each of these extensions, the :ref:`XBlock Asides Reference` is the authoritative source. For the trade-offs and current limitations of the -aside mechanism, read :ref:`About XBlock Asides` before scaling up your +Aside mechanism, read :ref:`About XBlock Asides` before scaling up your design. .. _Getting started: https://docs.tutor.edly.io/quickstart.html @@ -250,7 +250,7 @@ design. Why asides exist, what problem they solve, and current limitations. :ref:`Add an XBlock Aside` (how-to) - A step-by-step recipe for adding an aside to existing XBlocks. + A step-by-step recipe for adding an Aside to existing XBlocks. :ref:`XBlock Asides Reference` (reference) The complete API surface for ``XBlockAside`` and its runtime hooks. diff --git a/source/developers/references/developer_guide/extending_platform/xblock_asides.rst b/source/developers/references/developer_guide/extending_platform/xblock_asides.rst index a2c6f3cae..7e3ce0b74 100644 --- a/source/developers/references/developer_guide/extending_platform/xblock_asides.rst +++ b/source/developers/references/developer_guide/extending_platform/xblock_asides.rst @@ -9,7 +9,7 @@ XBlock Asides Reference This reference describes the public API surface for XBlock Asides as defined in the ``xblock`` package. It covers the :class:`~xblock.core.XBlockAside` base class, the decorators and -classmethods that subclasses use, the runtime hooks that govern aside +classmethods that subclasses use, the runtime hooks that govern Aside discovery and rendering, the entry point group that registers asides as plugins, and the OLX serialization contract. @@ -25,7 +25,7 @@ The XBlockAside Class ********************* The base class for all asides is ``xblock.core.XBlockAside``. It inherits -from ``Plugin`` and ``Blocklike``, so an aside can declare scoped fields, +from ``Plugin`` and ``Blocklike``, so an Aside can declare scoped fields, mark methods as handlers, and be loaded as a plugin in the same way as an XBlock. @@ -34,17 +34,17 @@ XBlock. from xblock.core import XBlockAside class MyAside(XBlockAside): - """An aside that decorates one or more XBlock views.""" + """An Aside that decorates one or more XBlock views.""" Class Attributes ================ ``entry_point`` - The Python entry point group used to discover aside plugins. Set on the + The Python entry point group used to discover Aside plugins. Set on the base class to ``"xblock_asides.v1"``. Subclasses should not change this. ``fields`` - The set of fields declared on the aside, automatically collected from + The set of fields declared on the Aside, automatically collected from class-level :class:`~xblock.fields.Field` declarations. Inherited from ``Blocklike``. @@ -54,9 +54,9 @@ Decorators ``XBlockAside.aside_for(view_name)`` ==================================== -A classmethod decorator that marks a method as the aside view for the +A classmethod decorator that marks a method as the Aside view for the named XBlock view. When the runtime renders an XBlock view called -``view_name``, every applicable aside whose class declares a method +``view_name``, every applicable Aside whose class declares a method decorated with ``@XBlockAside.aside_for(view_name)`` is invoked, and the returned fragments are appended to the host block's rendered fragment. @@ -71,17 +71,17 @@ returned fragments are appended to the host block's rendered fragment. The decorated method takes: -* ``self`` — the aside instance. +* ``self`` — the Aside instance. * ``block`` — the host XBlock instance being rendered. * ``context`` — an optional dictionary of context data passed by the caller of ``render``. The method must return a :class:`~web_fragments.fragment.Fragment`. -**Multiple views per aside** +**Multiple views per Aside** A single method may decorate multiple views by stacking decorators, and a -single aside class may define different methods for different views. +single Aside class may define different methods for different views. .. code-block:: python @@ -89,12 +89,12 @@ single aside class may define different methods for different views. @XBlockAside.aside_for("student_view") def student_view_aside(self, block, context=None): - return Fragment("
Student-side aside
") + return Fragment("
Student-side Aside
") @XBlockAside.aside_for("author_view") @XBlockAside.aside_for("studio_view") def studio_aside(self, block, context=None): - return Fragment("
Author/Studio aside
") + return Fragment("
Author/Studio Aside
") **Common view names** @@ -110,7 +110,7 @@ The view names commonly decorated by asides include: | ``studio_view`` | The author-facing edit form of a block in Studio. | +----------------------+---------------------------------------------------+ -XBlocks may define additional views; an aside can decorate any of them by +XBlocks may define additional views; an Aside can decorate any of them by name. Classmethods @@ -119,13 +119,13 @@ Classmethods ``XBlockAside.should_apply_to_block(cls, block)`` ================================================= -A classmethod that returns ``True`` if the aside should apply to the given +A classmethod that returns ``True`` if the Aside should apply to the given ``block``, and ``False`` otherwise. The default implementation returns ``True`` unconditionally. -Override this method to restrict the aside to specific block types, +Override this method to restrict the Aside to specific block types, courses, or feature flags. The runtime calls this method on every -aside-block pair before rendering, and asides for which it returns +Aside-block pair before rendering, and asides for which it returns ``False`` are skipped. .. code-block:: python @@ -146,32 +146,32 @@ Instance Methods ``aside_view_declaration(view_name)`` ===================================== -Return the bound method on this aside instance that is decorated with +Return the bound method on this Aside instance that is decorated with ``@XBlockAside.aside_for(view_name)``, or ``None`` if no such method -exists. The runtime uses this method to look up the aside's view function +exists. The runtime uses this method to look up the Aside's view function when rendering. Subclasses do not normally need to call it directly. ``needs_serialization()`` ========================= -Return ``True`` if the aside has any field whose value differs from its -default. The default implementation iterates over the aside's fields and +Return ``True`` if the Aside has any field whose value differs from its +default. The default implementation iterates over the Aside's fields and returns ``True`` if any field is set on this instance. The runtime calls -this method during OLX export to decide whether to serialize the aside as +this method during OLX export to decide whether to serialize the Aside as an XML element. Subclasses rarely need to override this. ``parse_xml(node, runtime, keys)`` and ``add_xml_to_node(node)`` ================================================================ -Inherited from ``Blocklike``. Override these to customize how the aside is +Inherited from ``Blocklike``. Override these to customize how the Aside is read from and written to OLX. The default implementations serialize the -aside's fields as XML attributes and child elements, identical to the +Aside's fields as XML attributes and child elements, identical to the default XBlock behavior. Fields and Scopes ***************** -An aside declares fields the same way an XBlock does, with class-level +An Aside declares fields the same way an XBlock does, with class-level field declarations. Fields are scoped, and the scope determines where the field's value is stored and which entities share it. @@ -185,7 +185,7 @@ field's value is stored and which entities share it. display_name="Enabled", default=False, scope=Scope.settings, - help="Whether this aside is active for this block.", + help="Whether this Aside is active for this block.", ) last_message = String( @@ -197,16 +197,16 @@ field's value is stored and which entities share it. The supported scopes are the standard XBlock scopes from :mod:`xblock.fields`: ``Scope.content``, ``Scope.settings``, ``Scope.user_state``, ``Scope.user_state_summary``, -``Scope.preferences``, and ``Scope.user_info``. An aside's field values -are stored under the aside's own usage ID, separate from the host block's +``Scope.preferences``, and ``Scope.user_info``. An Aside's field values +are stored under the Aside's own usage ID, separate from the host block's field values. Handlers ******** -An aside can define AJAX handlers using the standard +An Aside can define AJAX handlers using the standard ``@XBlock.handler`` decorator from ``xblock.core``. The handler URL is -generated by the runtime in the same way as for an XBlock, so an aside's +generated by the runtime in the same way as for an XBlock, so an Aside's view fragment can call its own handler with ``self.runtime.handler_url(self, "handler_name")``. @@ -227,14 +227,14 @@ identical to XBlock handlers. Entry Point Registration ************************ -An aside is discovered by the runtime through a Python entry point in the +An Aside is discovered by the runtime through a Python entry point in the ``xblock_asides.v1`` group. Declare the entry point in the package's ``pyproject.toml``: .. code-block:: toml [project.entry-points."xblock_asides.v1"] - my_aside = "my_package.aside:MyAside" + my_aside = "my_package.Aside:MyAside" Or in ``setup.py``: @@ -243,14 +243,14 @@ Or in ``setup.py``: setup( entry_points={ "xblock_asides.v1": [ - "my_aside = my_package.aside:MyAside", + "my_aside = my_package.Aside:MyAside", ], }, ) -The entry point name on the left of the equals sign is the aside's -**type name**. It is used as the XML tag when the aside is serialized to -OLX, and as the key in :class:`~xblock.scopes.ScopeIds` when an aside +The entry point name on the left of the equals sign is the Aside's +**type name**. It is used as the XML tag when the Aside is serialized to +OLX, and as the key in :class:`~xblock.scopes.ScopeIds` when an Aside instance is constructed. Choose a name that is unique across all installed asides on a deployment. @@ -258,23 +258,23 @@ Runtime API *********** The methods listed below are defined on -:class:`~xblock.runtime.Runtime` and govern aside discovery, instantiation, +:class:`~xblock.runtime.Runtime` and govern Aside discovery, instantiation, and rendering. Aside subclasses do not normally call these directly. They -are documented here so runtime implementors and aside authors can +are documented here so runtime implementors and Aside authors can understand the lifecycle. Discovery ========= ``runtime.applicable_aside_types(block)`` - Return the list of aside type names that may apply to ``block``. The - default implementation returns every aside class registered through + Return the list of Aside type names that may apply to ``block``. The + default implementation returns every Aside class registered through the ``xblock_asides.v1`` entry point. A runtime may override this to filter by user, course, or other context. The edx-platform LMS runtime overrides this through ``lms_applicable_aside_types`` in - ``lms/djangoapps/lms_xblock/runtime.py``, which gates aside rendering + ``lms/djangoapps/lms_xblock/runtime.py``, which gates Aside rendering on the ``XBlockAsidesConfig`` Django configuration model. When the model's current revision has ``enabled=False``, no asides render in the LMS. When enabled, asides do not render on block types listed in @@ -285,32 +285,32 @@ Discovery ``runtime.load_aside_type(aside_type)`` Return the :class:`XBlockAside` subclass corresponding to the given - ``aside_type`` string. Raises if no aside is registered under that + ``aside_type`` string. Raises if no Aside is registered under that name. Instantiation ============= ``runtime.create_aside(block_type, keys)`` - Construct an aside instance of the named ``block_type`` (the aside + Construct an Aside instance of the named ``block_type`` (the Aside type name, despite the parameter name) with the given ``ScopeIds``. Returns an :class:`XBlockAside` instance. ``runtime.get_aside_of_type(block, aside_type)`` - Construct an aside of the named type that is bound to the given host - ``block``. Generates the aside's definition and usage IDs from the + Construct an Aside of the named type that is bound to the given host + ``block``. Generates the Aside's definition and usage IDs from the host block's IDs using the runtime's ``id_generator``. Returns an :class:`XBlockAside` instance. ``runtime.get_aside(aside_usage_id)`` - Construct an aside instance from a previously known aside usage ID. - Used during OLX import and other paths where the aside's identity is + Construct an Aside instance from a previously known Aside usage ID. + Used during OLX import and other paths where the Aside's identity is already established. ``runtime.get_asides(block)`` - Return the list of aside instances that should decorate the given + Return the list of Aside instances that should decorate the given ``block``. This method composes ``applicable_aside_types`` and each - aside's ``should_apply_to_block`` filter, returning only the asides + Aside's ``should_apply_to_block`` filter, returning only the asides for which both pass. Rendering @@ -319,20 +319,20 @@ Rendering ``runtime.render_asides(block, view_name, frag, context)`` Called by the runtime's ``render`` method after the block's own view has produced its fragment. Iterates over ``get_asides(block)``, looks - up each aside's view declaration for ``view_name``, and dispatches + up each Aside's view declaration for ``view_name``, and dispatches layout to ``layout_asides``. Returns the augmented :class:`Fragment`. ``runtime.layout_asides(block, context, frag, view_name, aside_frag_fns)`` - Execute the aside view functions and combine their fragments with the - block's fragment. The default implementation appends each aside's + Execute the Aside view functions and combine their fragments with the + block's fragment. The default implementation appends each Aside's wrapped fragment after the block's fragment. Override this to control the placement, ordering, or conditional inclusion of asides. Any - override must call ``wrap_aside`` around each aside fragment to + override must call ``wrap_aside`` around each Aside fragment to preserve client-side identification. -``runtime.wrap_aside(block, aside, view, frag, context)`` - Wrap an aside's fragment with a ``
`` carrying the aside's usage +``runtime.wrap_aside(block, Aside, view, frag, context)`` + Wrap an Aside's fragment with a ``
`` carrying the Aside's usage ID, the host block's usage ID, and any JavaScript initialization metadata. Override this if you need a different wrapping element or different ``data-`` attributes. @@ -340,24 +340,24 @@ Rendering OLX Serialization ***************** -When a course is exported to OLX, the runtime serializes every aside +When a course is exported to OLX, the runtime serializes every Aside that returns ``True`` from ``needs_serialization()`` as an XML child -element of its host block. The XML tag of the aside element is the -aside's entry point name. Field values are written as attributes or -nested elements according to the aside's ``add_xml_to_node`` +element of its host block. The XML tag of the Aside element is the +Aside's entry point name. Field values are written as attributes or +nested elements according to the Aside's ``add_xml_to_node`` implementation. -On import, the runtime detects aside elements by looking up their tag +On import, the runtime detects Aside elements by looking up their tag names in the registered ``xblock_asides.v1`` entry points. Tag names -that do not resolve to a registered aside are ignored. Field values are -then read by the aside's ``parse_xml`` implementation. +that do not resolve to a registered Aside are ignored. Field values are +then read by the Aside's ``parse_xml`` implementation. Two practical consequences: -* An aside's data only round-trips through OLX if both the source and - destination platforms have the same aside installed under the same +* An Aside's data only round-trips through OLX if both the source and + destination platforms have the same Aside installed under the same entry point name. -* Some XBlocks do not preserve aside child elements through their own +* Some XBlocks do not preserve Aside child elements through their own export and import paths. See :ref:`About XBlock Asides` for the current list of known issues. @@ -371,12 +371,12 @@ The full sequence of calls when a runtime renders an XBlock view is: #. The runtime calls ``runtime.wrap_xblock`` on that fragment. #. The runtime calls ``runtime.render_asides(block, view_name, frag, context)``. #. ``render_asides`` calls ``runtime.get_asides(block)``, which uses - ``applicable_aside_types(block)`` and each aside's + ``applicable_aside_types(block)`` and each Aside's ``should_apply_to_block(block)`` to compute the filtered list. -#. For each surviving aside, ``render_asides`` calls - ``aside.aside_view_declaration(view_name)`` to find the matching +#. For each surviving Aside, ``render_asides`` calls + ``Aside.aside_view_declaration(view_name)`` to find the matching method. -#. ``render_asides`` calls ``layout_asides``, which invokes each aside +#. ``render_asides`` calls ``layout_asides``, which invokes each Aside view function, calls ``wrap_aside`` on each result, and appends the wrapped fragments to the block's fragment. #. The combined fragment is returned to the original caller. @@ -387,10 +387,10 @@ The full sequence of calls when a runtime renders an XBlock view is: Why asides exist, what problem they solve, and current limitations. :ref:`Add an XBlock Aside` (how-to) - A step-by-step recipe for adding an aside to existing XBlocks. + A step-by-step recipe for adding an Aside to existing XBlocks. :ref:`XBlock Aside Quickstart` (quickstart) - A beginner-friendly walkthrough from zero to a running aside. + A beginner-friendly walkthrough from zero to a running Aside. **Maintenance chart**