From 2d87434d6ea1859173bf99b6c8a0730908e96828 Mon Sep 17 00:00:00 2001
From: salmannawaz
Date: Thu, 13 Aug 2026 17:57:29 +0500
Subject: [PATCH 1/6] feat: remove legacy Django unit editor
The legacy unit editor (controlled by LEGACY_STUDIO_UNIT_EDITOR waffle
flag) has been fully removed. The MFE-based unit editor (frontend-app-
authoring) is now the only editor and is always active.
Removed:
- LEGACY_STUDIO_UNIT_EDITOR CourseWaffleFlag
- container_handler legacy Django-rendered path (now always redirects to MFE)
- xblock_edit_view and xblock_edit_handler URL (/xblock//action/edit)
- container.html and container_editor.html templates
- unit_outline_spec.js and related legacy tests
Kept (still used by MFE):
- container_embed_handler and /container_embed/ URL
- container_chromeless.html template (rendered for MFE iframes)
- use_new_unit_page serializer field (hardcoded True; MFE still reads it)
Co-Authored-By: Claude Sonnet 4.6
---
.../v1/serializers/course_waffle_flags.py | 10 +-
.../rest_api/v1/views/course_waffle_flags.py | 2 +-
.../contentstore/tests/test_contentstore.py | 61 +---
cms/djangoapps/contentstore/toggles.py | 19 +-
cms/djangoapps/contentstore/utils.py | 12 +-
cms/djangoapps/contentstore/views/block.py | 33 ---
.../contentstore/views/component.py | 59 ++--
.../contentstore/views/tests/test_block.py | 73 -----
.../views/tests/test_container_page.py | 112 +------
cms/static/js/spec/views/unit_outline_spec.js | 150 ----------
cms/templates/container.html | 276 ------------------
cms/templates/container_editor.html | 130 ---------
cms/urls.py | 3 -
13 files changed, 51 insertions(+), 889 deletions(-)
delete mode 100644 cms/static/js/spec/views/unit_outline_spec.js
delete mode 100644 cms/templates/container.html
delete mode 100644 cms/templates/container_editor.html
diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py
index b00f769b5754..d2b2791af710 100644
--- a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py
+++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py
@@ -158,10 +158,14 @@ def get_use_new_course_outline_page(self, obj):
def get_use_new_unit_page(self, obj):
"""
- Method to get the use_new_unit_page switch
+ Method to get the use_new_unit_page switch.
+
+ Always returns True. The legacy unit editor has been removed.
+ This method will be removed in a follow-up cleanup.
+
+ See https://github.com/openedx/edx-platform/issues/36275
"""
- course_key = self.get_course_key()
- return toggles.use_new_unit_page(course_key)
+ return True
def get_use_new_course_team_page(self, obj):
"""
diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_waffle_flags.py
index 69b2898912aa..ffab81ef8a66 100644
--- a/cms/djangoapps/contentstore/rest_api/v1/views/course_waffle_flags.py
+++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_waffle_flags.py
@@ -57,7 +57,7 @@ def get(self, request, course_id=None):
"use_new_files_uploads_page": true,
"use_new_video_uploads_page": false,
"use_new_course_outline_page": true,
- "use_new_unit_page": false,
+ "use_new_unit_page": true,
"use_new_course_team_page": true,
"use_new_certificates_page": true,
"use_new_textbooks_page": true,
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index d09a40ba71ef..8894596511fe 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -1,10 +1,5 @@
# pylint: disable=missing-module-docstring
-# TODO: Rewrite several of these assertions so that they check the output of the REST or Python
-# APIs rather than parsing HTML from the deprecated legacy frontend pages. In particular, any
-# test case using override_waffle_flag(toggles.LEGACY_STUDIO_*, True) will need to be fixed.
-# Part of https://github.com/openedx/edx-platform/issues/36275.
-
import copy
import re
import shutil
@@ -21,7 +16,7 @@
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
-from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch
+from edx_toggles.toggles.testutils import override_waffle_switch
from edxval.api import create_video, get_videos_for_course
from fs.osfs import OSFS
from lxml import etree
@@ -30,7 +25,6 @@
from opaque_keys.edx.locations import CourseLocator
from path import Path as path
-from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore.config import waffle
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, get_url, parse_json
from cms.djangoapps.contentstore.utils import delete_course, reverse_course_url, reverse_url
@@ -568,44 +562,6 @@ def setUp(self):
)
self.course = self.store.publish(self.course.location, self.user.id)
- def check_components_on_page(self, component_types, expected_types):
- """
- Ensure that the right types end up on the page.
-
- component_types is the list of advanced components.
-
- expected_types is the list of elements that should appear on the page.
-
- expected_types and component_types should be similar, but not
- exactly the same -- for example, 'video' in
- component_types should cause 'Video' to be present.
- """
- self.course.advanced_modules = component_types
- self.store.update_item(self.course, self.user.id)
-
- # just pick one vertical
- resp = self.client.get_html(get_url('container_handler', self.vert_loc))
- for expected in expected_types:
- self.assertContains(resp, expected)
-
- @override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True)
- @ddt.data("", "alert('hi')", "")
- def test_container_handler_xss_prevent(self, malicious_code):
- """
- Test that XSS attack is prevented
- """
- resp = self.client.get_html(get_url('container_handler', self.vert_loc) + '?action=' + malicious_code)
- # Test that malicious code does not appear in html
- self.assertNotContains(resp, malicious_code)
-
- @override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True)
- def test_advanced_components_in_edit_unit(self):
- # This could be made better, but for now let's just assert that we see the advanced modules mentioned in the
- # page response HTML
- self.check_components_on_page(
- ADVANCED_COMPONENT_TYPES,
- ['Word cloud', 'Annotation', 'split_test'],
- )
@ddt.data('/Fake/asset/displayname', '\\Fake\\asset\\displayname')
def test_export_on_invalid_displayname(self, invalid_displayname):
@@ -700,14 +656,6 @@ def test_assets_overwrite(self):
# Remove tempdir
shutil.rmtree(root_dir)
- @override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True)
- def test_advanced_components_require_two_clicks(self):
- self.check_components_on_page(['word_cloud'], ['Word cloud'])
-
- @override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True)
- def test_edit_unit(self):
- """Verifies rendering the editor in all the verticals in the given test course"""
- self._check_verticals([self.vert_loc])
def _get_draft_counts(self, item): # pylint: disable=missing-function-docstring
cnt = 1 if not self.store.has_published_version(item) else 0
@@ -1520,11 +1468,10 @@ def test_get_json(handler):
)
self.assertEqual(resp.status_code, 200) # noqa: PT009
- # go look at the Edit page
+ # go look at the Edit page — now redirects to MFE unit editor
unit_key = course_key.make_usage_key('vertical', 'test_vertical')
- with override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True):
- resp = self.client.get_html(get_url('container_handler', unit_key))
- self.assertEqual(resp.status_code, 200) # noqa: PT009
+ resp = self.client.get_html(get_url('container_handler', unit_key))
+ self.assertEqual(resp.status_code, 302) # noqa: PT009
def delete_item(category, name):
""" Helper method for testing the deletion of an xblock item. """
diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py
index 94960ba66ec0..f7f67adc9057 100644
--- a/cms/djangoapps/contentstore/toggles.py
+++ b/cms/djangoapps/contentstore/toggles.py
@@ -194,23 +194,14 @@ def use_new_video_uploads_page(course_key):
LEGACY_STUDIO_COURSE_OUTLINE = CourseWaffleFlag('legacy_studio.course_outline', __name__)
-# .. toggle_name: legacy_studio.unit_editor
-# .. toggle_implementation: WaffleFlag
-# .. toggle_default: False
-# .. toggle_description: Temporarily fall back to the old Studio unit editing page.
-# .. toggle_use_cases: temporary
-# .. toggle_creation_date: 2025-03-14
-# .. toggle_target_removal_date: 2025-09-14
-# .. toggle_tickets: https://github.com/openedx/edx-platform/issues/36275
-# .. toggle_warning: In Ulmo, this toggle will be removed. Only the new (React-based) experience will be available.
-LEGACY_STUDIO_UNIT_EDITOR = CourseWaffleFlag('legacy_studio.unit_editor', __name__)
-
-
def use_new_unit_page(course_key):
"""
- Returns a boolean if new studio course outline mfe is enabled
+ Returns a boolean if new studio unit page is enabled.
+
+ Always returns True. The legacy unit editor has been removed.
+ This function will be removed in a follow-up cleanup once all callers are updated.
"""
- return not LEGACY_STUDIO_UNIT_EDITOR.is_enabled(course_key)
+ return True
# .. toggle_name: contentstore.mock_video_uploads
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 2f43a00610e8..cdd0bdf8c671 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -42,7 +42,6 @@
libraries_v1_enabled,
libraries_v2_enabled,
split_library_view_on_dashboard,
- use_new_unit_page,
)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
@@ -425,13 +424,10 @@ def get_unit_url(course_locator, unit_locator) -> str:
"""
Gets course authoring microfrontend URL for unit page view.
"""
- unit_url = None
- if use_new_unit_page(course_locator):
- mfe_base_url = get_course_authoring_url(course_locator)
- course_mfe_url = f'{mfe_base_url}/course/{course_locator}/container/{unit_locator}'
- if mfe_base_url:
- unit_url = course_mfe_url
- return unit_url
+ mfe_base_url = get_course_authoring_url(course_locator)
+ if mfe_base_url:
+ return f'{mfe_base_url}/course/{course_locator}/container/{unit_locator}'
+ return None
def get_certificates_url(course_locator) -> str:
diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py
index b2c0e39df77c..09f8c34a89aa 100644
--- a/cms/djangoapps/contentstore/views/block.py
+++ b/cms/djangoapps/contentstore/views/block.py
@@ -41,7 +41,6 @@
)
from ..helpers import is_unit
-from ..utils import get_container_handler_context
from .component import _get_item_in_course
from .preview import get_preview_fragment
@@ -289,38 +288,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
return HttpResponse(status=406)
-@xframe_options_exempt
-@require_http_methods(["GET"])
-@login_required
-def xblock_edit_view(request, usage_key_string):
- """
- Return rendered xblock edit view.
-
- Allows editing of an XBlock specified by the usage key.
- """
- usage_key = usage_key_with_run(usage_key_string)
- if not has_studio_read_access(request.user, usage_key.course_key):
- raise PermissionDenied()
-
- store = modulestore()
-
- with store.bulk_operations(usage_key.course_key):
- course, xblock, _, __ = _get_item_in_course(request, usage_key)
- container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
-
- fragment = get_preview_fragment(request, xblock, {})
-
- hashed_resources = {
- hash_resource(resource): resource._asdict() for resource in fragment.resources
- }
-
- container_handler_context.update({
- "action_name": "edit",
- "resources": list(hashed_resources.items()),
- })
-
- return render_to_response('container_editor.html', container_handler_context)
-
@require_http_methods("GET")
@login_required
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 05a9fc12d291..0b8176fbb5bd 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -22,7 +22,7 @@
from xblock.runtime import Mixologist
from cms.djangoapps.contentstore.helpers import get_parent_if_split_test, is_library_content, is_unit
-from cms.djangoapps.contentstore.toggles import libraries_v2_enabled, use_new_unit_page
+from cms.djangoapps.contentstore.toggles import libraries_v1_enabled, libraries_v2_enabled
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.auth import has_course_author_access
@@ -130,44 +130,36 @@ def _load_mixed_class(category):
@require_GET
@login_required
-def container_handler(request, usage_key_string): # pylint: disable=too-many-statements
+def container_handler(request, usage_key_string):
"""
- The restful handler for container xblock requests.
+ Redirects to the MFE unit editor.
- GET
- html: returns the HTML page for editing a container
- json: not currently supported
+ The legacy Django-template-based unit editor has been removed.
+ This view exists for backward compatibility with any existing links to /container/.
"""
+ from ..utils import get_unit_url
- from ..utils import get_container_handler_context, get_unit_url
+ if 'text/html' not in request.META.get('HTTP_ACCEPT', 'text/html'):
+ return HttpResponseBadRequest("Only supports HTML requests")
- if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
+ try:
+ usage_key = UsageKey.from_string(usage_key_string)
+ except InvalidKeyError:
+ raise Http404 # pylint: disable=raise-missing-from # noqa: B904
+ with modulestore().bulk_operations(usage_key.course_key):
try:
- usage_key = UsageKey.from_string(usage_key_string)
- except InvalidKeyError: # Raise Http404 on invalid 'usage_key_string'
- raise Http404 # pylint: disable=raise-missing-from # noqa: B904
- with modulestore().bulk_operations(usage_key.course_key):
- try:
- course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
- except ItemNotFoundError:
- return HttpResponseBadRequest()
-
- if use_new_unit_page(course.id):
- if is_unit(xblock) or is_library_content(xblock):
- return redirect(get_unit_url(course.id, xblock.location))
-
- if split_xblock := get_parent_if_split_test(xblock):
- return redirect(get_unit_url(course.id, split_xblock.location))
-
- container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
- container_handler_context.update({
- 'draft_preview_link': preview_lms_link,
- 'published_preview_link': lms_link,
- })
- return render_to_response('container.html', container_handler_context)
- else:
- return HttpResponseBadRequest("Only supports HTML requests")
+ course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
+ except ItemNotFoundError:
+ return HttpResponseBadRequest()
+
+ if is_unit(xblock) or is_library_content(xblock):
+ return redirect(get_unit_url(course.id, xblock.location))
+
+ if split_xblock := get_parent_if_split_test(xblock):
+ return redirect(get_unit_url(course.id, split_xblock.location))
+
+ raise Http404
@require_GET
@@ -176,7 +168,8 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st
def container_embed_handler(request, usage_key_string): # pylint: disable=too-many-statements
"""
Returns an HttpResponse with HTML content for the container XBlock.
- The returned HTML is a chromeless rendering of the XBlock.
+ The returned HTML is a chromeless rendering of the XBlock, used by the
+ Authoring MFE to display xblock content in iframes.
GET
html: returns the HTML page for editing a container
diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py
index 9e53a3aa0195..44b2cbdbca8d 100644
--- a/cms/djangoapps/contentstore/views/tests/test_block.py
+++ b/cms/djangoapps/contentstore/views/tests/test_block.py
@@ -4700,76 +4700,3 @@ def test_update_clobbers(self):
self.check_updated(source_block, destination_block.location)
-class TestXblockEditView(CourseTestCase):
- """
- Test xblock_edit_view.
- """
-
- def setUp(self):
- super().setUp()
- self.chapter = self._create_block(self.course, "chapter", "Week 1")
- self.sequential = self._create_block(self.chapter, "sequential", "Lesson 1")
- self.vertical = self._create_block(self.sequential, "vertical", "Unit")
- self.html = self._create_block(self.vertical, "html", "HTML")
- self.child_container = self._create_block(
- self.vertical, "split_test", "Split Test"
- )
- self.child_vertical = self._create_block(
- self.child_container, "vertical", "Child Vertical"
- )
- self.video = self._create_block(self.child_vertical, "video", "My Video")
- self.store = modulestore()
-
- self.store.publish(self.vertical.location, self.user.id)
-
- def _create_block(self, parent, category, display_name, **kwargs):
- """
- creates a block in the module store, without publishing it.
- """
- return BlockFactory.create(
- parent=parent,
- category=category,
- display_name=display_name,
- publish_item=False,
- user_id=self.user.id,
- **kwargs,
- )
-
- def test_xblock_edit_view(self):
- url = reverse_usage_url("xblock_edit_handler", self.video.location)
- resp = self.client.get_html(url)
- self.assertEqual(resp.status_code, 200) # noqa: PT009
-
- html_content = resp.content.decode(resp.charset)
- self.assertIn("var decodedActionName = 'edit';", html_content) # noqa: PT009
-
- def test_xblock_edit_view_contains_resources(self):
- url = reverse_usage_url("xblock_edit_handler", self.video.location)
- resp = self.client.get(url)
- self.assertEqual(resp.status_code, 200) # noqa: PT009
-
- html_content = resp.content.decode(resp.charset)
- soup = BeautifulSoup(html_content, "html.parser")
-
- resource_links = [link["href"] for link in soup.find_all("link", {"rel": "stylesheet"})]
- script_sources = [script["src"] for script in soup.find_all("script") if script.get("src")]
-
- self.assertGreater(len(resource_links), 0, f"No CSS resources found in HTML. Found: {resource_links}") # noqa: PT009 # pylint: disable=line-too-long
- self.assertGreater(len(script_sources), 0, f"No JS resources found in HTML. Found: {script_sources}") # noqa: PT009 # pylint: disable=line-too-long
-
- def test_xblock_edit_view_contains_page_notification(self):
- """
- The page-notification element is required for XBlock runtime error
- notifications (e.g. ORA validation errors) to be visible to the user.
- """
- url = reverse_usage_url("xblock_edit_handler", self.video.location)
- resp = self.client.get(url)
- self.assertEqual(resp.status_code, 200) # noqa: PT009
-
- html_content = resp.content.decode(resp.charset)
- soup = BeautifulSoup(html_content, "html.parser")
- self.assertIsNotNone( # noqa: PT009
- soup.find(id="page-notification"),
- "container_editor.html must include a #page-notification element "
- "so that XBlock runtime error notifications are rendered.",
- )
diff --git a/cms/djangoapps/contentstore/views/tests/test_container_page.py b/cms/djangoapps/contentstore/views/tests/test_container_page.py
index fbb14634820c..f0df927d49f8 100644
--- a/cms/djangoapps/contentstore/views/tests/test_container_page.py
+++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py
@@ -5,17 +5,14 @@
import datetime
import re
-from unittest.mock import Mock, patch
from urllib.parse import quote
from django.http import Http404
from django.test.client import RequestFactory
from django.urls import reverse
-from edx_toggles.toggles.testutils import override_waffle_flag
from pytz import UTC
import cms.djangoapps.contentstore.views.component as views
-from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase
from xmodule.modulestore import ModuleStoreEnum # pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore # pylint: disable=wrong-import-order
@@ -88,46 +85,6 @@ def test_container_html(self):
),
)
- @override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True)
- def test_container_on_container_html(self):
- """
- Create the scenario of an xblock with children (non-vertical) on the container page.
- This should create a container page that is a child of another container page.
- """
- draft_container = self._create_block(self.child_container, "wrapper", "Wrapper")
- self._create_block(draft_container, "html", "Child HTML")
-
- def test_container_html(xblock):
- assets_url = reverse(
- 'assets_handler', kwargs={'course_key_string': str(draft_container.location.course_key)}
- )
- self._test_html_content(
- xblock,
- expected_section_tag=(
- ''.format(
- draft_container.location, assets_url
- )
- ),
- expected_breadcrumbs=(
- 'Lesson 1.*'
- 'Unit.*'
- ).format(
- course=re.escape(str(self.course.id)),
- unit_parameters=re.escape(str(self.vertical.location)),
- subsection_parameters=re.escape('?show={}'.format(quote(
- str(self.sequential.location).encode()
- ))),
- ),
- )
-
- # Test the draft version of the container
- test_container_html(draft_container)
-
- # Now publish the unit and validate again
- self.store.publish(self.vertical.location, self.user.id)
- draft_container = self.store.get_item(draft_container.location)
- test_container_html(draft_container)
def _test_html_content(self, xblock, expected_section_tag, expected_breadcrumbs):
"""
@@ -223,17 +180,13 @@ def test_draft_child_container_preview_html(self):
empty_child_container = self._create_block(self.vertical, 'split_test', 'Split Test 1')
self.validate_preview_html(empty_child_container, self.reorderable_child_view, can_add=False)
- @patch(
- 'cms.djangoapps.contentstore.views.component.render_to_response',
- Mock(return_value=Mock(status_code=200, content=''))
- )
- @override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True)
def test_container_page_with_valid_and_invalid_usage_key_string(self):
"""
- Check that invalid 'usage_key_string' raises Http404.
+ Check that invalid 'usage_key_string' raises Http404 and valid key redirects to MFE.
"""
request = RequestFactory().get('foo')
request.user = self.user
+ request.META['HTTP_ACCEPT'] = 'text/html'
request.LANGUAGE_CODE = 'en'
# Check for invalid 'usage_key_strings'
@@ -243,68 +196,11 @@ def test_container_page_with_valid_and_invalid_usage_key_string(self):
usage_key_string='i4x://InvalidOrg/InvalidCourse/vertical/static/InvalidContent',
)
- # Check 200 response if 'usage_key_string' is correct
+ # Check redirect response if 'usage_key_string' is correct (now redirects to MFE)
response = views.container_handler(
request=request,
usage_key_string=str(self.vertical.location)
)
- self.assertEqual(response.status_code, 200) # noqa: PT009
-
-
-class ContainerEmbedPageTestCase(ContainerPageTestCase): # pylint: disable=test-inherits-tests
- """
- Unit tests for the container embed page.
- """
-
- def test_container_html(self):
- assets_url = reverse(
- 'assets_handler', kwargs={'course_key_string': str(self.child_container.location.course_key)}
- )
- self._test_html_content(
- self.child_container,
- expected_section_tag=(
- ''.format(
- self.child_container.location, assets_url
- )
- ),
- )
-
- @override_waffle_flag(toggles.LEGACY_STUDIO_UNIT_EDITOR, True)
- def test_container_on_container_html(self):
- """
- Create the scenario of an xblock with children (non-vertical) on the container page.
- This should create a container page that is a child of another container page.
- """
- draft_container = self._create_block(self.child_container, "wrapper", "Wrapper")
- self._create_block(draft_container, "html", "Child HTML")
+ self.assertEqual(response.status_code, 302) # noqa: PT009
- def test_container_html(xblock):
- assets_url = reverse(
- 'assets_handler', kwargs={'course_key_string': str(draft_container.location.course_key)}
- )
- self._test_html_content(
- xblock,
- expected_section_tag=(
- ''.format(
- draft_container.location, assets_url
- )
- ),
- )
- # Test the draft version of the container
- test_container_html(draft_container)
-
- # Now publish the unit and validate again
- self.store.publish(self.vertical.location, self.user.id)
- draft_container = self.store.get_item(draft_container.location)
- test_container_html(draft_container)
-
- def _test_html_content(self, xblock, expected_section_tag): # pylint: disable=arguments-differ
- """
- Get the HTML for a container page and verify the section tag is correct
- and the breadcrumbs trail is correct.
- """
- html = self.get_page_html(xblock)
- self.assertIn(expected_section_tag, html) # noqa: PT009
diff --git a/cms/static/js/spec/views/unit_outline_spec.js b/cms/static/js/spec/views/unit_outline_spec.js
deleted file mode 100644
index 99eb7d5da399..000000000000
--- a/cms/static/js/spec/views/unit_outline_spec.js
+++ /dev/null
@@ -1,150 +0,0 @@
-define(['jquery', 'sinon', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers',
- 'common/js/spec_helpers/view_helpers', 'common/js/components/utils/view_utils', 'js/models/course',
- 'js/views/unit_outline', 'js/models/xblock_info'],
-function($, sinon, AjaxHelpers, TemplateHelpers, ViewHelpers, ViewUtils,
- Course, UnitOutlineView, XBlockInfo) {
- 'use strict';
-
- describe('UnitOutlineView', function() {
- // Preload before beforeEach installs fake XHR, so readFixtures uses real XHR
- // and the result is cached. See group_configuration_spec.js for full explanation.
- readFixtures('unit-outline.underscore');
-
- var createUnitOutlineView, createMockXBlockInfo,
- requests, xhrFactory, model, unitOutlineView;
-
- createUnitOutlineView = function(test, unitJSON, createOnly) {
- model = new XBlockInfo(unitJSON, {parse: true});
- unitOutlineView = new UnitOutlineView({
- model: model,
- el: $('.wrapper-unit-overview')
- });
- if (!createOnly) {
- unitOutlineView.render();
- }
- return unitOutlineView;
- };
-
- createMockXBlockInfo = function(displayName) {
- return {
- id: 'mock-unit',
- category: 'vertical',
- display_name: displayName,
- studio_url: '/container/mock-unit',
- visibility_state: 'unscheduled',
- ancestor_info: {
- ancestors: [{
- id: 'mock-subsection',
- category: 'sequential',
- display_name: 'Mock Subsection',
- studio_url: '/course/mock-course?show=mock-subsection',
- visibility_state: 'unscheduled',
- child_info: {
- category: 'vertical',
- display_name: 'Unit',
- children: [{
- id: 'mock-unit',
- category: 'vertical',
- display_name: displayName,
- studio_url: '/container/mock-unit',
- visibility_state: 'unscheduled'
- }, {
- id: 'mock-unit-2',
- category: 'vertical',
- display_name: 'Mock Unit 2',
- studio_url: '/container/mock-unit-2',
- visibility_state: 'unscheduled'
- }]
- }
- }, {
- id: 'mock-section',
- category: 'chapter',
- display_name: 'Section',
- studio_url: '/course/slashes:mock-course?show=mock-section',
- visibility_state: 'unscheduled'
- }, {
- id: 'mock-course',
- category: 'course',
- display_name: 'Mock Course',
- studio_url: '/course/mock-course',
- visibility_state: 'unscheduled'
- }]
- },
- metadata: {
- display_name: 'Mock Unit'
- }
- };
- };
-
- beforeEach(function() {
- xhrFactory = sinon.useFakeXMLHttpRequest();
- requests = [];
- requests.currentIndex = 0;
- requests.restore = function() { xhrFactory.restore(); };
- xhrFactory.onCreate = function(req) { requests.push(req); };
- window.course = new Course({
- id: '5',
- name: 'Course Name',
- url_name: 'course_name',
- org: 'course_org',
- num: 'course_num',
- revision: 'course_rev'
- });
- ViewHelpers.installMockAnalytics();
- ViewHelpers.installViewTemplates();
- TemplateHelpers.installTemplate('unit-outline');
- appendSetFixtures('');
- });
-
- afterEach(function() {
- requests.restore();
- delete window.course;
- ViewHelpers.removeMockAnalytics();
- });
-
- it('can render itself', function() {
- createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
- expect(unitOutlineView.$('.list-sections')).toExist();
- expect(unitOutlineView.$('.list-subsections')).toExist();
- expect(unitOutlineView.$('.list-units')).toExist();
- });
-
- it('highlights the current unit', function() {
- createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
- $('.outline-unit').each(function(i) {
- if ($(this).data('locator') === model.get('id')) {
- expect($(this)).toHaveClass('is-current');
- } else {
- expect($(this)).not.toHaveClass('is-current');
- }
- });
- });
-
- it('can add a unit', function() {
- var redirectSpy;
- createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
- redirectSpy = spyOn(ViewUtils, 'redirect');
- unitOutlineView.$('.outline-subsection > .outline-content > .add-unit .button-new').click();
- AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
- category: 'vertical',
- display_name: 'Unit',
- parent_locator: 'mock-subsection'
- });
- AjaxHelpers.respondWithJson(requests, {
- locator: 'new-mock-unit',
- courseKey: 'slashes:MockCourse'
- });
- expect(redirectSpy).toHaveBeenCalledWith('/container/new-mock-unit?action=new');
- });
-
- it('refreshes when the XBlockInfo model syncs', function() {
- var updatedDisplayName = 'Mock Unit Updated';
- createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
- unitOutlineView.refresh();
- AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/mock-unit');
- AjaxHelpers.respondWithJson(requests,
- createMockXBlockInfo(updatedDisplayName));
- expect(unitOutlineView.$('.outline-unit .unit-title').first().text().trim()).toBe(updatedDisplayName);
- });
- });
-});
diff --git a/cms/templates/container.html b/cms/templates/container.html
deleted file mode 100644
index e1995de4ba58..000000000000
--- a/cms/templates/container.html
+++ /dev/null
@@ -1,276 +0,0 @@
-<%page expression_filter="h"/>
-<%inherit file="base.html" />
-<%def name="online_help_token()">
-<%
-if is_unit_page:
- return "unit"
-else:
- return "container"
-%>
-%def>
-<%!
-from django.urls import reverse
-from django.utils.translation import gettext as _
-
-from cms.djangoapps.contentstore.helpers import xblock_studio_url, xblock_type_display_name
-from cms.djangoapps.contentstore.toggles import use_new_pdf_editor, use_video_gallery_flow
-from cms.djangoapps.contentstore.utils import get_editor_page_base_url
-from openedx.core.djangolib.js_utils import (
- dump_js_escaped_json, js_escaped_string
-)
-from openedx.core.djangolib.markup import HTML, Text
-%>
-
-<%block name="title">${xblock.display_name_with_default} ${xblock_type_display_name(xblock)}%block>
-<%block name="bodyclass">is-signedin course container view-container%block>
-
-<%namespace name='static' file='static_content.html'/>
-
-<%block name="header_extras">
-% for template_name in templates:
-
-% endfor
-
-
-%block>
-
-<%block name="page_bundle">
- <%static:webpack entry="js/factories/container">
- ContainerFactory(
- ${component_templates | n, dump_js_escaped_json},
- ${xblock_info | n, dump_js_escaped_json},
- "${action | n, js_escaped_string}",
- {
- isUnitPage: ${is_unit_page | n, dump_js_escaped_json},
- canEdit: true,
- outlineURL: "${outline_url | n, js_escaped_string}",
- libraryContentPickerUrl: "${library_content_picker_url | n, js_escaped_string}",
- clipboardData: ${user_clipboard | n, dump_js_escaped_json},
- }
- );
-
- require(["js/models/xblock_info", "js/views/xblock", "js/views/utils/xblock_utils", "common/js/components/utils/view_utils", "gettext"], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext) {
- var model = new XBlockInfo({
- id: '${subsection.location|n, decode.utf8}'
- });
- var xblockView = new XBlockView({
- model: model,
- el: $('#sequence-nav'),
- view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}',
- clipboardData: ${user_clipboard | n, dump_js_escaped_json},
- });
-
- xblockView.xblockReady = function() {
-
- var toggleCaretButton = function(clipboardData) {
- if (clipboardData && clipboardData.content && clipboardData.source_usage_key.includes("vertical")) {
- $('.dropdown-toggle-button').show();
- } else {
- $('.dropdown-toggle-button').hide();
- $('.dropdown-options').hide();
- }
- };
- this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel");
- this.clipboardBroadcastChannel.onmessage = (event) => {
- toggleCaretButton(event.data);
- };
- toggleCaretButton(this.options.clipboardData);
-
- $('#new-unit-button').on('click', function(event) {
- event.preventDefault();
- XBlockUtils.addXBlock($(this)).done(function(locator) {
- ViewUtils.redirect('/container/' + locator + '?action=new');
- });
- });
-
- $('.custom-dropdown .dropdown-toggle-button').on('click', function(event) {
- event.stopPropagation(); // Prevent the event from closing immediately when we open it
- $(this).next('.dropdown-options').slideToggle('fast'); // This toggles the dropdown visibility
- var isExpanded = $(this).attr('aria-expanded') === 'true';
- $(this).attr('aria-expanded', !isExpanded);
- });
-
- $('.seq_paste_unit').on('click', function(event) {
- event.preventDefault();
- $('.dropdown-options').hide();
- XBlockUtils.pasteXBlock($(this)).done(function(data) {
- ViewUtils.redirect('/container/' + data.locator + '?action=new');
- });
- });
- };
-
- xblockView.render();
- });
- %static:webpack>
-%block>
-
-<%block name="content">
-
-<%
-use_new_editor_pdf = use_new_pdf_editor()
-use_new_video_gallery_flow = use_video_gallery_flow()
-%>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- % if is_unit_page:
-
- % endif
-
-
-
-
-
-
-
-
- <%
- assets_url = reverse('assets_handler', kwargs={'course_key_string': str(xblock_locator.course_key)})
- %>
-
-
-
-
-
-
-
-
-
-
-%block>
diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html
deleted file mode 100644
index 3bea1e3d2118..000000000000
--- a/cms/templates/container_editor.html
+++ /dev/null
@@ -1,130 +0,0 @@
-## coding=utf-8
-## mako
-
-## Pages currently use v1 styling by default. Once the Pattern Library
-## rollout has been completed, this default can be switched to v2.
-<%! main_css = "style-main-v1" %>
-
-<%! course_unit_mfe_iframe_css = "course-unit-mfe-iframe-bundle" %>
-
-## Standard imports
-<%namespace name='static' file='static_content.html'/>
-<%!
-from django.utils.translation import gettext as _
-from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
-from lms.djangoapps.branding import api as branding_api
-from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
-from cms.djangoapps.contentstore.helpers import xblock_type_display_name
-from openedx.core.release import RELEASE_LINE
-%>
-<%def name="online_help_token()">
-<%
- return "container"
-%>
-%def>
-
-<%page expression_filter="h"/>
-
-
-
- <%
- jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE)
- %>
-
- % if getattr(settings, 'CAPTURE_CONSOLE_LOG', False):
-
- % endif
-
-
-
- % if settings.DEBUG:
-
- % endif
-
- <%static:css group='style-vendor'/>
- <%static:css group='style-vendor-tinymce-content'/>
- <%static:css group='style-vendor-tinymce-skin'/>
- <%static:css group='${self.attr.course_unit_mfe_iframe_css}'/>
-
- % if uses_bootstrap:
-
- % else:
- <%static:css group='${self.attr.main_css}'/>
- % endif
-
- <%include file="widgets/segment-io.html" />
-
- <%block name="header_extras">
- % for template_name in templates:
-
- % endfor
-
-
-
- % for _, resource in resources:
- % if resource['kind'] == 'url' and resource['mimetype'] == 'text/css':
-
- % endif
- % endfor
- %block>
-
-
-
-
-
-
-
- <%static:js group='base_vendor' />
- <%static:webpack entry='commons' />
-
-
-
-
- <%block name='page_bundle'>
- <%static:webpack entry="js/factories/container">
- require(['js/models/xblock_info', 'js/views/modals/edit_xblock'],
- function (XBlockInfo, EditXBlockModal) {
- var decodedActionName = '${action_name|n, decode.utf8}';
- var encodedXBlockDetails = ${xblock_info | n, dump_js_escaped_json};
-
- if (decodedActionName === 'edit') {
- var editXBlockModal = new EditXBlockModal();
- var xblockInfoInstance = new XBlockInfo(encodedXBlockDetails);
-
- editXBlockModal.edit([], xblockInfoInstance, {});
- }
- });
- %static:webpack>
- %block>
-
-
diff --git a/cms/urls.py b/cms/urls.py
index c0f96f489bb8..d0d917294d96 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -19,7 +19,6 @@
import openedx.core.djangoapps.lang_pref.views
from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore import views as contentstore_views
-from cms.djangoapps.contentstore.views.block import xblock_edit_view
from cms.djangoapps.contentstore.views.organization import OrganizationListView
from openedx.core import toggles as core_toggles
from openedx.core.apidocs import api_info
@@ -153,8 +152,6 @@
name='xblock_outline_handler'),
re_path(fr'^xblock/container/{settings.USAGE_KEY_PATTERN}$', contentstore_views.xblock_container_handler,
name='xblock_container_handler'),
- re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/action/edit$', xblock_edit_view,
- name='xblock_edit_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/(?P[^/]+)$', contentstore_views.xblock_view_handler,
name='xblock_view_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}?$', contentstore_views.xblock_handler,
From 2d839dbbf6dc08e3f990c81848d57f532ea69a4b Mon Sep 17 00:00:00 2001
From: salmannawaz
Date: Mon, 17 Aug 2026 15:50:10 +0500
Subject: [PATCH 2/6] fix: remove unused imports and stale Karma spec reference
- Remove unused imports in block.py (xframe_options_exempt,
render_to_response, _get_item_in_course)
- Remove unused libraries_v1_enabled import in component.py
- Remove unused ADVANCED_COMPONENT_TYPES import in test_contentstore.py
- Remove unused BeautifulSoup import in test_block.py
- Fix trailing newlines in test_block.py and test_container_page.py
- Remove deleted unit_outline_spec.js from Karma spec list in main.js
Co-Authored-By: Claude Sonnet 4.6
---
cms/djangoapps/contentstore/tests/test_contentstore.py | 1 -
cms/djangoapps/contentstore/views/block.py | 4 +---
cms/djangoapps/contentstore/views/component.py | 2 +-
cms/djangoapps/contentstore/views/tests/test_block.py | 3 ---
.../contentstore/views/tests/test_container_page.py | 2 --
cms/static/cms/js/spec/main.js | 1 -
6 files changed, 2 insertions(+), 11 deletions(-)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 8894596511fe..bc1b6b0be931 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -28,7 +28,6 @@
from cms.djangoapps.contentstore.config import waffle
from cms.djangoapps.contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, get_url, parse_json
from cms.djangoapps.contentstore.utils import delete_course, reverse_course_url, reverse_url
-from cms.djangoapps.contentstore.views.component import ADVANCED_COMPONENT_TYPES
from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError
from common.djangoapps.course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from common.djangoapps.student import auth
diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py
index 09f8c34a89aa..a9fc1c2720ab 100644
--- a/cms/djangoapps/contentstore/views/block.py
+++ b/cms/djangoapps/contentstore/views/block.py
@@ -9,7 +9,6 @@
from django.db import transaction
from django.http import Http404, HttpResponse
from django.utils.translation import gettext as _
-from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.http import require_http_methods
from opaque_keys.edx.keys import CourseKey
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE
@@ -25,7 +24,7 @@
)
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import get_tags_count, usage_key_with_run
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
-from common.djangoapps.edxmako.shortcuts import render_to_response, render_to_string
+from common.djangoapps.edxmako.shortcuts import render_to_string
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
from common.djangoapps.util.json_request import JsonResponse, expect_json
from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
@@ -41,7 +40,6 @@
)
from ..helpers import is_unit
-from .component import _get_item_in_course
from .preview import get_preview_fragment
__all__ = [
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 0b8176fbb5bd..cbab7b844f6d 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -22,7 +22,7 @@
from xblock.runtime import Mixologist
from cms.djangoapps.contentstore.helpers import get_parent_if_split_test, is_library_content, is_unit
-from cms.djangoapps.contentstore.toggles import libraries_v1_enabled, libraries_v2_enabled
+from cms.djangoapps.contentstore.toggles import libraries_v2_enabled
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.auth import has_course_author_access
diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py
index 44b2cbdbca8d..016939f2004f 100644
--- a/cms/djangoapps/contentstore/views/tests/test_block.py
+++ b/cms/djangoapps/contentstore/views/tests/test_block.py
@@ -7,7 +7,6 @@
from unittest.mock import Mock, PropertyMock, patch
import ddt
-from bs4 import BeautifulSoup
from django.conf import settings
from django.http import Http404
from django.test import TestCase
@@ -4698,5 +4697,3 @@ def test_update_clobbers(self):
user_id=user.id,
)
self.check_updated(source_block, destination_block.location)
-
-
diff --git a/cms/djangoapps/contentstore/views/tests/test_container_page.py b/cms/djangoapps/contentstore/views/tests/test_container_page.py
index f0df927d49f8..642c28393ab9 100644
--- a/cms/djangoapps/contentstore/views/tests/test_container_page.py
+++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py
@@ -202,5 +202,3 @@ def test_container_page_with_valid_and_invalid_usage_key_string(self):
usage_key_string=str(self.vertical.location)
)
self.assertEqual(response.status_code, 302) # noqa: PT009
-
-
diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js
index 019fb2b67887..bc284ed395bd 100644
--- a/cms/static/cms/js/spec/main.js
+++ b/cms/static/cms/js/spec/main.js
@@ -250,7 +250,6 @@
'js/spec/views/assets_spec',
'js/spec/views/baseview_spec',
'js/spec/views/paged_container_spec',
- 'js/spec/views/unit_outline_spec',
'js/spec/views/xblock_spec',
'js/spec/views/xblock_validation_spec',
'js/spec/views/license_spec',
From 95b84bd2e57cc8e26a45b7484c00c3da3bd81a0b Mon Sep 17 00:00:00 2001
From: salmannawaz
Date: Mon, 17 Aug 2026 23:30:46 +0500
Subject: [PATCH 3/6] fix: remove legacy test_container_html and revert
split_test fallback
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The test_container_html was testing legacy Django-rendered HTML content
(breadcrumbs, section tags from the deleted container.html template).
Removed it since the legacy editor is gone and the split_test container
URL (/container/) was legacy-only — the MFE does not use it.
Also reverts the get_parent_xblock fallback added to container_handler
as it was adding new functionality rather than just removing legacy code.
Co-Authored-By: Claude Sonnet 4.6
---
.../views/tests/test_container_page.py | 39 -------------------
1 file changed, 39 deletions(-)
diff --git a/cms/djangoapps/contentstore/views/tests/test_container_page.py b/cms/djangoapps/contentstore/views/tests/test_container_page.py
index 642c28393ab9..64abff538897 100644
--- a/cms/djangoapps/contentstore/views/tests/test_container_page.py
+++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py
@@ -4,12 +4,9 @@
import datetime
-import re
-from urllib.parse import quote
from django.http import Http404
from django.test.client import RequestFactory
-from django.urls import reverse
from pytz import UTC
import cms.djangoapps.contentstore.views.component as views
@@ -59,42 +56,6 @@ def setUp(self):
self.store.publish(self.released_public_vertical.location, self.user.id)
self.store.publish(self.vertical.location, self.user.id)
- def test_container_html(self):
- assets_url = reverse(
- 'assets_handler', kwargs={'course_key_string': str(self.child_container.location.course_key)}
- )
- self._test_html_content(
- self.child_container,
- expected_section_tag=(
- ''.format(
- self.child_container.location, assets_url
- )
- ),
- expected_breadcrumbs=(
- '\\s*Week 1<\\/a>.*'
- 'Lesson 1'
- ).format(
- course=re.escape(str(self.course.id)),
- section_parameters=re.escape('?show={}'.format(quote(
- str(self.chapter.location).encode()
- ))),
- subsection_parameters=re.escape('?show={}'.format(quote(
- str(self.sequential.location).encode()
- ))),
- ),
- )
-
-
- def _test_html_content(self, xblock, expected_section_tag, expected_breadcrumbs):
- """
- Get the HTML for a container page and verify the section tag is correct
- and the breadcrumbs trail is correct.
- """
- html = self.get_page_html(xblock)
- self.assertIn(expected_section_tag, html) # noqa: PT009
- self.assertRegex(html, re.compile(expected_breadcrumbs, re.DOTALL)) # noqa: PT009
-
def test_public_container_preview_html(self):
"""
Verify that a public xblock's container preview returns the expected HTML.
From db13ea0cafb98195aea6e0abcf85ed6a89fd5cc5 Mon Sep 17 00:00:00 2001
From: salmannawaz
Date: Fri, 21 Aug 2026 19:28:26 +0500
Subject: [PATCH 4/6] refactor: restore xblock_edit_view and remove
use_new_unit_page from toggles
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Restore xblock_edit_view, container_editor.html, and the xblock_edit_handler
URL — the Authoring MFE still calls /xblock//action/edit to load the
xblock edit modal in an iframe (getLegacyEditModalUrl in xblock-container-iframe)
- Remove use_new_unit_page() from toggles.py — it had no remaining callers
after container_handler was simplified to always redirect to the MFE
Co-Authored-By: Claude Sonnet 4.6
---
cms/djangoapps/contentstore/toggles.py | 9 --
cms/djangoapps/contentstore/views/block.py | 37 ++++-
.../contentstore/views/tests/test_block.py | 76 ++++++++++
cms/templates/container_editor.html | 130 ++++++++++++++++++
cms/urls.py | 3 +
5 files changed, 245 insertions(+), 10 deletions(-)
create mode 100644 cms/templates/container_editor.html
diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py
index f7f67adc9057..1ac5889908bb 100644
--- a/cms/djangoapps/contentstore/toggles.py
+++ b/cms/djangoapps/contentstore/toggles.py
@@ -194,15 +194,6 @@ def use_new_video_uploads_page(course_key):
LEGACY_STUDIO_COURSE_OUTLINE = CourseWaffleFlag('legacy_studio.course_outline', __name__)
-def use_new_unit_page(course_key):
- """
- Returns a boolean if new studio unit page is enabled.
-
- Always returns True. The legacy unit editor has been removed.
- This function will be removed in a follow-up cleanup once all callers are updated.
- """
- return True
-
# .. toggle_name: contentstore.mock_video_uploads
# .. toggle_implementation: WaffleFlag
diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py
index a9fc1c2720ab..b2c0e39df77c 100644
--- a/cms/djangoapps/contentstore/views/block.py
+++ b/cms/djangoapps/contentstore/views/block.py
@@ -9,6 +9,7 @@
from django.db import transaction
from django.http import Http404, HttpResponse
from django.utils.translation import gettext as _
+from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.http import require_http_methods
from opaque_keys.edx.keys import CourseKey
from openedx_authz.constants.permissions import COURSES_VIEW_COURSE
@@ -24,7 +25,7 @@
)
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import get_tags_count, usage_key_with_run
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
-from common.djangoapps.edxmako.shortcuts import render_to_string
+from common.djangoapps.edxmako.shortcuts import render_to_response, render_to_string
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
from common.djangoapps.util.json_request import JsonResponse, expect_json
from openedx.core.djangoapps.authz.constants import LegacyAuthoringPermission
@@ -40,6 +41,8 @@
)
from ..helpers import is_unit
+from ..utils import get_container_handler_context
+from .component import _get_item_in_course
from .preview import get_preview_fragment
__all__ = [
@@ -286,6 +289,38 @@ def xblock_view_handler(request, usage_key_string, view_name):
return HttpResponse(status=406)
+@xframe_options_exempt
+@require_http_methods(["GET"])
+@login_required
+def xblock_edit_view(request, usage_key_string):
+ """
+ Return rendered xblock edit view.
+
+ Allows editing of an XBlock specified by the usage key.
+ """
+ usage_key = usage_key_with_run(usage_key_string)
+ if not has_studio_read_access(request.user, usage_key.course_key):
+ raise PermissionDenied()
+
+ store = modulestore()
+
+ with store.bulk_operations(usage_key.course_key):
+ course, xblock, _, __ = _get_item_in_course(request, usage_key)
+ container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
+
+ fragment = get_preview_fragment(request, xblock, {})
+
+ hashed_resources = {
+ hash_resource(resource): resource._asdict() for resource in fragment.resources
+ }
+
+ container_handler_context.update({
+ "action_name": "edit",
+ "resources": list(hashed_resources.items()),
+ })
+
+ return render_to_response('container_editor.html', container_handler_context)
+
@require_http_methods("GET")
@login_required
diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py
index 016939f2004f..9e53a3aa0195 100644
--- a/cms/djangoapps/contentstore/views/tests/test_block.py
+++ b/cms/djangoapps/contentstore/views/tests/test_block.py
@@ -7,6 +7,7 @@
from unittest.mock import Mock, PropertyMock, patch
import ddt
+from bs4 import BeautifulSoup
from django.conf import settings
from django.http import Http404
from django.test import TestCase
@@ -4697,3 +4698,78 @@ def test_update_clobbers(self):
user_id=user.id,
)
self.check_updated(source_block, destination_block.location)
+
+
+class TestXblockEditView(CourseTestCase):
+ """
+ Test xblock_edit_view.
+ """
+
+ def setUp(self):
+ super().setUp()
+ self.chapter = self._create_block(self.course, "chapter", "Week 1")
+ self.sequential = self._create_block(self.chapter, "sequential", "Lesson 1")
+ self.vertical = self._create_block(self.sequential, "vertical", "Unit")
+ self.html = self._create_block(self.vertical, "html", "HTML")
+ self.child_container = self._create_block(
+ self.vertical, "split_test", "Split Test"
+ )
+ self.child_vertical = self._create_block(
+ self.child_container, "vertical", "Child Vertical"
+ )
+ self.video = self._create_block(self.child_vertical, "video", "My Video")
+ self.store = modulestore()
+
+ self.store.publish(self.vertical.location, self.user.id)
+
+ def _create_block(self, parent, category, display_name, **kwargs):
+ """
+ creates a block in the module store, without publishing it.
+ """
+ return BlockFactory.create(
+ parent=parent,
+ category=category,
+ display_name=display_name,
+ publish_item=False,
+ user_id=self.user.id,
+ **kwargs,
+ )
+
+ def test_xblock_edit_view(self):
+ url = reverse_usage_url("xblock_edit_handler", self.video.location)
+ resp = self.client.get_html(url)
+ self.assertEqual(resp.status_code, 200) # noqa: PT009
+
+ html_content = resp.content.decode(resp.charset)
+ self.assertIn("var decodedActionName = 'edit';", html_content) # noqa: PT009
+
+ def test_xblock_edit_view_contains_resources(self):
+ url = reverse_usage_url("xblock_edit_handler", self.video.location)
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200) # noqa: PT009
+
+ html_content = resp.content.decode(resp.charset)
+ soup = BeautifulSoup(html_content, "html.parser")
+
+ resource_links = [link["href"] for link in soup.find_all("link", {"rel": "stylesheet"})]
+ script_sources = [script["src"] for script in soup.find_all("script") if script.get("src")]
+
+ self.assertGreater(len(resource_links), 0, f"No CSS resources found in HTML. Found: {resource_links}") # noqa: PT009 # pylint: disable=line-too-long
+ self.assertGreater(len(script_sources), 0, f"No JS resources found in HTML. Found: {script_sources}") # noqa: PT009 # pylint: disable=line-too-long
+
+ def test_xblock_edit_view_contains_page_notification(self):
+ """
+ The page-notification element is required for XBlock runtime error
+ notifications (e.g. ORA validation errors) to be visible to the user.
+ """
+ url = reverse_usage_url("xblock_edit_handler", self.video.location)
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200) # noqa: PT009
+
+ html_content = resp.content.decode(resp.charset)
+ soup = BeautifulSoup(html_content, "html.parser")
+ self.assertIsNotNone( # noqa: PT009
+ soup.find(id="page-notification"),
+ "container_editor.html must include a #page-notification element "
+ "so that XBlock runtime error notifications are rendered.",
+ )
diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html
new file mode 100644
index 000000000000..3bea1e3d2118
--- /dev/null
+++ b/cms/templates/container_editor.html
@@ -0,0 +1,130 @@
+## coding=utf-8
+## mako
+
+## Pages currently use v1 styling by default. Once the Pattern Library
+## rollout has been completed, this default can be switched to v2.
+<%! main_css = "style-main-v1" %>
+
+<%! course_unit_mfe_iframe_css = "course-unit-mfe-iframe-bundle" %>
+
+## Standard imports
+<%namespace name='static' file='static_content.html'/>
+<%!
+from django.utils.translation import gettext as _
+from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
+from lms.djangoapps.branding import api as branding_api
+from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
+from cms.djangoapps.contentstore.helpers import xblock_type_display_name
+from openedx.core.release import RELEASE_LINE
+%>
+<%def name="online_help_token()">
+<%
+ return "container"
+%>
+%def>
+
+<%page expression_filter="h"/>
+
+
+
+ <%
+ jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE)
+ %>
+
+ % if getattr(settings, 'CAPTURE_CONSOLE_LOG', False):
+
+ % endif
+
+
+
+ % if settings.DEBUG:
+
+ % endif
+
+ <%static:css group='style-vendor'/>
+ <%static:css group='style-vendor-tinymce-content'/>
+ <%static:css group='style-vendor-tinymce-skin'/>
+ <%static:css group='${self.attr.course_unit_mfe_iframe_css}'/>
+
+ % if uses_bootstrap:
+
+ % else:
+ <%static:css group='${self.attr.main_css}'/>
+ % endif
+
+ <%include file="widgets/segment-io.html" />
+
+ <%block name="header_extras">
+ % for template_name in templates:
+
+ % endfor
+
+
+
+ % for _, resource in resources:
+ % if resource['kind'] == 'url' and resource['mimetype'] == 'text/css':
+
+ % endif
+ % endfor
+ %block>
+
+
+
+
+
+
+
+ <%static:js group='base_vendor' />
+ <%static:webpack entry='commons' />
+
+
+
+
+ <%block name='page_bundle'>
+ <%static:webpack entry="js/factories/container">
+ require(['js/models/xblock_info', 'js/views/modals/edit_xblock'],
+ function (XBlockInfo, EditXBlockModal) {
+ var decodedActionName = '${action_name|n, decode.utf8}';
+ var encodedXBlockDetails = ${xblock_info | n, dump_js_escaped_json};
+
+ if (decodedActionName === 'edit') {
+ var editXBlockModal = new EditXBlockModal();
+ var xblockInfoInstance = new XBlockInfo(encodedXBlockDetails);
+
+ editXBlockModal.edit([], xblockInfoInstance, {});
+ }
+ });
+ %static:webpack>
+ %block>
+
+
diff --git a/cms/urls.py b/cms/urls.py
index d0d917294d96..c0f96f489bb8 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -19,6 +19,7 @@
import openedx.core.djangoapps.lang_pref.views
from cms.djangoapps.contentstore import toggles
from cms.djangoapps.contentstore import views as contentstore_views
+from cms.djangoapps.contentstore.views.block import xblock_edit_view
from cms.djangoapps.contentstore.views.organization import OrganizationListView
from openedx.core import toggles as core_toggles
from openedx.core.apidocs import api_info
@@ -152,6 +153,8 @@
name='xblock_outline_handler'),
re_path(fr'^xblock/container/{settings.USAGE_KEY_PATTERN}$', contentstore_views.xblock_container_handler,
name='xblock_container_handler'),
+ re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/action/edit$', xblock_edit_view,
+ name='xblock_edit_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/(?P[^/]+)$', contentstore_views.xblock_view_handler,
name='xblock_view_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}?$', contentstore_views.xblock_handler,
From 12c14ab66b4a09f19e9d9eaa07f9feb9902c5b5c Mon Sep 17 00:00:00 2001
From: salmannawaz
Date: Wed, 21 Oct 2026 12:02:32 +0500
Subject: [PATCH 5/6] fix: fix multiple review comments
- container_handler: redirect any xblock with its own Studio page
(via xblock_has_own_studio_page) instead of only units/library-content/
split-test-children, fixing a 404 for nested containers (e.g. split_test)
that Studio still links to directly
- drop unused lms_link/preview_lms_link locals in container_handler
- repoint get_page_html at container_embed_handler (what the MFE actually
loads) instead of container_handler, which now only redirects
- restore container page test coverage for a unit, a split_test, and a
nested wrapper container via container_embed_handler
- add a split_test redirect test case to container_handler's redirect test
- remove dead test helpers _check_verticals/check_verticals (their only
caller was already removed)
- remove a stray blank line left in toggles.py
- update get_use_new_unit_page docstring to reflect that the Authoring MFE
never read this waffle flag key
- delete the dead unit_outline JS view, its child view, and template: their
only mount point (.wrapper-unit-overview) existed solely in the already-
removed legacy container.html
Co-Authored-By: Claude Sonnet 5
---
.../v1/serializers/course_waffle_flags.py | 7 ++-
.../contentstore/tests/test_contentstore.py | 9 ---
cms/djangoapps/contentstore/tests/utils.py | 8 ---
cms/djangoapps/contentstore/toggles.py | 1 -
.../contentstore/views/component.py | 9 +--
.../views/tests/test_container_page.py | 36 +++++++++++
.../contentstore/views/tests/utils.py | 10 ++--
.../views/pages/container_subviews_spec.js | 1 -
cms/static/js/views/pages/container.js | 11 +---
cms/static/js/views/unit_outline.js | 59 -------------------
cms/static/js/views/unit_outline_child.js | 34 -----------
.../js/mock/mock-container-page.underscore | 5 --
cms/templates/js/unit-outline.underscore | 28 ---------
13 files changed, 49 insertions(+), 169 deletions(-)
delete mode 100644 cms/static/js/views/unit_outline.js
delete mode 100644 cms/static/js/views/unit_outline_child.js
delete mode 100644 cms/templates/js/unit-outline.underscore
diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py
index d2b2791af710..5ae1d4c61d40 100644
--- a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py
+++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py
@@ -160,10 +160,11 @@ def get_use_new_unit_page(self, obj):
"""
Method to get the use_new_unit_page switch.
- Always returns True. The legacy unit editor has been removed.
- This method will be removed in a follow-up cleanup.
+ Always returns True. The legacy unit editor has been removed and the
+ Authoring MFE never read this key; it goes when the other hard-coded
+ fields in this serializer are dropped.
- See https://github.com/openedx/edx-platform/issues/36275
+ See https://github.com/openedx/openedx-platform/issues/37497
"""
return True
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index bc1b6b0be931..37ba0aae3d92 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -982,15 +982,6 @@ def test_prefetch_children(self):
with check_mongo_calls(2):
self.store.get_course(self.course.id, depth=2)
- def _check_verticals(self, locations):
- """ Test getting the editing HTML for each vertical. """
- # Assert is here to make sure that the course being tested actually has verticals (units) to check.
- self.assertGreater(len(locations), 0) # noqa: PT009
- for loc in locations:
- resp = self.client.get_html(get_url('container_handler', loc))
- self.assertEqual(resp.status_code, 200) # noqa: PT009
-
-
@ddt.ddt
class ContentStoreTest(ContentStoreTestCase):
"""
diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py
index 4b5e33fb65ec..b45d46db7e25 100644
--- a/cms/djangoapps/contentstore/tests/utils.py
+++ b/cms/djangoapps/contentstore/tests/utils.py
@@ -188,14 +188,6 @@ def assertCoursesEqual(self, course1_id, course2_id):
asset_son = asset.get('content_son', asset['_id'])
self.assertAssetsEqual(asset_son, course1_id, course2_id)
- def check_verticals(self, items):
- """ Test getting the editing HTML for each vertical. """
- # assert is here to make sure that the course being tested actually has verticals (units) to check.
- self.assertGreater(len(items), 0, "Course has no verticals (units) to check") # noqa: PT009
- for block in items:
- resp = self.client.get_html(get_url('container_handler', block.location))
- self.assertEqual(resp.status_code, 200) # noqa: PT009
-
def assertAssetsEqual(self, asset_son, course1_id, course2_id):
"""Verifies the asset of the given key has the same attributes in both given courses."""
content_store = contentstore()
diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py
index 1ac5889908bb..0472111145e7 100644
--- a/cms/djangoapps/contentstore/toggles.py
+++ b/cms/djangoapps/contentstore/toggles.py
@@ -194,7 +194,6 @@ def use_new_video_uploads_page(course_key):
LEGACY_STUDIO_COURSE_OUTLINE = CourseWaffleFlag('legacy_studio.course_outline', __name__)
-
# .. toggle_name: contentstore.mock_video_uploads
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index cbab7b844f6d..0f74d1c6fbdc 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -21,7 +21,7 @@
from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist
-from cms.djangoapps.contentstore.helpers import get_parent_if_split_test, is_library_content, is_unit
+from cms.djangoapps.contentstore.helpers import xblock_has_own_studio_page
from cms.djangoapps.contentstore.toggles import libraries_v2_enabled
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio
from common.djangoapps.edxmako.shortcuts import render_to_response
@@ -149,16 +149,13 @@ def container_handler(request, usage_key_string):
with modulestore().bulk_operations(usage_key.course_key):
try:
- course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
+ course, xblock, _, _ = _get_item_in_course(request, usage_key)
except ItemNotFoundError:
return HttpResponseBadRequest()
- if is_unit(xblock) or is_library_content(xblock):
+ if xblock_has_own_studio_page(xblock):
return redirect(get_unit_url(course.id, xblock.location))
- if split_xblock := get_parent_if_split_test(xblock):
- return redirect(get_unit_url(course.id, split_xblock.location))
-
raise Http404
diff --git a/cms/djangoapps/contentstore/views/tests/test_container_page.py b/cms/djangoapps/contentstore/views/tests/test_container_page.py
index 64abff538897..edb30bd2ff12 100644
--- a/cms/djangoapps/contentstore/views/tests/test_container_page.py
+++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py
@@ -7,6 +7,7 @@
from django.http import Http404
from django.test.client import RequestFactory
+from django.urls import reverse
from pytz import UTC
import cms.djangoapps.contentstore.views.component as views
@@ -56,6 +57,34 @@ def setUp(self):
self.store.publish(self.released_public_vertical.location, self.user.id)
self.store.publish(self.vertical.location, self.user.id)
+ def test_container_html(self):
+ """
+ Verify the chromeless container page (what the Authoring MFE loads via
+ container_embed_handler) renders the expected section tag for a unit,
+ a split test, and a non-vertical container nested inside it.
+ """
+ wrapper = self._create_block(self.child_container, "wrapper", "Wrapper")
+ self._create_block(wrapper, "html", "Child HTML")
+
+ for xblock in (self.vertical, self.child_container, wrapper):
+ assets_url = reverse(
+ 'assets_handler', kwargs={'course_key_string': str(xblock.location.course_key)}
+ )
+ expected_section_tag = (
+ ''.format(
+ xblock.location, assets_url
+ )
+ )
+ self._test_html_content(xblock, expected_section_tag)
+
+ def _test_html_content(self, xblock, expected_section_tag):
+ """
+ Get the HTML for a container page and verify the section tag is correct.
+ """
+ html = self.get_page_html(xblock)
+ self.assertIn(expected_section_tag, html) # noqa: PT009
+
def test_public_container_preview_html(self):
"""
Verify that a public xblock's container preview returns the expected HTML.
@@ -163,3 +192,10 @@ def test_container_page_with_valid_and_invalid_usage_key_string(self):
usage_key_string=str(self.vertical.location)
)
self.assertEqual(response.status_code, 302) # noqa: PT009
+
+ # A split_test block has children, so it has its own Studio page and also redirects.
+ response = views.container_handler(
+ request=request,
+ usage_key_string=str(self.child_container.location)
+ )
+ self.assertEqual(response.status_code, 302) # noqa: PT009
diff --git a/cms/djangoapps/contentstore/views/tests/utils.py b/cms/djangoapps/contentstore/views/tests/utils.py
index cbc51c0acc63..9c1a72c916a4 100644
--- a/cms/djangoapps/contentstore/views/tests/utils.py
+++ b/cms/djangoapps/contentstore/views/tests/utils.py
@@ -5,11 +5,9 @@
import json
-from cms.djangoapps.contentstore.tests.utils import CourseTestCase
+from cms.djangoapps.contentstore.tests.utils import CourseTestCase, get_url
from xmodule.modulestore.tests.factories import BlockFactory # pylint: disable=wrong-import-order
-from ...helpers import xblock_studio_url
-
class StudioPageTestCase(CourseTestCase):
"""
@@ -25,10 +23,10 @@ def setUp(self):
def get_page_html(self, xblock):
"""
- Returns the HTML for the page representing the xblock.
+ Returns the HTML for the page representing the xblock, as rendered by
+ container_embed_handler (the chromeless container page the Authoring MFE loads).
"""
- url = xblock_studio_url(xblock)
- self.assertIsNotNone(url) # noqa: PT009
+ url = get_url('container_embed_handler', xblock.location)
resp = self.client.get_html(url)
self.assertEqual(resp.status_code, 200) # noqa: PT009
return resp.content.decode(resp.charset)
diff --git a/cms/static/js/spec/views/pages/container_subviews_spec.js b/cms/static/js/spec/views/pages/container_subviews_spec.js
index 27175090f211..d850a74d77f7 100644
--- a/cms/static/js/spec/views/pages/container_subviews_spec.js
+++ b/cms/static/js/spec/views/pages/container_subviews_spec.js
@@ -40,7 +40,6 @@ describe('Container Subviews', function() {
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('publish-xblock');
TemplateHelpers.installTemplate('publish-history');
- TemplateHelpers.installTemplate('unit-outline');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
});
diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js
index d4afa4cab692..671c5bf83738 100644
--- a/cms/static/js/views/pages/container.js
+++ b/cms/static/js/views/pages/container.js
@@ -6,7 +6,7 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
'common/js/components/utils/view_utils', 'js/views/container', 'js/views/xblock',
'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal',
'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor',
- 'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils',
+ 'js/views/pages/container_subviews', 'js/views/utils/xblock_utils',
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
'js/views/utils/tagging_drawer_utils', 'js/utils/module', 'js/views/modals/preview_v2_library_changes',
'js/views/modals/select_v2_library_content'
@@ -15,7 +15,7 @@ function($, _, Backbone, gettext, BasePage,
ViewUtils, ContainerView, XBlockView,
AddXBlockComponent, EditXBlockModal, MoveXBlockModal,
XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
- ContainerSubviews, UnitOutlineView, XBlockUtils,
+ ContainerSubviews, XBlockUtils,
NotificationView, PromptView, TaggingDrawerUtils, ModuleUtils,
PreviewLibraryChangesModal, SelectV2LibraryContent) {
'use strict';
@@ -127,13 +127,6 @@ function($, _, Backbone, gettext, BasePage,
this.tagListView.setupMessageListener();
this.tagListView.render();
}
-
- this.unitOutlineView = new UnitOutlineView({
- el: this.$('.wrapper-unit-overview'),
- model: this.model
- });
- this.unitOutlineView.render();
-
}
if (this.options.isIframeEmbed) {
diff --git a/cms/static/js/views/unit_outline.js b/cms/static/js/views/unit_outline.js
deleted file mode 100644
index 8c6c4605af5b..000000000000
--- a/cms/static/js/views/unit_outline.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * The UnitOutlineView is used to render the Unit Outline component on the unit page. It shows
- * the ancestors of the unit along with its direct siblings. It also has a single "New Unit"
- * button to allow a new sibling unit to be added.
- */
-define(['underscore', 'js/views/xblock_outline', 'js/views/unit_outline_child'],
- function(_, XBlockOutlineView, UnitOutlineChildView) {
- var UnitOutlineView = XBlockOutlineView.extend({
- // takes XBlockInfo as a model
-
- templateName: 'unit-outline',
-
- render: function() {
- XBlockOutlineView.prototype.render.call(this);
- this.renderAncestors();
- return this;
- },
-
- renderAncestors: function() {
- var i, listElement,
- ancestors, ancestor,
- ancestorView = this,
- previousAncestor = null;
- if (this.model.get('ancestor_info')) {
- ancestors = this.model.get('ancestor_info').ancestors;
- listElement = this.getListElement();
- // Note: the ancestors are processed in reverse order because the tree wants to
- // start at the root, but the ancestors are ordered by closeness to the unit,
- // i.e. subsection and then section.
- for (i = ancestors.length - 1; i >= 0; i--) {
- ancestor = ancestors[i];
- ancestorView = this.createChildView(
- ancestor,
- previousAncestor,
- {parentView: ancestorView, currentUnitId: this.model.get('id')}
- );
- ancestorView.render();
- listElement.append(ancestorView.$el);
- previousAncestor = ancestor;
- listElement = ancestorView.getListElement();
- }
- }
- return ancestorView;
- },
-
- getChildViewClass: function() {
- return UnitOutlineChildView;
- },
-
- getTemplateContext: function() {
- return _.extend(
- XBlockOutlineView.prototype.getTemplateContext.call(this),
- {currentUnitId: this.model.get('id')}
- );
- }
- });
-
- return UnitOutlineView;
- }); // end define();
diff --git a/cms/static/js/views/unit_outline_child.js b/cms/static/js/views/unit_outline_child.js
deleted file mode 100644
index f327c6367ebf..000000000000
--- a/cms/static/js/views/unit_outline_child.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * The UnitOutlineChildView is used to render each Section,
- * Subsection, and Unit within the Unit Outline component on the unit
- * page.
- */
-define(['underscore', 'js/views/xblock_outline'],
- function(_, XBlockOutlineView) {
- var UnitOutlineChildView = XBlockOutlineView.extend({
- initialize: function() {
- XBlockOutlineView.prototype.initialize.call(this);
- this.currentUnitId = this.options.currentUnitId;
- },
-
- getTemplateContext: function() {
- return _.extend(
- XBlockOutlineView.prototype.getTemplateContext.call(this),
- {currentUnitId: this.currentUnitId}
- );
- },
-
- getChildViewClass: function() {
- return UnitOutlineChildView;
- },
-
- createChildView: function(childInfo, parentInfo, options) {
- options = _.isUndefined(options) ? {} : options;
- return XBlockOutlineView.prototype.createChildView.call(
- this, childInfo, parentInfo, _.extend(options, {currentUnitId: this.currentUnitId})
- );
- }
- });
-
- return UnitOutlineChildView;
- }); // end define()
diff --git a/cms/templates/js/mock/mock-container-page.underscore b/cms/templates/js/mock/mock-container-page.underscore
index 620f8995f0a6..cd1acd3815d9 100644
--- a/cms/templates/js/mock/mock-container-page.underscore
+++ b/cms/templates/js/mock/mock-container-page.underscore
@@ -75,11 +75,6 @@
Tip: Use this ID when you create links to this unit from other course content. You enter the ID in the URL field.
-
-
Location in Course Outline
-
-
-
diff --git a/cms/templates/js/unit-outline.underscore b/cms/templates/js/unit-outline.underscore
deleted file mode 100644
index 8ea1ddf1098a..000000000000
--- a/cms/templates/js/unit-outline.underscore
+++ /dev/null
@@ -1,28 +0,0 @@
-<% if (parentInfo) { %>
-
-
-<% } %>
-
-
-
-
- <% if (childType) { %>
-
- <% } %>
-
-
-<% if (parentInfo) { %>
-
-<% } %>
From 8edd15ef57dd4c87d5130a29909f9bb127c4347a Mon Sep 17 00:00:00 2001
From: salmannawaz
Date: Wed, 21 Oct 2026 15:36:59 +0500
Subject: [PATCH 6/6] fix: remove dangling unit-outline entry from
CONTAINER_TEMPLATES
CONTAINER_TEMPLATES is rendered into every container_embed_handler,
xblock_edit_view, and library page via get_container_handler_context.
It still listed "unit-outline" after the underlying unit-outline.underscore
template file was deleted, breaking those pages with
TemplateDoesNotExist: js/unit-outline.underscore.
Co-Authored-By: Claude Sonnet 5
---
cms/djangoapps/contentstore/views/component.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 0f74d1c6fbdc..bbb19f1ddf94 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -68,7 +68,7 @@
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-support-legend", "add-xblock-component-support-level", "add-xblock-component-menu-problem",
"xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history", "tag-list",
- "unit-outline", "container-message", "container-access", "license-selector", "copy-clipboard-button",
+ "container-message", "container-access", "license-selector", "copy-clipboard-button",
"edit-title-button", "edit-upstream-alert",
]