Skip to content

Commit 8dfc037

Browse files
authored
feat: pdf editor authoring flag (#38148)
* feat: pdf editor authoring flag * fix: permit library assets to be iframed from permitted sources
1 parent 5cfc6c8 commit 8dfc037

8 files changed

Lines changed: 52 additions & 5 deletions

File tree

cms/djangoapps/contentstore/rest_api/v1/serializers/course_waffle_flags.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class CourseWaffleFlagsSerializer(serializers.Serializer):
2121
use_new_import_page = serializers.SerializerMethodField()
2222
use_new_export_page = serializers.SerializerMethodField()
2323
use_new_files_uploads_page = serializers.SerializerMethodField()
24+
use_new_pdf_editor = serializers.SerializerMethodField()
2425
use_new_video_uploads_page = serializers.SerializerMethodField()
2526
use_new_course_outline_page = serializers.SerializerMethodField()
2627
use_new_unit_page = serializers.SerializerMethodField()
@@ -120,6 +121,12 @@ def get_use_new_files_uploads_page(self, obj):
120121
"""
121122
return True
122123

124+
def get_use_new_pdf_editor(self, obj):
125+
"""
126+
Method to get the use_new_pdf_editor switch
127+
"""
128+
return toggles.use_new_pdf_editor()
129+
123130
def get_use_new_video_uploads_page(self, obj):
124131
"""
125132
Method to get the use_new_video_uploads_page switch.

cms/djangoapps/contentstore/rest_api/v1/views/tests/test_course_waffle_flags.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class CourseWaffleFlagsViewTest(CourseTestCase):
3535
"use_new_unit_page": True,
3636
"use_new_updates_page": True,
3737
"use_new_video_uploads_page": False,
38+
"use_new_pdf_editor": True,
3839
"use_react_markdown_editor": False,
3940
"use_video_gallery_flow": False,
4041
"enable_course_optimizer_check_prev_run_links": False,

cms/djangoapps/contentstore/toggles.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,23 @@ def use_new_video_editor(course_key):
105105
return not LEGACY_STUDIO_VIDEO_EDITOR.is_enabled(course_key)
106106

107107

108+
# .. toggle_name: legacy_studio.pdf_editor
109+
# .. toggle_implementation: WaffleFlag
110+
# .. toggle_default: False
111+
# .. toggle_description: Use the PDF XBlock's studio_view instead of the new React-based editor. You may wish to do
112+
# this if you run a custom PDF block.
113+
# .. toggle_use_cases: opt_out
114+
# .. toggle_creation_date: 2026-03-10
115+
LEGACY_STUDIO_PDF_EDITOR = WaffleFlag('legacy_studio.pdf_editor', __name__)
116+
117+
118+
def use_new_pdf_editor():
119+
"""
120+
Returns a boolean = true if new video editor is enabled
121+
"""
122+
return not LEGACY_STUDIO_PDF_EDITOR.is_enabled()
123+
124+
108125
# .. toggle_name: new_core_editors.use_video_gallery_flow
109126
# .. toggle_implementation: WaffleFlag
110127
# .. toggle_default: False

cms/static/js/views/pages/container.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -491,14 +491,14 @@ function($, _, Backbone, gettext, BasePage,
491491
if ($target.closest('button, a, input, label, .actions-list').length) {
492492
return;
493493
}
494-
494+
495495
var $wrapper = $target.closest('.studio-xblock-wrapper');
496496

497497
// Deselect all other xblocks
498498
this.$('.studio-xblock-wrapper.is-selected').not($wrapper).removeClass('is-selected');
499499

500500
$wrapper.toggleClass('is-selected');
501-
501+
502502
if (this.options.isIframeEmbed) {
503503
const contentId = this.findXBlockElement(event.target).data('locator');
504504
this.postMessageToParent({
@@ -539,11 +539,13 @@ function($, _, Backbone, gettext, BasePage,
539539
const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions');
540540

541541
var useNewVideoEditor = primaryHeader.attr('use-new-editor-video'),
542-
blockType = primaryHeader.attr('data-block-type');
542+
blockType = primaryHeader.attr('data-block-type'),
543+
useNewPdfEditor = primaryHeader.attr('use-new-editor-pdf');
543544

544545
if((blockType === 'html')
545546
|| (useNewVideoEditor === 'True' && blockType === 'video')
546547
|| (blockType === 'problem')
548+
|| (useNewPdfEditor === 'True' && blockType === 'pdf')
547549
) {
548550
var destinationUrl = primaryHeader.attr('authoring_MFE_base_url')
549551
+ '/' + blockType

cms/templates/container.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from django.utils.translation import gettext as _
1414

1515
from cms.djangoapps.contentstore.helpers import xblock_studio_url, xblock_type_display_name
16-
from cms.djangoapps.contentstore.toggles import use_new_video_editor, use_video_gallery_flow
16+
from cms.djangoapps.contentstore.toggles import use_new_pdf_editor, use_new_video_editor, use_video_gallery_flow
1717
from cms.djangoapps.contentstore.utils import get_editor_page_base_url
1818
from openedx.core.djangolib.js_utils import (
1919
dump_js_escaped_json, js_escaped_string
@@ -112,6 +112,7 @@
112112

113113
<%
114114
use_new_editor_video = use_new_video_editor(xblock_locator.course_key)
115+
use_new_editor_pdf = use_new_pdf_editor()
115116
use_new_video_gallery_flow = use_video_gallery_flow()
116117
%>
117118

@@ -167,6 +168,7 @@ <h1 class="page-header-title xblock-field-value incontext-editor-value"><span cl
167168

168169
<nav class="nav-actions" aria-label="${_('Page Actions')}"
169170
use-new-editor-video = ${use_new_editor_video}
171+
use-new-editor-pdf = ${use_new_editor_pdf}
170172
use-video-gallery-flow = ${use_new_video_gallery_flow}
171173
authoring_MFE_base_url = ${get_editor_page_base_url(xblock_locator.course_key)}
172174
data-block-type = ${xblock.scope_ids.block_type}

cms/templates/studio_xblock_wrapper.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
from openedx.core.djangolib.js_utils import (
99
dump_js_escaped_json, js_escaped_string
1010
)
11-
from cms.djangoapps.contentstore.toggles import use_new_video_editor, use_video_gallery_flow
11+
from cms.djangoapps.contentstore.toggles import use_new_pdf_editor, use_new_video_editor, use_video_gallery_flow
1212
from cms.lib.xblock.upstream_sync import UpstreamLink
1313
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
1414
%>
1515
<%
1616
use_new_editor_video = use_new_video_editor(xblock.context_key)
17+
use_new_editor_pdf = use_new_pdf_editor()
1718
use_new_video_gallery_flow = use_video_gallery_flow()
1819
use_tagging = not is_tagging_feature_disabled()
1920
xblock_url = xblock_studio_url(xblock)
@@ -83,6 +84,7 @@
8384
% endif
8485
"
8586
use-new-editor-video = ${use_new_editor_video}
87+
use-new-editor-pdf = ${use_new_editor_pdf}
8688
use-video-gallery-flow = ${use_new_video_gallery_flow}
8789
authoring_MFE_base_url = ${get_editor_page_base_url(xblock.location.course_key)}
8890
data-block-type = ${xblock.scope_ids.block_type}

openedx/core/djangoapps/content_libraries/rest_api/blocks.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
import edx_api_doc_tools as apidocs
55
from django.core.exceptions import ObjectDoesNotExist
6+
from django.conf import settings
67
from django.db.transaction import non_atomic_requests
78
from django.http import Http404, HttpResponse, StreamingHttpResponse
89
from django.urls import reverse
@@ -18,6 +19,7 @@
1819
from rest_framework.response import Response
1920
from rest_framework.views import APIView
2021

22+
import openedx.core.djangoapps.site_configuration.helpers as configuration_helpers
2123
from openedx.core.djangoapps.content_libraries import api, permissions
2224
from openedx.core.djangoapps.content_libraries.rest_api import serializers
2325
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):
435437
# not needed there (the reverse-proxy would have direct access to the file).
436438
headers['Content-Length'] = media.size
437439

440+
# Some assets, such as PDFs, need to be embedded in an iFrame in the MFE
441+
# studio. Permit this, so long as the file is in the cors_origin_whitelist.
442+
cors_origin_whitelist = configuration_helpers.get_value(
443+
'CORS_ORIGIN_WHITELIST', getattr(settings, 'CORS_ORIGIN_WHITELIST', []),
444+
)
445+
headers["Content-Security-Policy"] = f"frame-ancestors 'self' {' '.join(cors_origin_whitelist)};"
446+
438447
if request.method == "HEAD":
439448
return HttpResponse(headers=headers)
440449

openedx/core/djangoapps/content_libraries/tests/test_static_assets.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from uuid import UUID
55

6+
from django.test.utils import override_settings
67
from opaque_keys.edx.keys import UsageKey
78

89
from common.djangoapps.student.tests.factories import UserFactory
@@ -115,6 +116,7 @@ def check_download():
115116
check_download()
116117

117118

119+
@override_settings(CORS_ORIGIN_WHITELIST=["https://example.com/", "https://example2.com/"])
118120
@skip_unless_cms
119121
class ContentLibrariesComponentVersionAssetTest(ContentLibrariesRestApiTest):
120122
"""
@@ -144,6 +146,11 @@ def test_good_responses(self):
144146
f"/library_assets/component_versions/{self.draft_component_version.uuid}/static/test.svg"
145147
)
146148
assert good_head_response.headers == get_response.headers
149+
assert "Content-Security-Policy" in get_response.headers
150+
assert get_response.headers["Content-Security-Policy"] == (
151+
"frame-ancestors 'self' https://example.com/ https://example2.com/;"
152+
)
153+
147154

148155
def test_missing(self):
149156
"""Test asset requests that should 404."""

0 commit comments

Comments
 (0)