diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index e2d1572bc25b..de70e8ce5fe6 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, @@ -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 ( @@ -300,6 +301,23 @@ def xblock_view_handler(request, usage_key_string, view_name): return HttpResponse(status=406) +@require_http_methods("GET") +@login_required +def edit_view_xblock(request, usage_key_string): + """ + 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() + + 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) + return render_to_response('container_editor.html', container_handler_context) + + @require_http_methods("GET") @login_required @expect_json diff --git a/cms/templates/container_editor.html b/cms/templates/container_editor.html new file mode 100644 index 000000000000..a5afa5125e62 --- /dev/null +++ b/cms/templates/container_editor.html @@ -0,0 +1,317 @@ +## 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"> + 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..7241242d5b59 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 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$', 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,