From dd3cf83a64c9ca15dc020b06e44ed38f40204f97 Mon Sep 17 00:00:00 2001 From: Fox Danger Piacenti Date: Tue, 10 Mar 2026 14:03:02 -0500 Subject: [PATCH 1/2] feat: pdf editor authoring flag --- .../v1/serializers/course_waffle_flags.py | 7 +++++++ .../v1/views/tests/test_course_waffle_flags.py | 1 + cms/djangoapps/contentstore/toggles.py | 17 +++++++++++++++++ cms/static/js/views/pages/container.js | 8 +++++--- cms/templates/container.html | 4 +++- cms/templates/studio_xblock_wrapper.html | 4 +++- 6 files changed, 36 insertions(+), 5 deletions(-) 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 c3e9342a1c95..46f8cebd140d 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 @@ -21,6 +21,7 @@ class CourseWaffleFlagsSerializer(serializers.Serializer): use_new_import_page = serializers.SerializerMethodField() use_new_export_page = serializers.SerializerMethodField() use_new_files_uploads_page = serializers.SerializerMethodField() + use_new_pdf_editor = serializers.SerializerMethodField() use_new_video_uploads_page = serializers.SerializerMethodField() use_new_course_outline_page = serializers.SerializerMethodField() use_new_unit_page = serializers.SerializerMethodField() @@ -120,6 +121,12 @@ def get_use_new_files_uploads_page(self, obj): """ return True + def get_use_new_pdf_editor(self, obj): + """ + Method to get the use_new_pdf_editor switch + """ + return toggles.use_new_pdf_editor() + def get_use_new_video_uploads_page(self, obj): """ Method to get the use_new_video_uploads_page switch. diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py index fd7b5882cf97..39783e191da4 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py @@ -35,6 +35,7 @@ class CourseWaffleFlagsViewTest(CourseTestCase): "use_new_unit_page": True, "use_new_updates_page": True, "use_new_video_uploads_page": False, + "use_new_pdf_editor": True, "use_react_markdown_editor": False, "use_video_gallery_flow": False, "enable_course_optimizer_check_prev_run_links": False, diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py index 7aadfe64e94d..b0c4ba032aca 100644 --- a/cms/djangoapps/contentstore/toggles.py +++ b/cms/djangoapps/contentstore/toggles.py @@ -105,6 +105,23 @@ def use_new_video_editor(course_key): return not LEGACY_STUDIO_VIDEO_EDITOR.is_enabled(course_key) +# .. toggle_name: legacy_studio.pdf_editor +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Use the PDF XBlock's studio_view instead of the new React-based editor. You may wish to do +# this if you run a custom PDF block. +# .. toggle_use_cases: opt_out +# .. toggle_creation_date: 2026-03-10 +LEGACY_STUDIO_PDF_EDITOR = WaffleFlag('legacy_studio.pdf_editor', __name__) + + +def use_new_pdf_editor(): + """ + Returns a boolean = true if new video editor is enabled + """ + return not LEGACY_STUDIO_PDF_EDITOR.is_enabled() + + # .. toggle_name: new_core_editors.use_video_gallery_flow # .. toggle_implementation: WaffleFlag # .. toggle_default: False diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 7982740ad6b7..543503a598df 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -491,14 +491,14 @@ function($, _, Backbone, gettext, BasePage, if ($target.closest('button, a, input, label, .actions-list').length) { return; } - + var $wrapper = $target.closest('.studio-xblock-wrapper'); // Deselect all other xblocks this.$('.studio-xblock-wrapper.is-selected').not($wrapper).removeClass('is-selected'); $wrapper.toggleClass('is-selected'); - + if (this.options.isIframeEmbed) { const contentId = this.findXBlockElement(event.target).data('locator'); this.postMessageToParent({ @@ -539,11 +539,13 @@ function($, _, Backbone, gettext, BasePage, const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions'); var useNewVideoEditor = primaryHeader.attr('use-new-editor-video'), - blockType = primaryHeader.attr('data-block-type'); + blockType = primaryHeader.attr('data-block-type'), + useNewPdfEditor = primaryHeader.attr('use-new-editor-pdf'); if((blockType === 'html') || (useNewVideoEditor === 'True' && blockType === 'video') || (blockType === 'problem') + || (useNewPdfEditor === 'True' && blockType === 'pdf') ) { var destinationUrl = primaryHeader.attr('authoring_MFE_base_url') + '/' + blockType diff --git a/cms/templates/container.html b/cms/templates/container.html index 047578be7595..09fcf19047e7 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -13,7 +13,7 @@ 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_video_editor, use_video_gallery_flow +from cms.djangoapps.contentstore.toggles import use_new_pdf_editor, use_new_video_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 @@ -112,6 +112,7 @@ <% use_new_editor_video = use_new_video_editor(xblock_locator.course_key) +use_new_editor_pdf = use_new_pdf_editor() use_new_video_gallery_flow = use_video_gallery_flow() %> @@ -167,6 +168,7 @@

<% use_new_editor_video = use_new_video_editor(xblock.context_key) +use_new_editor_pdf = use_new_pdf_editor() use_new_video_gallery_flow = use_video_gallery_flow() use_tagging = not is_tagging_feature_disabled() xblock_url = xblock_studio_url(xblock) @@ -83,6 +84,7 @@ % endif " use-new-editor-video = ${use_new_editor_video} + use-new-editor-pdf = ${use_new_editor_pdf} use-video-gallery-flow = ${use_new_video_gallery_flow} authoring_MFE_base_url = ${get_editor_page_base_url(xblock.location.course_key)} data-block-type = ${xblock.scope_ids.block_type} From ec27f0af9fa2eb9ee57e378e8cf43950469bc3b7 Mon Sep 17 00:00:00 2001 From: Fox Danger Piacenti Date: Fri, 3 Apr 2026 15:33:13 -0500 Subject: [PATCH 2/2] fix: permit library assets to be iframed from permitted sources --- .../core/djangoapps/content_libraries/rest_api/blocks.py | 9 +++++++++ .../content_libraries/tests/test_static_assets.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/openedx/core/djangoapps/content_libraries/rest_api/blocks.py b/openedx/core/djangoapps/content_libraries/rest_api/blocks.py index 86bd8f6112dd..1a6358a3168e 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/blocks.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/blocks.py @@ -3,6 +3,7 @@ """ import edx_api_doc_tools as apidocs from django.core.exceptions import ObjectDoesNotExist +from django.conf import settings from django.db.transaction import non_atomic_requests from django.http import Http404, HttpResponse, StreamingHttpResponse from django.urls import reverse @@ -18,6 +19,7 @@ from rest_framework.response import Response from rest_framework.views import APIView +import openedx.core.djangoapps.site_configuration.helpers as configuration_helpers from openedx.core.djangoapps.content_libraries import api, permissions from openedx.core.djangoapps.content_libraries.rest_api import serializers from openedx.core.djangoapps.xblock import api as xblock_api @@ -435,6 +437,13 @@ def get_component_version_asset(request, component_version_uuid, asset_path): # not needed there (the reverse-proxy would have direct access to the file). headers['Content-Length'] = media.size + # Some assets, such as PDFs, need to be embedded in an iFrame in the MFE + # studio. Permit this, so long as the file is in the cors_origin_whitelist. + cors_origin_whitelist = configuration_helpers.get_value( + 'CORS_ORIGIN_WHITELIST', getattr(settings, 'CORS_ORIGIN_WHITELIST', []), + ) + headers["Content-Security-Policy"] = f"frame-ancestors 'self' {' '.join(cors_origin_whitelist)};" + if request.method == "HEAD": return HttpResponse(headers=headers) diff --git a/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py b/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py index 3859224cfd51..b98487e43ad6 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_static_assets.py @@ -3,6 +3,7 @@ """ from uuid import UUID +from django.test.utils import override_settings from opaque_keys.edx.keys import UsageKey from common.djangoapps.student.tests.factories import UserFactory @@ -117,6 +118,7 @@ def check_download(): check_download() +@override_settings(CORS_ORIGIN_WHITELIST=["https://example.com/", "https://example2.com/"]) @skip_unless_cms class ContentLibrariesComponentVersionAssetTest(ContentLibrariesRestApiTest): """ @@ -146,6 +148,11 @@ def test_good_responses(self): f"/library_assets/component_versions/{self.draft_component_version.uuid}/static/test.svg" ) assert good_head_response.headers == get_response.headers + assert "Content-Security-Policy" in get_response.headers + assert get_response.headers["Content-Security-Policy"] == ( + "frame-ancestors 'self' https://example.com/ https://example2.com/;" + ) + def test_missing(self): """Test asset requests that should 404."""