diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index d52cc5eecfd4..46e49c2fae4a 100644 --- a/cms/djangoapps/contentstore/views/block.py +++ b/cms/djangoapps/contentstore/views/block.py @@ -9,12 +9,13 @@ from django.db import transaction from django.http import Http404, HttpResponse from django.utils.translation import gettext as _ +from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.http import require_http_methods from opaque_keys.edx.keys import CourseKey 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, @@ -38,10 +39,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 ( @@ -300,6 +302,30 @@ def xblock_view_handler(request, usage_key_string, view_name): return HttpResponse(status=406) +@xframe_options_exempt +@require_http_methods("GET") +@login_required +def xblock_actions_view(request, usage_key_string, action_name): + """ + Return rendered xblock action view. + The action name should be provided as an argument. + Valid options for action names are edit and move. + """ + usage_key = usage_key_with_run(usage_key_string) + if not has_studio_read_access(request.user, usage_key.course_key): + raise PermissionDenied() + if action_name not in ['edit', 'move']: + return HttpResponse(status=404) + + store = modulestore() + + 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({'action_name': action_name}) + return render_to_response('container_editor.html', container_handler_context) + + @require_http_methods("GET") @login_required @expect_json diff --git a/cms/static/js/views/modals/base_modal.js b/cms/static/js/views/modals/base_modal.js index 65b7f06ae021..828289533a73 100644 --- a/cms/static/js/views/modals/base_modal.js +++ b/cms/static/js/views/modals/base_modal.js @@ -119,6 +119,10 @@ define(['jquery', 'underscore', 'gettext', 'js/views/baseview'], event.preventDefault(); event.stopPropagation(); // Make sure parent modals don't see the click } + window.parent.postMessage({ + method: 'close_modal', + msg: 'Sends a message when the modal window is closed' + }, '*'); this.hide(); }, diff --git a/cms/static/js/views/modals/edit_xblock.js b/cms/static/js/views/modals/edit_xblock.js index 7182d8b0e51a..0ced05cea737 100644 --- a/cms/static/js/views/modals/edit_xblock.js +++ b/cms/static/js/views/modals/edit_xblock.js @@ -208,6 +208,11 @@ function($, _, Backbone, gettext, BaseModal, ViewUtils, XBlockViewUtils, XBlockE // Notify child views to stop listening events Backbone.trigger('xblock:editorModalHidden'); + window.parent.postMessage({ + method: 'close_modal', + msg: 'Sends a message when the edit modal window is closed' + }, '*'); + BaseModal.prototype.hide.call(this); // Notify the runtime that the modal has been hidden diff --git a/cms/static/js/views/utils/move_xblock_utils.js b/cms/static/js/views/utils/move_xblock_utils.js index 9bde9c6d087e..f3bcc675f1da 100644 --- a/cms/static/js/views/utils/move_xblock_utils.js +++ b/cms/static/js/views/utils/move_xblock_utils.js @@ -26,8 +26,25 @@ function($, _, Backbone, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils, .done(function(response) { // hide modal Backbone.trigger('move:hideMoveModal'); - // hide xblock element - data.sourceXBlockElement.hide(); + if (data.sourceXBlockElement) { + // hide xblock element + data.sourceXBlockElement.hide(); + } + + window.parent.postMessage({ + method: 'move_xblock', + msg: 'Sends a message when the xblock is moved', + params: { + sourceDisplayName: data.sourceDisplayName, + sourceLocator: data.sourceLocator, + targetParentLocator: data.targetParentLocator, + } + }, '*'); + window.parent.postMessage({ + method: 'close_modal', + msg: 'Sends a message when the modal window is closed' + }, '*'); + showMovedNotification( StringUtils.interpolate( gettext('Success! "{displayName}" has been moved.'), @@ -36,7 +53,7 @@ function($, _, Backbone, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils, } ), { - sourceXBlockElement: data.sourceXBlockElement, + sourceXBlockElement: data.sourceXBlockElement ? data.sourceXBlockElement : null, sourceDisplayName: data.sourceDisplayName, sourceLocator: data.sourceLocator, sourceParentLocator: data.sourceParentLocator, @@ -78,7 +95,7 @@ function($, _, Backbone, Feedback, AlertView, XBlockViewUtils, MoveXBlockUtils, click: function() { undoMoveXBlock( { - sourceXBlockElement: data.sourceXBlockElement, + sourceXBlockElement: data.sourceXBlockElement ? data.sourceXBlockElement : null, sourceDisplayName: data.sourceDisplayName, sourceLocator: data.sourceLocator, sourceParentLocator: data.sourceParentLocator, diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html new file mode 100644 index 000000000000..43f23d2100a4 --- /dev/null +++ b/cms/templates/container_editor.html @@ -0,0 +1,223 @@ +## 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 +%> +<%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"> + + require(['js/models/xblock_info', 'js/views/xblock', 'js/views/utils/xblock_utils', +'common/js/components/utils/view_utils', 'gettext', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal'], +function (XBlockInfo, XBlockView, XBlockUtils, ViewUtils, gettext, EditXBlockModal, MoveXBlockModal) { + function showMoveModal(unitLocation, xblockInfo, outlineUrl) { + var parentModel = new XBlockInfo({ id: unitLocation, category: 'vertical' }); + var moveModal = new MoveXBlockModal({ + sourceXBlockInfo: new XBlockInfo(xblockInfo), + sourceParentXBlockInfo: parentModel, + XBlockURLRoot: '/xblock', + outlineURL: outlineUrl, + }); + moveModal.show(); + } + + function showEditModal(xblockInfo) { + var editModal = new EditXBlockModal(); + editModal.edit([], new XBlockInfo(xblockInfo), {}); + } + + var actionName = '${action_name|n, decode.utf8}'; + if (actionName === 'move') { + showMoveModal('${unit.location|n, decode.utf8}', ${xblock_info | n, dump_js_escaped_json}, '${outline_url | n, js_escaped_string}'); + } else if (actionName === 'edit') { + showEditModal(${xblock_info | n, dump_js_escaped_json}); + } + }); + + + + + + diff --git a/cms/urls.py b/cms/urls.py index 9828e9d0fbf0..aff79f5ca763 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 xblock_actions_view 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}/actions/(?P[^/]+)$', xblock_actions_view, + name='xblock_actions_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,