From fe32596f6cded49d2e98d0173b93b011624ff173 Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Mon, 1 Apr 2024 15:21:21 +0300 Subject: [PATCH 1/8] feat: expose editor for advanced xblocks --- cms/djangoapps/contentstore/views/block.py | 124 +++++++++ cms/templates/container_editor.html | 300 +++++++++++++++++++++ cms/urls.py | 3 + 3 files changed, 427 insertions(+) create mode 100644 cms/templates/container_editor.html diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index e2d1572bc25b..6e4302ae94ec 100644 --- a/cms/djangoapps/contentstore/views/block.py +++ b/cms/djangoapps/contentstore/views/block.py @@ -299,6 +299,130 @@ def xblock_view_handler(request, usage_key_string, view_name): else: return HttpResponse(status=406) +from common.djangoapps.edxmako.shortcuts import render_to_response +@require_http_methods("GET") +@login_required +def partial_edit_view_xblock(request,usage_key_string): + 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() + xblock = store.get_item(usage_key) + + # wrap the generated fragment in the xmodule_editor div so that the javascript + # can bind to it correctly + xblock.runtime.wrappers.append( + partial( + wrap_xblock, + "StudioRuntime", + usage_id_serializer=str, + request_token=request_token(request), + ) + ) + + xblock.runtime.wrappers_asides.append( + partial( + wrap_xblock_aside, + "StudioRuntime", + usage_id_serializer=str, + request_token=request_token(request), + extra_classes=["wrapper-comp-plugins"], + ) + ) + + + load_services_for_studio(xblock.runtime, request.user) + + # try: + studio_fragment = xblock.render("studio_view") + # catch exceptions indiscriminately, since after this point they escape the + # dungeon and surface as uneditable, unsaveable, and undeletable + # component-goblins. + # except Exception as exc: # pylint: disable=broad-except + # log.debug( + # "Unable to render %s for %r", "STUDIO_VIEW", xblock, exc_info=True + # ) + # fragment = Fragment( + # render_to_string("html_error.html", {"message": str(exc)}) + # ) + + can_edit = has_studio_write_access(request.user, usage_key.course_key) + + # Determine the items to be shown as reorderable. Note that the view + # 'reorderable_container_child_preview' is only rendered for xblocks that + # are being shown in a reorderable container, so the xblock is automatically + # added to the list. + reorderable_items = set() + + + force_render = request.GET.get("force_render", None) + + # Fetch tags of children components + tags_count_map = {} + if use_tagging_taxonomy_list_page(): + tags_count_map = get_children_tags_count(xblock) + + # Set up the context to be passed to each XBlock's render method. + context = request.GET.dict() + context.update( + { + # This setting disables the recursive wrapping of xblocks + "is_pages_view": True, + "is_unit_page": is_unit(xblock), + "can_edit": can_edit, + "root_xblock": None, + "reorderable_items": None, + "paging": None, + "force_render": force_render, + "item_url": "/container/{usage_key}", + "tags_count_map": tags_count_map, + } + ) + wrapper_fragment = get_preview_fragment(request, xblock, context) + + # Note that the container view recursively adds headers into the preview fragment, + # so only the "Pages" view requires that this extra wrapper be included. + display_label = xblock.display_name or xblock.scope_ids.block_type + if not xblock.display_name and xblock.scope_ids.block_type == "html": + display_label = _("Text") + wrapper_fragment.content = render_to_string( + "component.html", + { + "xblock_context": context, + "xblock": xblock, + "locator": usage_key, + "preview": wrapper_fragment.content, + "label": display_label, + }, + ) + + + fragment_content = studio_fragment.content + if isinstance(fragment_content, bytes): + fragment_content = studio_fragment.content.decode("utf-8") + + context = { + "studio_fragment": studio_fragment, + "wrapper_fragment": wrapper_fragment, + "is_learning_mfe":True + + } + + from ..utils import get_container_handler_context + from .component import _get_item_in_course + # return render_to_response('courseware-chromeless.html', context) + usage_key = usage_key_with_run(usage_key_string) + with modulestore().bulk_operations(usage_key.course_key): + course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) + + container_handler_context = get_container_handler_context(request, usage_key, course, xblock) + container_handler_context.update({ + "studio_fragment": studio_fragment, + }) + return render_to_response('container_editor.html', container_handler_context) + @require_http_methods("GET") @login_required diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html new file mode 100644 index 000000000000..cb7d6ddbf68a --- /dev/null +++ b/cms/templates/container_editor.html @@ -0,0 +1,300 @@ + +## coding=utf-8 +## mako + +## Pages currently use v1 styling by default. Once the Pattern Library +## rollout has been completed, this default can be switched to v2. +<%! main_css = "style-main-v1" %> + +## Standard imports +<%namespace name='static' file='static_content.html'/> +<%! +from django.utils.translation import gettext as _ + +from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES +from lms.djangoapps.branding import api as branding_api +from openedx.core.djangoapps.util.user_messages import PageLevelMessages +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) +from openedx.core.djangolib.markup import HTML +from openedx.core.release import RELEASE_LINE +%> +<%page expression_filter="h"/> +<%def name="online_help_token()"> +<% + return "container" +%> + +<%! +from django.urls import reverse +from django.utils.translation import gettext as _ + +from cms.djangoapps.contentstore.helpers import xblock_studio_url, xblock_type_display_name +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) +from openedx.core.djangolib.markup import HTML, Text +%> + +<%page expression_filter="h"/> + + + + + + + + + <%block name="title"> +${xblock.display_name_with_default} ${xblock_type_display_name(xblock)} + </%block> | + % if context_course: + <% ctx_loc = context_course.location %> + ${context_course.display_name_with_default} | + % elif context_library: + ${context_library.display_name_with_default} | + % endif + ${settings.STUDIO_NAME} + + + + <% + jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE) + %> + + % if getattr(settings, 'CAPTURE_CONSOLE_LOG', False): + + % endif + + + % if settings.DEBUG: + ## Provides a fallback for gettext functions in development environment + + % endif + + + <%block name="header_meta"> + <% favicon_url = branding_api.get_favicon_url() %> + + <%static:css group='style-vendor'/> + <%static:css group='style-vendor-tinymce-content'/> + <%static:css group='style-vendor-tinymce-skin'/> + + % if uses_bootstrap: + + % else: + <%static:css group='${self.attr.main_css}'/> + % endif + + <%include file="widgets/segment-io.html" /> + + <%block name="header_extras"> + +% for template_name in templates: + +% endfor + + +% if not settings.STUDIO_FRONTEND_CONTAINER_URL: + + +% endif + + + + + + + + <%block name="view_notes"> + + ${_("Skip to main content")} + + <%static:js group='base_vendor'/> + + <%static:webpack entry="commons"/> + + + + + + +
+ <% + banner_messages = list(PageLevelMessages.user_messages(request)) + %> + +
+
+ <%block name="content"> + + + + + + + + + +
+
+ + +
+ + + <%block name="modal_placeholder"> + + <%block name="jsextra"> + + % if context_course: + <%static:webpack entry="js/factories/context_course"/> + + % endif + % if user.is_authenticated: + <%static:webpack entry='js/sock'/> + % endif + <%block name='page_bundle'> + + <%static:webpack entry="js/factories/container"> + ContainerFactory( + ${component_templates | n, dump_js_escaped_json}, + ${xblock_info | n, dump_js_escaped_json}, + "${action | n, js_escaped_string}", + { + isUnitPage: ${is_unit_page | n, dump_js_escaped_json}, + canEdit: true, + outlineURL: "${outline_url | n, js_escaped_string}", + clipboardData: ${user_clipboard | n, dump_js_escaped_json}, + } + ); + + require(["js/models/xblock_info", "js/views/xblock", "js/views/utils/xblock_utils", "common/js/components/utils/view_utils", "gettext"], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext) { + var model = new XBlockInfo({ + id: '${subsection.location|n, decode.utf8}' + }); + var xblockView = new XBlockView({ + model: model, + el: $('#sequence-nav'), + view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}', + clipboardData: ${user_clipboard | n, dump_js_escaped_json}, + }); + + xblockView.xblockReady = function() { + + var toggleCaretButton = function(clipboardData) { + if (clipboardData && clipboardData.content && clipboardData.source_usage_key.includes("vertical")) { + $('.dropdown-toggle-button').show(); + } else { + $('.dropdown-toggle-button').hide(); + $('.dropdown-options').hide(); + } + }; + this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel"); + this.clipboardBroadcastChannel.onmessage = (event) => { + toggleCaretButton(event.data); + }; + toggleCaretButton(this.options.clipboardData); + + $('#new-unit-button').on('click', function(event) { + event.preventDefault(); + XBlockUtils.addXBlock($(this)).done(function(locator) { + ViewUtils.redirect('/container/' + locator + '?action=new'); + }); + }); + + $('.custom-dropdown .dropdown-toggle-button').on('click', function(event) { + event.stopPropagation(); // Prevent the event from closing immediately when we open it + $(this).next('.dropdown-options').slideToggle('fast'); // This toggles the dropdown visibility + var isExpanded = $(this).attr('aria-expanded') === 'true'; + $(this).attr('aria-expanded', !isExpanded); + }); + + $('.seq_paste_unit').on('click', function(event) { + event.preventDefault(); + $('.dropdown-options').hide(); + XBlockUtils.pasteXBlock($(this)).done(function(data) { + ViewUtils.redirect('/container/' + data.locator + '?action=new'); + }); + }); + }; + + xblockView.render(); + }); + + + + + + + diff --git a/cms/urls.py b/cms/urls.py index 9828e9d0fbf0..36ee3b5fec67 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -18,6 +18,7 @@ import openedx.core.djangoapps.lang_pref.views from cms.djangoapps.contentstore import toggles from cms.djangoapps.contentstore import views as contentstore_views +from cms.djangoapps.contentstore.views.block import partial_edit_view_xblock from cms.djangoapps.contentstore.views.organization import OrganizationListView from openedx.core.apidocs import api_info from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance @@ -145,6 +146,8 @@ name='xblock_outline_handler'), re_path(fr'^xblock/container/{settings.USAGE_KEY_PATTERN}$', contentstore_views.xblock_container_handler, name='xblock_container_handler'), + re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/editor$', partial_edit_view_xblock, + name='xblock_editor_handler'), re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/(?P[^/]+)$', contentstore_views.xblock_view_handler, name='xblock_view_handler'), re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}?$', contentstore_views.xblock_handler, From 60c8c7f63477bdf2da5c581d987b98d1bb817126 Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Thu, 4 Apr 2024 00:18:30 +0300 Subject: [PATCH 2/8] feat: added logic for displaying and hiding the xblock modal editing window --- cms/templates/container_editor.html | 43 +++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html index cb7d6ddbf68a..c091e2500f48 100644 --- a/cms/templates/container_editor.html +++ b/cms/templates/container_editor.html @@ -93,6 +93,11 @@ <%static:css group='style-vendor'/> <%static:css group='style-vendor-tinymce-content'/> <%static:css group='style-vendor-tinymce-skin'/> + % if uses_bootstrap: @@ -132,7 +137,7 @@ - + <%block name="view_notes"> ${_("Skip to main content")} @@ -181,8 +186,40 @@ From f2fc1c3c43a5a22021fac5640c851fecbf0de599 Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Thu, 4 Apr 2024 12:44:31 +0300 Subject: [PATCH 3/8] refactor: container_editor template refactoring --- cms/templates/container_editor.html | 507 +++++++++++++--------------- 1 file changed, 241 insertions(+), 266 deletions(-) diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html index c091e2500f48..b8f2403ad16c 100644 --- a/cms/templates/container_editor.html +++ b/cms/templates/container_editor.html @@ -1,4 +1,3 @@ - ## coding=utf-8 ## mako @@ -10,7 +9,6 @@ <%namespace name='static' file='static_content.html'/> <%! from django.utils.translation import gettext as _ - from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES from lms.djangoapps.branding import api as branding_api from openedx.core.djangoapps.util.user_messages import PageLevelMessages @@ -29,7 +27,6 @@ <%! from django.urls import reverse from django.utils.translation import gettext as _ - from cms.djangoapps.contentstore.helpers import xblock_studio_url, xblock_type_display_name from openedx.core.djangolib.js_utils import ( dump_js_escaped_json, js_escaped_string @@ -41,297 +38,275 @@ - - - - - - <%block name="title"> -${xblock.display_name_with_default} ${xblock_type_display_name(xblock)} - </%block> | - % if context_course: - <% ctx_loc = context_course.location %> - ${context_course.display_name_with_default} | - % elif context_library: - ${context_library.display_name_with_default} | - % endif - ${settings.STUDIO_NAME} - - - - <% - jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE) - %> - - % if getattr(settings, 'CAPTURE_CONSOLE_LOG', False): - + % endif - var messages = JSON.parse(window.localStorage.getItem('console_log_capture')); - messages.push([message, url, lineno, colno, (error || {}).stack]); - window.localStorage.setItem('console_log_capture', JSON.stringify(messages)); + + % if settings.DEBUG: + ## Provides a fallback for gettext functions in development environment + + % endif + + + <%block name="header_meta"> + <% favicon_url = branding_api.get_favicon_url() %> + + <%static:css group='style-vendor'/> + <%static:css group='style-vendor-tinymce-content'/> + <%static:css group='style-vendor-tinymce-skin'/> + - - % if uses_bootstrap: - - % else: - <%static:css group='${self.attr.main_css}'/> - % endif - - <%include file="widgets/segment-io.html" /> - - <%block name="header_extras"> + -% for template_name in templates: - -% endfor - - -% if not settings.STUDIO_FRONTEND_CONTAINER_URL: - - -% endif - - - - - - - - <%block name="view_notes"> - - ${_("Skip to main content")} - - <%static:js group='base_vendor'/> - - <%static:webpack entry="commons"/> - - - - - - -
- <% - banner_messages = list(PageLevelMessages.user_messages(request)) - %> - -
-
- <%block name="content"> - - - - - - + <%include file="widgets/segment-io.html" /> + <%block name="header_extras"> + % for template_name in templates: + + % endfor + + + % if not settings.STUDIO_FRONTEND_CONTAINER_URL: + + + % endif -
-
- + + + + + <%block name="view_notes"> + ${_("Skip to main content")} + <%static:js group='base_vendor'/> + <%static:webpack entry="commons"/> + + + +
+ <% + banner_messages = list(PageLevelMessages.user_messages(request)) + %> +
+
+ <%block name="content"> + + + + + + +
+
- <%block name="modal_placeholder"> - <%block name="jsextra"> % if context_course: - <%static:webpack entry="js/factories/context_course"/> - + <%static:webpack entry="js/factories/context_course"/> + % endif + % if user.is_authenticated: - <%static:webpack entry='js/sock'/> + <%static:webpack entry='js/sock'/> % endif - <%block name='page_bundle'> - - <%static:webpack entry="js/factories/container"> - ContainerFactory( - ${component_templates | n, dump_js_escaped_json}, - ${xblock_info | n, dump_js_escaped_json}, - "${action | n, js_escaped_string}", - { - isUnitPage: ${is_unit_page | n, dump_js_escaped_json}, - canEdit: true, - outlineURL: "${outline_url | n, js_escaped_string}", - clipboardData: ${user_clipboard | n, dump_js_escaped_json}, - } - ); - require(["js/models/xblock_info", "js/views/xblock", "js/views/utils/xblock_utils", "common/js/components/utils/view_utils", "gettext"], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext) { - var model = new XBlockInfo({ - id: '${subsection.location|n, decode.utf8}' - }); - var xblockView = new XBlockView({ - model: model, - el: $('#sequence-nav'), - view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}', - clipboardData: ${user_clipboard | n, dump_js_escaped_json}, + <%block name='page_bundle'> + + <%static:webpack entry="js/factories/container"> + ContainerFactory( + ${component_templates | n, dump_js_escaped_json}, + ${xblock_info | n, dump_js_escaped_json}, + '${action | n, js_escaped_string}', + { + isUnitPage: ${is_unit_page | n, dump_js_escaped_json}, + canEdit: true, + outlineURL: '${outline_url | n, js_escaped_string}', + clipboardData: ${user_clipboard | n, dump_js_escaped_json}, + } + ); + + require(['js/models/xblock_info', 'js/views/xblock', 'js/views/utils/xblock_utils', 'common/js/components/utils/view_utils', 'gettext'], function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext) { + var model = new XBlockInfo({ id: '${subsection.location|n, decode.utf8}' }); + var xblockView = new XBlockView({ + model: model, + el: $('#sequence-nav'), + view: 'author_view?position=${position|n, decode.utf8}&next_url=${next_url|n, decode.utf8}&prev_url=${prev_url|n, decode.utf8}', + clipboardData: ${user_clipboard | n, dump_js_escaped_json}, + }); - xblockView.xblockReady = function() { - - var toggleCaretButton = function(clipboardData) { - if (clipboardData && clipboardData.content && clipboardData.source_usage_key.includes("vertical")) { - $('.dropdown-toggle-button').show(); - } else { - $('.dropdown-toggle-button').hide(); - $('.dropdown-options').hide(); - } - }; - this.clipboardBroadcastChannel = new BroadcastChannel("studio_clipboard_channel"); - this.clipboardBroadcastChannel.onmessage = (event) => { - toggleCaretButton(event.data); - }; - toggleCaretButton(this.options.clipboardData); - - $('#new-unit-button').on('click', function(event) { - event.preventDefault(); - XBlockUtils.addXBlock($(this)).done(function(locator) { - ViewUtils.redirect('/container/' + locator + '?action=new'); + xblockView.xblockReady = function() { + var toggleCaretButton = function(clipboardData) { + if (clipboardData && clipboardData.content && clipboardData.source_usage_key.includes('vertical')) { + $('.dropdown-toggle-button').show(); + } else { + $('.dropdown-toggle-button').hide(); + $('.dropdown-options').hide(); + } + }; + this.clipboardBroadcastChannel = new BroadcastChannel('studio_clipboard_channel'); + this.clipboardBroadcastChannel.onmessage = (event) => toggleCaretButton(event.data); + toggleCaretButton(this.options.clipboardData); + + $('#new-unit-button').on('click', function(event) { + event.preventDefault(); + XBlockUtils.addXBlock($(this)).done(function(locator) { + ViewUtils.redirect('/container/' + locator + '?action=new'); + }); }); - }); - $('.custom-dropdown .dropdown-toggle-button').on('click', function(event) { - event.stopPropagation(); // Prevent the event from closing immediately when we open it - $(this).next('.dropdown-options').slideToggle('fast'); // This toggles the dropdown visibility - var isExpanded = $(this).attr('aria-expanded') === 'true'; - $(this).attr('aria-expanded', !isExpanded); - }); + $('.custom-dropdown .dropdown-toggle-button').on('click', function(event) { + event.stopPropagation(); // Prevent the event from closing immediately when we open it + $(this).next('.dropdown-options').slideToggle('fast'); // This toggles the dropdown visibility + var isExpanded = $(this).attr('aria-expanded') === 'true'; + $(this).attr('aria-expanded', !isExpanded); + }); - $('.seq_paste_unit').on('click', function(event) { - event.preventDefault(); - $('.dropdown-options').hide(); - XBlockUtils.pasteXBlock($(this)).done(function(data) { - ViewUtils.redirect('/container/' + data.locator + '?action=new'); + $('.seq_paste_unit').on('click', function(event) { + event.preventDefault(); + $('.dropdown-options').hide(); + XBlockUtils.pasteXBlock($(this)).done(function(data) { + ViewUtils.redirect('/container/' + data.locator + '?action=new'); + }); }); - }); - }; + }; - xblockView.render(); - }); - + xblockView.render(); + }); + + - - From 5f124d8ecf10f50c5b28e8a2e2faa35909698b82 Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Thu, 4 Apr 2024 18:31:26 +0300 Subject: [PATCH 4/8] feat: added ajax error handler --- cms/templates/container_editor.html | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html index b8f2403ad16c..7864b63cce8b 100644 --- a/cms/templates/container_editor.html +++ b/cms/templates/container_editor.html @@ -175,6 +175,10 @@ // Serves to initialize the rendering of a xblock edit modal window. setTimeout(() => $('.button-edit').trigger('click'), 300); + const iframeMessageHandler = (method, msg) => { + return window.parent.postMessage({ method, msg }, '*'); + } + /** * Callback function for the MutationObserver to handle mutations * and send information when the modal window close logic is triggered. @@ -195,7 +199,7 @@ // If removed nodes match the current modal, post a message to close the edit modal. if (editModalRemovedNodes.length > 0) { - window.parent.postMessage({ method: 'close_edit_modal', msg: 'Message from edit iframe modal' }, '*'); + iframeMessageHandler('close_edit_modal', 'Sends a message when the modal window is closed'); } }) @@ -205,6 +209,20 @@ attributes: true, attributeFilter: ['class'] }); + + const originalAjax = $.ajax; + // Notifies the parent element that a server error has occurred. + $.ajax = function(options) { + const errorHandler = options.error; + options.error = function(xhr, textStatus, errorThrown) { + iframeMessageHandler('edit_modal-error', 'Sends a message in case of server error'); + + if (errorHandler) { + errorHandler(xhr, textStatus, errorThrown); + } + }; + return originalAjax.call(this, options); + }; }); From 5b879da1631fddd3367bc7d2f69cc19a2597733b Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Mon, 8 Apr 2024 14:37:26 +0300 Subject: [PATCH 5/8] refactor: removed errors handling --- cms/templates/container_editor.html | 44 +++++++++++------------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html index 7864b63cce8b..6f1345e1a7a7 100644 --- a/cms/templates/container_editor.html +++ b/cms/templates/container_editor.html @@ -175,10 +175,6 @@ // Serves to initialize the rendering of a xblock edit modal window. setTimeout(() => $('.button-edit').trigger('click'), 300); - const iframeMessageHandler = (method, msg) => { - return window.parent.postMessage({ method, msg }, '*'); - } - /** * Callback function for the MutationObserver to handle mutations * and send information when the modal window close logic is triggered. @@ -188,41 +184,33 @@ */ const xblockEditModalObserver = new MutationObserver((mutations) => { const modalClassName = 'wrapper-modal-window-edit-xblock'; - // The index of the current modal window in the removed nodes. - const currentModalIndex = 1; - // Filter mutations to find removed nodes that match the current modal class name. - const editModalRemovedNodes = mutations + // When a modal window is opened while the template is rendering, + // an element with class modalClassName is rendered, + // the MutationObserver defines this process in removedNodes. + const modalElementMutationRecords = mutations .filter(({ removedNodes }) => { - return removedNodes.length > 0 && removedNodes[currentModalIndex].className.includes(modalClassName) + const filteredModalClassName = Array.from(removedNodes).filter((node) => + node.className && node.className.includes(modalClassName)); + + return filteredModalClassName.length > 0; }); - // If removed nodes match the current modal, post a message to close the edit modal. - if (editModalRemovedNodes.length > 0) { - iframeMessageHandler('close_edit_modal', 'Sends a message when the modal window is closed'); + // If the element was present and deleted, close the modal window. + if (modalElementMutationRecords.length > 0 && !$('.' + modalClassName).length) { + window.parent.postMessage({ + method: 'close_edit_modal', + msg: 'Sends a message when the modal window is closed' + }, '*'); } - }) + }); - xblockEditModalObserver.observe(document.body, { + xblockEditModalObserver.observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); - - const originalAjax = $.ajax; - // Notifies the parent element that a server error has occurred. - $.ajax = function(options) { - const errorHandler = options.error; - options.error = function(xhr, textStatus, errorThrown) { - iframeMessageHandler('edit_modal-error', 'Sends a message in case of server error'); - - if (errorHandler) { - errorHandler(xhr, textStatus, errorThrown); - } - }; - return originalAjax.call(this, options); - }; }); From 3ecbbd6055fd27178513f6a840ce629fd7c063b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Mon, 8 Apr 2024 15:20:04 +0300 Subject: [PATCH 6/8] style: [AXIMST-28] fix pylint issues --- cms/djangoapps/contentstore/views/block.py | 30 +++++++--------------- cms/templates/container_editor.html | 1 - cms/urls.py | 2 +- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index 6e4302ae94ec..ea2d1c437c5c 100644 --- a/cms/djangoapps/contentstore/views/block.py +++ b/cms/djangoapps/contentstore/views/block.py @@ -14,7 +14,7 @@ from web_fragments.fragment import Fragment from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW -from common.djangoapps.edxmako.shortcuts import render_to_string +from common.djangoapps.edxmako.shortcuts import render_to_response, render_to_string from common.djangoapps.student.auth import ( has_studio_read_access, has_studio_write_access, @@ -299,15 +299,17 @@ def xblock_view_handler(request, usage_key_string, view_name): else: return HttpResponse(status=406) -from common.djangoapps.edxmako.shortcuts import render_to_response + @require_http_methods("GET") @login_required -def partial_edit_view_xblock(request,usage_key_string): +def partial_edit_view_xblock(request, usage_key_string): + """ + The restful handler for requests for rendered xblock views. + """ 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() xblock = store.get_item(usage_key) @@ -332,7 +334,6 @@ def partial_edit_view_xblock(request,usage_key_string): ) ) - load_services_for_studio(xblock.runtime, request.user) # try: @@ -354,8 +355,7 @@ def partial_edit_view_xblock(request,usage_key_string): # 'reorderable_container_child_preview' is only rendered for xblocks that # are being shown in a reorderable container, so the xblock is automatically # added to the list. - reorderable_items = set() - + # reorderable_items = set() force_render = request.GET.get("force_render", None) @@ -398,21 +398,9 @@ def partial_edit_view_xblock(request,usage_key_string): }, ) + from ..utils import get_container_handler_context # pylint: disable=import-outside-toplevel + from .component import _get_item_in_course # pylint: disable=import-outside-toplevel - fragment_content = studio_fragment.content - if isinstance(fragment_content, bytes): - fragment_content = studio_fragment.content.decode("utf-8") - - context = { - "studio_fragment": studio_fragment, - "wrapper_fragment": wrapper_fragment, - "is_learning_mfe":True - - } - - from ..utils import get_container_handler_context - from .component import _get_item_in_course - # return render_to_response('courseware-chromeless.html', context) usage_key = usage_key_with_run(usage_key_string) with modulestore().bulk_operations(usage_key.course_key): course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html index 6f1345e1a7a7..a5afa5125e62 100644 --- a/cms/templates/container_editor.html +++ b/cms/templates/container_editor.html @@ -18,7 +18,6 @@ from openedx.core.djangolib.markup import HTML from openedx.core.release import RELEASE_LINE %> -<%page expression_filter="h"/> <%def name="online_help_token()"> <% return "container" diff --git a/cms/urls.py b/cms/urls.py index 36ee3b5fec67..811f78e620eb 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -147,7 +147,7 @@ re_path(fr'^xblock/container/{settings.USAGE_KEY_PATTERN}$', contentstore_views.xblock_container_handler, name='xblock_container_handler'), re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/editor$', partial_edit_view_xblock, - name='xblock_editor_handler'), + name='xblock_editor_handler'), re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/(?P[^/]+)$', contentstore_views.xblock_view_handler, name='xblock_view_handler'), re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}?$', contentstore_views.xblock_handler, From 75f0efe18eed3737111d08c302f7d6d5be3de96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Tue, 9 Apr 2024 10:54:28 +0300 Subject: [PATCH 7/8] fix: [AXIMST-28] remove unused variables from context --- cms/djangoapps/contentstore/views/block.py | 62 +++------------------- cms/urls.py | 4 +- 2 files changed, 10 insertions(+), 56 deletions(-) diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index ea2d1c437c5c..9f3f573981cb 100644 --- a/cms/djangoapps/contentstore/views/block.py +++ b/cms/djangoapps/contentstore/views/block.py @@ -38,10 +38,11 @@ STUDIO_VIEW, ) # lint-amnesty, pylint: disable=wrong-import-order - 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 from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import ( @@ -302,7 +303,7 @@ def xblock_view_handler(request, usage_key_string, view_name): @require_http_methods("GET") @login_required -def partial_edit_view_xblock(request, usage_key_string): +def edit_view_xblock(request, usage_key_string): """ The restful handler for requests for rendered xblock views. """ @@ -323,7 +324,6 @@ def partial_edit_view_xblock(request, usage_key_string): request_token=request_token(request), ) ) - xblock.runtime.wrappers_asides.append( partial( wrap_xblock_aside, @@ -333,53 +333,14 @@ def partial_edit_view_xblock(request, usage_key_string): extra_classes=["wrapper-comp-plugins"], ) ) - load_services_for_studio(xblock.runtime, request.user) - # try: - studio_fragment = xblock.render("studio_view") - # catch exceptions indiscriminately, since after this point they escape the - # dungeon and surface as uneditable, unsaveable, and undeletable - # component-goblins. - # except Exception as exc: # pylint: disable=broad-except - # log.debug( - # "Unable to render %s for %r", "STUDIO_VIEW", xblock, exc_info=True - # ) - # fragment = Fragment( - # render_to_string("html_error.html", {"message": str(exc)}) - # ) - - can_edit = has_studio_write_access(request.user, usage_key.course_key) - - # Determine the items to be shown as reorderable. Note that the view - # 'reorderable_container_child_preview' is only rendered for xblocks that - # are being shown in a reorderable container, so the xblock is automatically - # added to the list. - # reorderable_items = set() - - force_render = request.GET.get("force_render", None) - - # Fetch tags of children components - tags_count_map = {} - if use_tagging_taxonomy_list_page(): - tags_count_map = get_children_tags_count(xblock) - # Set up the context to be passed to each XBlock's render method. context = request.GET.dict() - context.update( - { - # This setting disables the recursive wrapping of xblocks - "is_pages_view": True, - "is_unit_page": is_unit(xblock), - "can_edit": can_edit, - "root_xblock": None, - "reorderable_items": None, - "paging": None, - "force_render": force_render, - "item_url": "/container/{usage_key}", - "tags_count_map": tags_count_map, - } - ) + context.update({ + "is_unit_page": is_unit(xblock), + "can_edit": has_studio_write_access(request.user, usage_key.course_key), + }) wrapper_fragment = get_preview_fragment(request, xblock, context) # Note that the container view recursively adds headers into the preview fragment, @@ -398,17 +359,10 @@ def partial_edit_view_xblock(request, usage_key_string): }, ) - from ..utils import get_container_handler_context # pylint: disable=import-outside-toplevel - from .component import _get_item_in_course # pylint: disable=import-outside-toplevel - usage_key = usage_key_with_run(usage_key_string) - with modulestore().bulk_operations(usage_key.course_key): + with store.bulk_operations(usage_key.course_key): course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) - container_handler_context = get_container_handler_context(request, usage_key, course, xblock) - container_handler_context.update({ - "studio_fragment": studio_fragment, - }) return render_to_response('container_editor.html', container_handler_context) diff --git a/cms/urls.py b/cms/urls.py index 811f78e620eb..7241242d5b59 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -18,7 +18,7 @@ import openedx.core.djangoapps.lang_pref.views from cms.djangoapps.contentstore import toggles from cms.djangoapps.contentstore import views as contentstore_views -from cms.djangoapps.contentstore.views.block import partial_edit_view_xblock +from cms.djangoapps.contentstore.views.block import edit_view_xblock from cms.djangoapps.contentstore.views.organization import OrganizationListView from openedx.core.apidocs import api_info from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance @@ -146,7 +146,7 @@ name='xblock_outline_handler'), re_path(fr'^xblock/container/{settings.USAGE_KEY_PATTERN}$', contentstore_views.xblock_container_handler, name='xblock_container_handler'), - re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/editor$', partial_edit_view_xblock, + re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/editor$', edit_view_xblock, name='xblock_editor_handler'), re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/(?P[^/]+)$', contentstore_views.xblock_view_handler, name='xblock_view_handler'), From 0413ef26565969d94b32521fab74b1e09e2e1a3a Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Tue, 9 Apr 2024 11:24:18 +0300 Subject: [PATCH 8/8] fix: remove unused part of the code and update docstring --- cms/djangoapps/contentstore/views/block.py | 50 +--------------------- 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index 9f3f573981cb..de70e8ce5fe6 100644 --- a/cms/djangoapps/contentstore/views/block.py +++ b/cms/djangoapps/contentstore/views/block.py @@ -305,61 +305,13 @@ def xblock_view_handler(request, usage_key_string, view_name): @login_required def edit_view_xblock(request, usage_key_string): """ - The restful handler for requests for rendered xblock views. + The handler for rendered edit xblock view. """ 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() - xblock = store.get_item(usage_key) - - # wrap the generated fragment in the xmodule_editor div so that the javascript - # can bind to it correctly - xblock.runtime.wrappers.append( - partial( - wrap_xblock, - "StudioRuntime", - usage_id_serializer=str, - request_token=request_token(request), - ) - ) - xblock.runtime.wrappers_asides.append( - partial( - wrap_xblock_aside, - "StudioRuntime", - usage_id_serializer=str, - request_token=request_token(request), - extra_classes=["wrapper-comp-plugins"], - ) - ) - load_services_for_studio(xblock.runtime, request.user) - - # Set up the context to be passed to each XBlock's render method. - context = request.GET.dict() - context.update({ - "is_unit_page": is_unit(xblock), - "can_edit": has_studio_write_access(request.user, usage_key.course_key), - }) - wrapper_fragment = get_preview_fragment(request, xblock, context) - - # Note that the container view recursively adds headers into the preview fragment, - # so only the "Pages" view requires that this extra wrapper be included. - display_label = xblock.display_name or xblock.scope_ids.block_type - if not xblock.display_name and xblock.scope_ids.block_type == "html": - display_label = _("Text") - wrapper_fragment.content = render_to_string( - "component.html", - { - "xblock_context": context, - "xblock": xblock, - "locator": usage_key, - "preview": wrapper_fragment.content, - "label": display_label, - }, - ) - usage_key = usage_key_with_run(usage_key_string) with store.bulk_operations(usage_key.course_key): course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) container_handler_context = get_container_handler_context(request, usage_key, course, xblock)