From e4a1e4136745ce02fc7424a3226f30d4c0965c8d Mon Sep 17 00:00:00 2001 From: Sagirov Evgeniy <34642612+UvgenGen@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:03:07 +0300 Subject: [PATCH] feat: new Studio view for rendering a Unit in an iframe [FC-0070] The first attempt at creating a new MFE-driven page for Studio Unit rendering involved rendering each XBlock separately in its own iframe. This turned out to be prohibitively slow because of the many redundant assets and JavaScript processing (e.g. MathJax) that happens for each XBlock component. In order to mitigate some of these issues, we decided to try a hybrid approach where we render the entire Unit's worth of XBlocks at once on the server side in a Studio view + template, and then invoke that from frontend-app-authoring as an iframe. The frontend-app-authoring MFE would still be responsible for displaying most of the interactive UI, but the per-component actions like "edit" would be triggered by buttons on the server-rendered Unit display. When one of those buttons is pressed, the server-rendered UI code in the iframe would use postMessage to communicate to the frontend-app-authoring MFE, which would then display the appropriate actions. To make this work, we're making a new view and template that copies a lot of existing code used to display the Unit in pre-MFE Studio, and then modifying that to remove things like the header/footer so that it can be invoked from an iframe. This entire design is a compromise in order to do as much of the UI development in frontend-app-authoring as possible while keeping XBlock rendering performance tolerable. We hope that we can find better solutions for this later. Authored-by: Sagirov Eugeniy --- .../contentstore/views/component.py | 34 +- .../views/tests/test_container_page.py | 58 ++ cms/envs/common.py | 6 + cms/static/images/pencil-icon.svg | 3 + cms/static/js/views/pages/container.js | 168 +++-- .../sass/course-unit-mfe-iframe-bundle.scss | 651 ++++++++++++++++++ .../elements/_course-unit-mfe-iframe.scss | 65 ++ .../partials/cms/theme/_variables-v1.scss | 9 + cms/templates/container_chromeless.html | 278 ++++++++ cms/urls.py | 2 + 10 files changed, 1197 insertions(+), 77 deletions(-) create mode 100644 cms/static/images/pencil-icon.svg create mode 100644 cms/static/sass/course-unit-mfe-iframe-bundle.scss create mode 100644 cms/static/sass/elements/_course-unit-mfe-iframe.scss create mode 100644 cms/templates/container_chromeless.html diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index ba767df78d..8fbadad799 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -11,6 +11,7 @@ from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponseBadRequest from django.shortcuts import redirect from django.utils.translation import gettext as _ +from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.http import require_GET from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import UsageKey @@ -35,7 +36,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, py __all__ = [ 'container_handler', - 'component_handler' + 'component_handler', + 'container_embed_handler', ] log = logging.getLogger(__name__) @@ -141,6 +143,36 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st return HttpResponseBadRequest("Only supports HTML requests") +@require_GET +@login_required +@xframe_options_exempt +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. + + GET + html: returns the HTML page for editing a container + json: not currently supported + """ + + # Avoiding a circular dependency + from ..utils import get_container_handler_context + + try: + usage_key = UsageKey.from_string(usage_key_string) + except InvalidKeyError: # Raise Http404 on invalid 'usage_key_string' + return HttpResponseBadRequest() + 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: + raise Http404 # lint-amnesty, pylint: disable=raise-missing-from + + container_handler_context = get_container_handler_context(request, usage_key, course, xblock) + return render_to_response('container_chromeless.html', container_handler_context) + + def get_component_templates(courselike, library=False): # lint-amnesty, pylint: disable=too-many-statements """ Returns the applicable component templates that can be used by the specified course or library. diff --git a/cms/djangoapps/contentstore/views/tests/test_container_page.py b/cms/djangoapps/contentstore/views/tests/test_container_page.py index 1d5b529053..426477e234 100644 --- a/cms/djangoapps/contentstore/views/tests/test_container_page.py +++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py @@ -242,3 +242,61 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase): usage_key_string=str(self.vertical.location) ) self.assertEqual(response.status_code, 200) + + +class ContainerEmbedPageTestCase(ContainerPageTestCase): # lint-amnesty, 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=( + '