Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,14 @@ def get_use_new_course_outline_page(self, obj):

def get_use_new_unit_page(self, obj):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

use_new_unit_page is kept in the serializer response for now as a field returning True. Once the MFE confirms it no longer reads this key from the waffle flags API, the field and its method can be removed in a follow-up PR.

"""
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

frontend-app-authoring has never read this key (nothing in its history matches use_new_unit_page), so there is nothing to wait on from the MFE. Point this at the ticket that tracks dropping the hard-coded fields, like get_use_new_home_page above does.

Suggested change
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
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/openedx-platform/issues/37497

"""
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):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
62 changes: 4 additions & 58 deletions cms/djangoapps/contentstore/tests/test_contentstore.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -30,11 +25,9 @@
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
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
Expand Down Expand Up @@ -568,44 +561,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("<script>alert(1)</script>", "alert('hi')", "</script><script>alert(1)</script>")
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'],
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: two consecutive blank lines left here after the deleted methods above — worth squashing to one.

@ddt.data('/Fake/asset/displayname', '\\Fake\\asset\\displayname')
def test_export_on_invalid_displayname(self, invalid_displayname):
Expand Down Expand Up @@ -700,14 +655,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])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This was the only caller of _check_verticals (line 985 in this file), so delete that too. Its 200 assertion is wrong now that the handler redirects. Not a blocker but while you're here, check_verticals in tests/utils.py has no callers either.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: same as above — two consecutive blank lines left after the deleted methods here too.

def _get_draft_counts(self, item): # pylint: disable=missing-function-docstring
cnt = 1 if not self.store.has_published_version(item) else 0
Expand Down Expand Up @@ -1520,11 +1467,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. """
Expand Down
19 changes: 5 additions & 14 deletions cms/djangoapps/contentstore/toggles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Link #35261 from the description and add a line to the operator notes for the release this lands in, since legacy_studio.unit_editor stops being read here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@feanil Could you please elaborate on this comment a bit more? I’m not sure I fully understand what you mean.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure. #35261 is the DEPR ticket for the legacy unit editor, add it to the References list in the description so the ticket and the PR that closes it point at each other. Then add a line to the Willow operator notes page, https://openedx.atlassian.net/wiki/spaces/COMM/pages/6476169217/Willow+-+Operator+Dev+Notes, saying the legacy_studio.unit_editor waffle flag is removed and can be deleted from any site that still has it set.

While you're in the description, the "until the MFE confirms it no longer reads this key" lines are out of date, the MFE never read it.



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


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

# .. toggle_name: contentstore.mock_video_uploads
Expand Down
12 changes: 4 additions & 8 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 1 addition & 36 deletions cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -41,8 +40,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

__all__ = [
Expand Down Expand Up @@ -289,38 +286,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
Expand Down
59 changes: 26 additions & 33 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_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
Expand Down Expand Up @@ -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/<usage_key>.
"""
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: the LMS links aren't used anymore.

Suggested change
course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
course, xblock, _, _ = _get_item_in_course(request, usage_key)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

On master a /container/<key> request for a block with children that isn't a unit, library content or a split test child renders the legacy container page. Now it is a 404, but Studio still hands out those URLs: the "Used in" links on the group configurations page point at /container/<split_test> (course_group_config.py), and the clipboard source_edit_url and the library component usage links point at /container/<parent> when the parent is a nested container. The MFE renders a container page for any block with children since openedx/frontend-app-authoring#3058, so redirect those blocks there too.

With this change on a scratch course, /container/ gives 302 for the unit, a split_test and a wrapper block and 404 for a problem. The current head gives 302/404/404/404 and master gives 302/200/200/200.

Suggested change
raise Http404
if xblock_has_own_studio_page(xblock):
return redirect(get_unit_url(course.id, xblock.location))
raise Http404

xblock_has_own_studio_page is in helpers.py next to the imports at the top of this file. Then add a test for the split test case here since test_container_on_container_html went away.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

Comment on lines +156 to +159

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A split test's group verticals 404 now. The xblock_has_own_studio_page helper returns False for a vertical whose parent is a split test, so dropping the get_parent_if_split_test branch loses the redirect they had. On a test course /container/<group vertical> gives a 302 to the split test's page on master and a 404 here, and Studio still links there from the deprecated-blocks list and the import error page.

Suggested change
if xblock_has_own_studio_page(xblock):
return redirect(get_unit_url(course.id, xblock.location))
raise Http404
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

Then add a self.child_vertical case next to the split test one in test_container_page_with_valid_and_invalid_usage_key_string so this stays covered.



@require_GET
Expand All @@ -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
Expand Down
Loading
Loading