Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/ol_openedx_feedback/README.rst
Comment thread
zamanafzal marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We need to add setup/installation steps.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you please add a step in installation guide to enable the asides from LMS admin?

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
ol-openedx-feedback
###################

An Open edX plugin that adds a per-block "Send feedback" trigger to applicable
leaf blocks in the LMS via an ``XBlockAside``. The trigger is shown only to
authenticated learners (never in Studio author/preview mode and never to
anonymous users).

When a learner clicks the trigger the aside posts an ``ol-feedback::drawer-open``
message to its parent window (the Learning MFE) using ``window.parent.postMessage``.
The message payload carries the block context needed to identify the content
being rated:

- ``courseId`` — the course key
- ``blockUsageKey`` — the block's usage key
- ``blockType`` — the XBlock category (e.g. ``problem``, ``video``)
- ``blockDisplayName`` — the block's display name

The Learning MFE receives the message and opens the feedback drawer, which
submits the learner's feedback directly to the **mit-learn** service. This
plugin does **not** persist anything in edx-platform and exposes no REST API.

Installation
============

Install the package into the LMS Python environment:

.. code-block:: bash

pip install ol-openedx-feedback

The plugin registers itself automatically through its entry points — the
``xblock_asides.v1`` aside plus the ``lms.djangoapp`` app config — so no changes
to ``INSTALLED_APPS`` are required. Restart the LMS after installing. (The
trigger is learner-facing only, so the plugin is LMS-only and is not installed
in Studio/CMS.)

Enable XBlock asides in the LMS admin
-------------------------------------

XBlock asides must be turned on for any aside (including this one) to render.
In the LMS Django admin, open **XBlock Asides Config**
(``/admin/lms_xblock/xblockasidesconfig/``), add a new entry, and check
**Enabled**. Make sure the block types you want the feedback trigger on are
**not** listed in **Disabled blocks** (the space-separated field defaults to
``about course_info static_tab``).

Enablement
==========

Feedback is gated by the ``ol_openedx_feedback.feedback_enabled`` course waffle
flag (default off). Enable it for the desired courses (or globally) to roll out.

Configuration
=============

By default the trigger renders on every leaf block and is suppressed only on
structural containers (``course`` / ``chapter`` / ``sequential`` / ``vertical``).
To additionally exclude one or more block types (for example ``html``), override
the excluded set through ``ENV_TOKENS`` (e.g. in ``lms.yml``):

.. code-block:: yaml

OL_OPENEDX_FEEDBACK_EXCLUDED_BLOCK_TYPES:
- course
- chapter
- sequential
- vertical
- html

The plugin reads this value via its ``settings.common`` ``plugin_settings`` hook
and exposes it as the ``OL_OPENEDX_FEEDBACK_EXCLUDED_BLOCK_TYPES`` Django
setting, defaulting to the structural set above when unset.
Empty file.
27 changes: 27 additions & 0 deletions src/ol_openedx_feedback/ol_openedx_feedback/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""ol_openedx_feedback Django application initialization."""

from django.apps import AppConfig
from edx_django_utils.plugins import PluginSettings
from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType


class OLOpenedxFeedbackConfig(AppConfig):
"""Configuration for the ol_openedx_feedback Django application.

Trigger-only plugin: the feedback trigger is registered via the
``xblock_asides.v1`` entry point (no URLs or models — feedback is persisted
in mit-learn and the MFE owns the submit URL). LMS-only, since the trigger
renders in the learner (student) view; the only Django settings are the
plugin defaults populated by ``settings.common``.
"""

name = "ol_openedx_feedback"
verbose_name = "Open edX Block Feedback"

plugin_app = {
PluginSettings.CONFIG: {
ProjectType.LMS: {
SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: "settings.common"},
},
},
}
93 changes: 93 additions & 0 deletions src/ol_openedx_feedback/ol_openedx_feedback/block.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""XBlockAside that renders the per-block feedback trigger."""

import logging

import pkg_resources
from django.conf import settings
from django.template import Context, Template
from django.utils.translation import gettext
from web_fragments.fragment import Fragment
from xblock.core import XBlockAside

try:
from xmodule.modulestore.xml import (
XMLImportingModuleStoreRuntime as XMLImportingModuleStoreRuntime, # noqa: PLC0414
)
except ImportError:
from xmodule.modulestore.xml import ImportSystem as XMLImportingModuleStoreRuntime

from xmodule.x_module import STUDENT_VIEW

from ol_openedx_feedback.compat import get_feedback_enabled_flag
from ol_openedx_feedback.utils import is_aside_applicable_to_block

log = logging.getLogger(__name__)


def _resource(path):
"""Return the decoded contents of a packaged resource."""
return pkg_resources.resource_string(__name__, path).decode("utf-8")


def _render(path, context):
"""Render a packaged Django template with the given context."""
return Template(_resource(path)).render(Context(context))


class FeedbackAside(XBlockAside):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can't disable feedback for a block from the authoring MFE, and also, we cannot disable it for a block type like text block, etc.

I think we should add the ability to disable feedback for a block from authoring and convert the constant to a setting to be able to disable it for a block type.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We intentionally left this out as per the direction on the parent issue. Feedback should be "enabled for all blocks"

"""Adds a small 'Send feedback' trigger to applicable blocks in the LMS."""

@XBlockAside.aside_for(STUDENT_VIEW)
def student_view_aside(self, block, context=None): # noqa: ARG002
"""Render the feedback trigger for authenticated learners only."""
fragment = Fragment("")

# Never render in Studio author/preview or for anonymous/preview users.
if getattr(self.runtime, "is_author_mode", False) or not getattr(
self.runtime, "user_id", None
):
return fragment

block_usage_key = block.usage_key
block_id = block_usage_key.block_id
block_type = getattr(block, "category", None)

fragment.add_content(
_render(
"static/html/student_view.html",
{
"block_id": block_id,
"label": gettext("Send feedback"),
},
)
)
fragment.add_css(_resource("static/css/feedback.css"))
fragment.add_javascript(_resource("static/js/feedback.js"))
fragment.initialize_js(
"FeedbackAsideInit",
json_args={
"block_id": block_id,
"learning_mfe_base_url": getattr(
settings, "LEARNING_MICROFRONTEND_URL", ""
),
"drawer_payload": {
"courseId": str(block_usage_key.course_key),
"blockUsageKey": str(block_usage_key),
"blockType": block_type,
"blockDisplayName": block.display_name or "",
},
},
)
return fragment

@classmethod
def should_apply_to_block(cls, block):
"""Apply to leaf blocks when the course waffle flag is enabled."""
# During course import the block lacks a resolvable course context, so
# skip the waffle-flag check (which reads the course context key) and
# gate on block type only.
if isinstance(block.runtime, XMLImportingModuleStoreRuntime):
return is_aside_applicable_to_block(block)
return get_feedback_enabled_flag().is_enabled(
block.scope_ids.usage_id.context_key
) and is_aside_applicable_to_block(block)
Comment thread
sentry[bot] marked this conversation as resolved.
20 changes: 20 additions & 0 deletions src/ol_openedx_feedback/ol_openedx_feedback/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Compatibility layer isolating core-platform imports."""

WAFFLE_FLAG_NAMESPACE = "ol_openedx_feedback"

# .. toggle_name: ol_openedx_feedback.feedback_enabled
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Enables the per-block feedback trigger for a course.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2026-06-09
OL_OPENEDX_FEEDBACK_ENABLED = "feedback_enabled"


def get_feedback_enabled_flag():
"""Return the CourseWaffleFlag controlling feedback rollout."""
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag # noqa: PLC0415

return CourseWaffleFlag(
f"{WAFFLE_FLAG_NAMESPACE}.{OL_OPENEDX_FEEDBACK_ENABLED}", __name__
)
7 changes: 7 additions & 0 deletions src/ol_openedx_feedback/ol_openedx_feedback/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Constants for ol_openedx_feedback."""

# Default structural/container blocks that never get a feedback trigger.
# Deployments can override this via the
# ``OL_OPENEDX_FEEDBACK_EXCLUDED_BLOCK_TYPES`` Django setting (e.g. to also
# exclude a content type like ``html``).
DEFAULT_EXCLUDED_BLOCK_TYPES = {"course", "chapter", "sequential", "vertical"}
18 changes: 18 additions & 0 deletions src/ol_openedx_feedback/ol_openedx_feedback/settings/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# noqa: INP001

"""Settings to provide to edX"""

from ol_openedx_feedback.constants import DEFAULT_EXCLUDED_BLOCK_TYPES


def plugin_settings(settings):
"""
Populate common settings
"""
env_tokens = getattr(settings, "ENV_TOKENS", {})
settings.OL_OPENEDX_FEEDBACK_EXCLUDED_BLOCK_TYPES = set(
env_tokens.get(
"OL_OPENEDX_FEEDBACK_EXCLUDED_BLOCK_TYPES",
DEFAULT_EXCLUDED_BLOCK_TYPES,
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
.ol-feedback-container {
display: flex;
justify-content: flex-end;
margin: 8px 0 4px;
}

.ol-feedback-container--relocated {
margin: 0;
}

.ol-feedback-anchor {
--ol-fb-brand: #8a1e2d;
position: relative;
display: inline-flex;
align-self: center;
}

.ol-feedback-anchor--docked {
/* Docked next to AskTIM: only positioning here (a gap before the AskTIM
button). The button appearance lives on .ol-feedback-trigger so the
megaphone always looks like a button, docked or standalone. */
margin-right: 10px;
z-index: 1;
}

/* Problem blocks draw a horizontal "saved/submit" notification line behind the
button row. Project an opaque white block across the gap to the AskTIM button
so the line is masked there too (invisible on the white problem-block
background). Not applied to video/html blocks, which have no such line.
Offset must match the docked gap (margin-right above) so the mask fills the
gap but stops at the AskTIM button's edge instead of overlapping its border. */
.ol-feedback-anchor--line-masked {
box-shadow: 10px 0 0 0 #fff;
}

.ol-feedback-trigger {
/* Always styled as a button (border + background), whether standalone or
docked next to the AskTIM button. */
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border: 1px solid #b8c2cc;
border-radius: 6px;
background: #fff;
color: #e0a106;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.ol-feedback-trigger:hover {
background: #fbf3dd;
color: #c98a00;
}

/* On click/focus the background fills the whole button and the border turns
red. */
.ol-feedback-trigger:active,
.ol-feedback-trigger:focus {
background: #e6e6e6;
border-color: var(--ol-fb-brand);
color: #c98a00;
outline: none;
}

.ol-feedback-trigger .ol-fb-ic {
width: 18px;
height: 18px;
display: block;
}

/* On video blocks the AskTIM chat container (.video-block-chat-button-container)
paints an opaque whitesmoke background plus an upward box-shadow inside its own
stacking context, which rides up over the platform's preceding "Staff Debug
Info" link (.wrap-instructor-info) and hides it for staff. Fix it here in the
feedback plugin so it holds regardless of the installed chat-plugin version.
The staff-info row is the adjacent sibling of the block's wrapper
(data-block-type="video"), so scope to video only: give the row its own
stacking context above the paint, and match the chat container's whitesmoke
background so it blends into the section instead of showing a white patch.
Problem/HTML rows are untouched. */
[data-block-type="video"] + .wrap-instructor-info {
position: relative;
z-index: 2;
background: whitesmoke;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="ol-feedback-container">
<span class="ol-feedback-anchor">
<button class="ol-feedback-trigger" id="ol-feedback-trigger-{{ block_id }}" type="button"
aria-haspopup="dialog" aria-label="{{ label }}" title="{{ label }}">
<svg class="ol-fb-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg>
</button>
</span>
</div>
Loading
Loading