Skip to content

Commit bb6ac5a

Browse files
authored
feat: new unstable URL to render a v2 library xblock in an iframe (#35473)
1 parent 4e5a3b2 commit bb6ac5a

File tree

4 files changed

+60
-3
lines changed

4 files changed

+60
-3
lines changed

cms/templates/content_libraries/xblock_iframe.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
<base target="_blank">
77
<meta charset="UTF-8">
88
<!-- gettext & XBlock JS i18n code -->
9-
<script type="text/javascript" src="{{ lms_root_url }}/static/js/i18n/en/djangojs.js"></script>
9+
{% if is_development %}
10+
<!-- in development, the djangojs file isn't available so use fallback-->
11+
<script type="text/javascript" src="{{ lms_root_url }}/static/js/src/gettext_fallback.js"></script>
12+
{% else %}
13+
<script type="text/javascript" src="{{ lms_root_url }}/static/js/i18n/en/djangojs.js"></script>
14+
{% endif %}
1015
<!-- Most XBlocks require jQuery: -->
1116
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
1217
<!-- The Video XBlock requires "ajaxWithPrefix" -->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../cms/templates/content_libraries/xblock_iframe.html

openedx/core/djangoapps/xblock/rest_api/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@
2929
),
3030
])),
3131
])),
32+
path('xblocks/v2/<str:usage_key_str>/', include([
33+
# render one of this XBlock's views (e.g. student_view) for embedding in an iframe
34+
# NOTE: this endpoint is **unstable** and subject to changes after Sumac
35+
re_path(r'^embed/(?P<view_name>[\w\-]+)/$', views.embed_block_view),
36+
])),
3237
]

openedx/core/djangoapps/xblock/rest_api/views.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
"""
22
Views that implement a RESTful API for interacting with XBlocks.
33
"""
4+
import itertools
5+
import json
46

57
from common.djangoapps.util.json_request import JsonResponse
68
from corsheaders.signals import check_request_enabled
9+
from django.conf import settings
710
from django.contrib.auth import get_user_model
811
from django.db.transaction import atomic
912
from django.http import Http404
13+
from django.shortcuts import render
1014
from django.utils.translation import gettext as _
1115
from django.views.decorators.clickjacking import xframe_options_exempt
1216
from django.views.decorators.csrf import csrf_exempt
@@ -21,6 +25,7 @@
2125

2226
from opaque_keys import InvalidKeyError
2327
from opaque_keys.edx.keys import UsageKey
28+
import openedx.core.djangoapps.site_configuration.helpers as configuration_helpers
2429
from openedx.core.djangoapps.xblock.learning_context.manager import get_learning_context_impl
2530
from openedx.core.lib.api.view_utils import view_auth_classes
2631
from ..api import (
@@ -87,6 +92,47 @@ def render_block_view(request, usage_key_str, view_name):
8792
return Response(response_data)
8893

8994

95+
@api_view(['GET'])
96+
@view_auth_classes(is_authenticated=False)
97+
@permission_classes((permissions.AllowAny, )) # Permissions are handled at a lower level, by the learning context
98+
@xframe_options_exempt
99+
def embed_block_view(request, usage_key_str, view_name):
100+
"""
101+
Render the given XBlock in an <iframe>
102+
103+
Unstable - may change after Sumac
104+
"""
105+
try:
106+
usage_key = UsageKey.from_string(usage_key_str)
107+
except InvalidKeyError as e:
108+
raise NotFound(invalid_not_found_fmt.format(usage_key=usage_key_str)) from e
109+
110+
try:
111+
block = load_block(usage_key, request.user)
112+
except NoSuchUsage as exc:
113+
raise NotFound(f"{usage_key} not found") from exc
114+
115+
fragment = _render_block_view(block, view_name, request.user)
116+
handler_urls = {
117+
str(key): _get_handler_url(key, 'handler_name', request.user)
118+
for key in itertools.chain([block.scope_ids.usage_id], getattr(block, 'children', []))
119+
}
120+
lms_root_url = configuration_helpers.get_value('LMS_ROOT_URL', settings.LMS_ROOT_URL)
121+
context = {
122+
'fragment': fragment,
123+
'handler_urls_json': json.dumps(handler_urls),
124+
'lms_root_url': lms_root_url,
125+
'is_development': settings.DEBUG,
126+
}
127+
response = render(request, 'xblock_v2/xblock_iframe.html', context, content_type='text/html')
128+
129+
# Only allow this iframe be embedded if the parent is in the CORS_ORIGIN_WHITELIST
130+
cors_origin_whitelist = configuration_helpers.get_value('CORS_ORIGIN_WHITELIST', settings.CORS_ORIGIN_WHITELIST)
131+
response["Content-Security-Policy"] = f"frame-ancestors 'self' {' '.join(cors_origin_whitelist)};"
132+
133+
return response
134+
135+
90136
@api_view(['GET'])
91137
@view_auth_classes(is_authenticated=False)
92138
def get_handler_url(request, usage_key_str, handler_name):
@@ -185,8 +231,8 @@ class BlockFieldsView(APIView):
185231
View to get/edit the field values of an XBlock as JSON (in the v2 runtime)
186232
187233
This class mimics the functionality of xblock_handler in block.py (for v1 xblocks), but for v2 xblocks.
188-
However, it only implements the exact subset of functionality needed to support the v2 editors (from
189-
the frontend-lib-content-components project). As such, it only supports GET and POST, and only the
234+
However, it only implements the exact subset of functionality needed to support the v2 editors (in
235+
the Course Authoring MFE). As such, it only supports GET and POST, and only the
190236
POSTing of data/metadata fields.
191237
"""
192238

0 commit comments

Comments
 (0)