feat: Add blockType to xblockPreview & title to xblock_iframe [FC-0097] (#37362)

- Adds `blockType` and `is_modified` to the `showXBlockLibraryChangesPreview` iframe message.
- Add title to the `xblock_iframe`
- Add `is-modified` to `studio_xblock_wrapper`
- Add `disable_staff_debug_info` as a query param in `render_xblock`
- `downstream_is_modified` added to ComponentLink and ContainerLink
This commit is contained in:
Chris Chávez
2025-09-29 18:27:32 -05:00
committed by GitHub
parent d34a6b9c6f
commit 51bfd3febe
11 changed files with 176 additions and 14 deletions

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.24 on 2025-09-23 19:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contentstore', '0012_componentlink_top_level_parent_and_more'),
]
operations = [
migrations.AddField(
model_name='componentlink',
name='downstream_is_modified',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='containerlink',
name='downstream_is_modified',
field=models.BooleanField(default=False),
),
]

View File

@@ -108,6 +108,7 @@ class EntityLinkBase(models.Model):
top_level_parent = models.ForeignKey("ContainerLink", on_delete=models.SET_NULL, null=True, blank=True)
version_synced = models.IntegerField()
version_declined = models.IntegerField(null=True, blank=True)
downstream_is_modified = models.BooleanField(default=False)
created = manual_date_time_field()
updated = manual_date_time_field()
@@ -257,6 +258,7 @@ class ComponentLink(EntityLinkBase):
version_synced: int,
top_level_parent_usage_key: UsageKey | None = None,
version_declined: int | None = None,
downstream_is_modified: bool = False,
created: datetime | None = None,
) -> "ComponentLink":
"""
@@ -281,6 +283,7 @@ class ComponentLink(EntityLinkBase):
'version_synced': version_synced,
'version_declined': version_declined,
'top_level_parent': top_level_parent,
'downstream_is_modified': downstream_is_modified,
}
if upstream_block:
new_values['upstream_block'] = upstream_block
@@ -482,6 +485,7 @@ class ContainerLink(EntityLinkBase):
version_synced: int,
top_level_parent_usage_key: UsageKey | None = None,
version_declined: int | None = None,
downstream_is_modified: bool = False,
created: datetime | None = None,
) -> "ContainerLink":
"""
@@ -506,6 +510,7 @@ class ContainerLink(EntityLinkBase):
'version_synced': version_synced,
'version_declined': version_declined,
'top_level_parent': top_level_parent,
'downstream_is_modified': downstream_is_modified,
}
if upstream_container_id:
new_values['upstream_container_id'] = upstream_container_id

View File

@@ -384,6 +384,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'updated': date_format,
'upstream_key': self.upstream_html1["id"],
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 2,
@@ -400,7 +401,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_problem1["id"],
'upstream_type': 'component'
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 3,
@@ -417,7 +419,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_problem2["id"],
'upstream_type': 'component'
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 1,
@@ -434,7 +437,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_unit["id"],
'upstream_type': 'container'
'upstream_type': 'container',
'downstream_is_modified': False,
}
]
data = downstreams.json()
@@ -533,6 +537,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'updated': date_format,
'upstream_key': self.upstream_html1["id"],
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 2,
@@ -549,7 +554,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_problem1["id"],
'upstream_type': 'component'
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 3,
@@ -566,7 +572,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_problem2["id"],
'upstream_type': 'component'
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 1,
@@ -583,7 +590,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_unit["id"],
'upstream_type': 'container'
'upstream_type': 'container',
'downstream_is_modified': False,
}
]
data = downstreams.json()
@@ -681,6 +689,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'updated': date_format,
'upstream_key': self.upstream_html1["id"],
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 2,
@@ -697,7 +706,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_problem1["id"],
'upstream_type': 'component'
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 4,
@@ -714,7 +724,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': upstream_problem3["id"],
'upstream_type': 'component'
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 1,
@@ -731,7 +742,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_unit["id"],
'upstream_type': 'container'
'upstream_type': 'container',
'downstream_is_modified': False,
}
]
data = downstreams.json()
@@ -810,6 +822,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'updated': date_format,
'upstream_key': self.upstream_html1["id"],
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 2,
@@ -826,7 +839,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_problem1["id"],
'upstream_type': 'component'
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 4,
@@ -843,7 +857,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': upstream_problem3["id"],
'upstream_type': 'component'
'upstream_type': 'component',
'downstream_is_modified': False,
},
{
'id': 1,
@@ -860,7 +875,8 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_unit["id"],
'upstream_type': 'container'
'upstream_type': 'container',
'downstream_is_modified': False,
}
]
data = downstreams.json()
@@ -1047,6 +1063,7 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
Test that we can sync a html from a library into a course.
"""
# 1⃣ First, create the html in the course, using the upstream problem as a template:
date_format = self.now.isoformat().split("+")[0] + 'Z'
downstream_html1 = self._create_block_from_upstream(
block_category="html",
parent_usage_key=str(self.course_subsection.usage_key),
@@ -1079,6 +1096,34 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
>This is the HTML.</html>
""")
# Check that: The downstream links are created as expected for the component
downstreams = self._get_downstream_links(
course_id=str(self.course.id)
)
expected_downstreams = [
{
'id': 1,
'upstream_context_title': self.library_title,
'upstream_version': 2,
'ready_to_sync': False,
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_html1["locator"],
'downstream_context_key': str(self.course.id),
'top_level_parent_usage_key': None,
'version_synced': 2,
'version_declined': None,
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_html1["id"],
'upstream_type': 'component',
'downstream_is_modified': False,
},
]
data = downstreams.json()
self.assertEqual(data["count"], 1)
self.assertListEqual(data["results"], expected_downstreams)
# 2⃣ Now, lets modify the upstream html AND the downstream display_name:
self._update_course_block_fields(downstream_html1["locator"], {
"display_name": "New Text Content",
@@ -1111,9 +1156,36 @@ class CourseToLibraryTestCase(ContentLibrariesRestApiTest, ModuleStoreTestCase):
'version_declined': None,
'ready_to_sync': True, # <--- updated
'error_message': None,
'is_modified': True,
'is_modified': True, # <--- updated
})
downstreams = self._get_downstream_links(
course_id=str(self.course.id)
)
expected_downstreams = [
{
'id': 1,
'upstream_context_title': self.library_title,
'upstream_version': 3, # <--- updated
'ready_to_sync': True, # <--- updated
'ready_to_sync_from_children': False,
'upstream_context_key': self.library_id,
'downstream_usage_key': downstream_html1["locator"],
'downstream_context_key': str(self.course.id),
'top_level_parent_usage_key': None,
'version_synced': 2,
'version_declined': None,
'created': date_format,
'updated': date_format,
'upstream_key': self.upstream_html1["id"],
'upstream_type': 'component',
'downstream_is_modified': True, # <--- updated
},
]
data = downstreams.json()
self.assertEqual(data["count"], 1)
self.assertListEqual(data["results"], expected_downstreams)
# 3⃣ Now, sync and check the resulting OLX of the downstream
self._sync_downstream(downstream_html1["locator"])

View File

@@ -675,6 +675,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -692,6 +693,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -709,6 +711,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_unit.usage_key),
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -726,6 +729,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -743,6 +747,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -760,6 +765,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -777,6 +783,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -794,6 +801,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -811,6 +819,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -828,6 +837,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -845,6 +855,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_is_modified': False,
},
]
self.assertListEqual(data["results"], expected)
@@ -884,6 +895,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -901,6 +913,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -918,6 +931,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_unit.usage_key),
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -935,6 +949,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_is_modified': False,
},
]
self.assertListEqual(data["results"], expected)
@@ -969,6 +984,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -986,6 +1002,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1003,6 +1020,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1020,6 +1038,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1037,6 +1056,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1054,6 +1074,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1071,6 +1092,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': str(self.top_level_downstream_chapter.usage_key),
'downstream_is_modified': False,
},
]
self.assertListEqual(data["results"], expected)
@@ -1170,6 +1192,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1187,6 +1210,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1204,6 +1228,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1221,6 +1246,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
]
print(data["results"])
@@ -1267,6 +1293,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1284,6 +1311,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1301,6 +1329,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
]
self.assertListEqual(data["results"], expected)
@@ -1354,6 +1383,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1371,6 +1401,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
{
'created': date_format,
@@ -1388,6 +1419,7 @@ class GetUpstreamViewTest(
'version_declined': None,
'version_synced': 1,
'top_level_parent_usage_key': None,
'downstream_is_modified': False,
},
]
self.assertListEqual(data["results"], expected)

View File

@@ -2426,6 +2426,7 @@ def _create_or_update_component_link(created: datetime | None, xblock):
top_level_parent_usage_key=top_level_parent_usage_key,
version_synced=xblock.upstream_version,
version_declined=xblock.upstream_version_declined,
downstream_is_modified=len(getattr(xblock, "downstream_customized", [])) > 0,
created=created,
)
@@ -2458,6 +2459,7 @@ def _create_or_update_container_link(created: datetime | None, xblock):
version_synced=xblock.upstream_version,
top_level_parent_usage_key=top_level_parent_usage_key,
version_declined=xblock.upstream_version_declined,
downstream_is_modified=len(getattr(xblock, "downstream_customized", [])) > 0,
created=created,
)

View File

@@ -577,6 +577,7 @@ function($, _, Backbone, gettext, BasePage,
const headerElement = xblockElement.find('.xblock-header-primary');
const upstreamBlockId = headerElement.data('upstream-ref');
const upstreamBlockVersionSynced = headerElement.data('version-synced');
const isLocallyModified = headerElement.data('is-modified');
try {
if (this.options.isIframeEmbed) {
@@ -586,9 +587,11 @@ function($, _, Backbone, gettext, BasePage,
payload: {
downstreamBlockId: xblockInfo.get('id'),
displayName: xblockInfo.get('display_name'),
isVertical: xblockInfo.isVertical(),
isContainer: false,
upstreamBlockId,
upstreamBlockVersionSynced,
isLocallyModified: isLocallyModified === 'True',
blockType: xblockInfo.get('category'),
}
}, document.referrer
);

View File

@@ -470,6 +470,14 @@ body,
&.xblock-iframe-content {
height: 100%;
.xblock-title {
margin-bottom: 1.5em !important;
font-size: 1.5em;
font-weight: bold;
margin-block-start: 0.83em;
margin-block-end: 0.83em;
}
// Reset the max-height to allow the settings list to grow
.wrapper-comp-settings .list-input.settings-list {
max-height: unset;

View File

@@ -93,6 +93,7 @@ can_unlink = upstream_info.upstream_ref and not upstream_info.has_top_level_pare
% if upstream_info.upstream_ref:
data-upstream-ref = ${upstream_info.upstream_ref}
data-version-synced = ${upstream_info.version_synced}
data-is-modified = ${upstream_info.is_modified}
%endif
>
<div class="header-details">

View File

@@ -196,6 +196,11 @@
event listeners below, in certain situations. Resetting it to the default "auto" skirts the problem.-->
<body style="background-color: white;" class="view-container">
<div id="content" class="wrapper xblock-iframe-content">
{% if show_title %}
<div class="xblock-title">
{{ display_name | safe }}
</div>
{% endif %}
<!-- fragment body -->
{{ fragment.body_html | safe }}
<!-- fragment foot -->

View File

@@ -47,6 +47,7 @@ from rest_framework import status
from rest_framework.decorators import api_view, throttle_classes
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
from rest_framework.fields import BooleanField
from web_fragments.fragment import Fragment
from xmodule.course_block import (
COURSE_VISIBILITY_PUBLIC,
@@ -1576,6 +1577,9 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True, disable_sta
Returns an HttpResponse with HTML content for the xBlock with the given usage_key.
The returned HTML is a chromeless rendering of the xBlock (excluding content of the containing courseware).
"""
if not disable_staff_debug_info:
disable_staff_debug_info = BooleanField().to_internal_value(request.GET.get('disable_staff_debug_info', False))
usage_key = UsageKey.from_string(usage_key_string)
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))

View File

@@ -18,6 +18,7 @@ from rest_framework import permissions, serializers
from rest_framework.decorators import api_view, permission_classes # lint-amnesty, pylint: disable=unused-import
from rest_framework.exceptions import PermissionDenied, AuthenticationFailed, NotFound
from rest_framework.response import Response
from rest_framework.fields import BooleanField
from rest_framework.views import APIView
from xblock.django.request import DjangoWebobRequest, webob_to_django_response
from xblock.exceptions import NoSuchUsage
@@ -100,6 +101,10 @@ def embed_block_view(request, usage_key: UsageKeyV2, view_name: str):
Unstable - may change after Sumac
"""
# Check if a specific version has been requested. TODO: move this to a URL path param like the other views?
show_title = request.GET.get('show_title', False)
if show_title is not None:
show_title = BooleanField().to_internal_value(show_title)
try:
version = VersionConverter().to_python(request.GET.get("version"))
except ValueError as exc:
@@ -147,6 +152,8 @@ def embed_block_view(request, usage_key: UsageKeyV2, view_name: str):
'view_name': view_name,
'is_development': settings.DEBUG,
'oa_manifest': new_oa_manifest,
'display_name': block.display_name,
'show_title': show_title,
}
response = render(request, 'xblock_v2/xblock_iframe.html', context, content_type='text/html')