Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [FC-0044] expose editor for advanced xblocks and move modal in CMS #34929

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions cms/static/js/views/modals/base_modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},

Expand Down
5 changes: 5 additions & 0 deletions cms/static/js/views/modals/edit_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 21 additions & 4 deletions cms/static/js/views/utils/move_xblock_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.'),
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
223 changes: 223 additions & 0 deletions cms/templates/container_editor.html
Original file line number Diff line number Diff line change
@@ -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"
%>
</%def>
<%!
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"/>
<!doctype html>
<!--[if lte IE 9]><html class="ie9 lte9" lang="${LANGUAGE_CODE}"><![endif]-->
<!--[if !IE]><<!--><html lang="${LANGUAGE_CODE}"><!--<![endif]-->
<head dir="${static.dir_rtl()}">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="openedx-release-line" content="${RELEASE_LINE}" />
<title>
<%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}
</title>

<%
jsi18n_path = "js/i18n/{language}/djangojs.js".format(language=LANGUAGE_CODE)
%>

% if getattr(settings, 'CAPTURE_CONSOLE_LOG', False):
<script type="text/javascript">
const oldOnError = window.onerror;
window.localStorage.setItem('console_log_capture', JSON.stringify([]));

window.onerror = function (message, url, lineno, colno, error) {
if (oldOnError) {
oldOnError.apply(this, arguments);
}

const 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));
}
</script>
% endif

<script type="text/javascript" src="${static.url(jsi18n_path)}"></script>
% if settings.DEBUG:
## Provides a fallback for gettext functions in development environment
<script type="text/javascript" src="${static.url('js/src/gettext_fallback.js')}"></script>
% endif
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="path_prefix" content="${EDX_ROOT_URL}">
<%block name="header_meta"></%block>
<% favicon_url = branding_api.get_favicon_url() %>
<link rel="icon" type="image/x-icon" href="${favicon_url}"/>
<%static:css group='style-vendor'/>
<%static:css group='style-vendor-tinymce-content'/>
<%static:css group='style-vendor-tinymce-skin'/>
<style>
html body {
background: transparent;
}
</style>

% if uses_bootstrap:
<link rel="stylesheet" href="${static.url(self.attr.main_css)}" type="text/css" media="all" />
% else:
<%static:css group='${self.attr.main_css}'/>
% endif

<%include file="widgets/segment-io.html" />
<%block name="header_extras">

% for template_name in templates:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
<script type="text/template" id="image-modal-tpl">
<%static:include path="common/templates/image-modal.underscore" />
</script>
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% if not settings.STUDIO_FRONTEND_CONTAINER_URL:
<link rel="stylesheet" type="text/css" href="${static.url('common/css/vendor/common.min.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('common/css/vendor/editImageModal.min.css')}" />
% endif

</%block>
<!-- Hotjar Tracking Code for studio -->
<script>
(function(h, o, t, j, a, r){
h.hj = h.hj || function() { (h.hj.q = h.hj.q || []).push(arguments) };
h._hjSettings={ hjid: Number('${settings.HOTJAR_ID |n, js_escaped_string}'), hjsv: 6 };
a = o.getElementsByTagName('head')[0];
r = o.createElement('script');
r.async = 1;
r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
</head>

<body class="${static.dir_rtl()} <%block name='bodyclass'></%block> lang_${LANGUAGE_CODE} view-container">
<%block name="view_notes"></%block>
<a class="nav-skip" href="#main">${_("Skip to main content")}</a>
<%static:js group='base_vendor'/>
<%static:webpack entry="commons"/>
<script type="text/javascript">
window.baseUrl = '${settings.STATIC_URL | n, js_escaped_string}';
require.config({ baseUrl: window.baseUrl });
</script>
<script type="text/javascript" src="${static.url("cms/js/require-config.js")}"></script>
<!-- view -->
<div class="wrapper wrapper-view" dir="${static.dir_rtl()}">
<%
banner_messages = list(PageLevelMessages.user_messages(request))
%>
<main id="main" aria-label="Content" tabindex="-1">
<div id="content">
<%block name="content">
</%block>
</div>
</main>
</div>

<%block name="modal_placeholder"></%block>
<%block name="jsextra"></%block>

% if context_course:
<%static:webpack entry="js/factories/context_course"/>
<script type="text/javascript">
window.course = new ContextCourse({
id: '${context_course.id | n, js_escaped_string}',
name: '${context_course.display_name_with_default | n, js_escaped_string}',
url_name: '${context_course.location.block_id | n, js_escaped_string}',
org: '${context_course.location.org | n, js_escaped_string}',
num: '${context_course.location.course | n, js_escaped_string}',
display_course_number: '${context_course.display_coursenumber | n, js_escaped_string}',
revision: '${context_course.location.branch | n, js_escaped_string}',
self_paced: ${ context_course.self_paced | n, dump_js_escaped_json },
is_custom_relative_dates_active: ${CUSTOM_RELATIVE_DATES.is_enabled(context_course.id) | n, dump_js_escaped_json},
start: ${context_course.start | n, dump_js_escaped_json},
discussions_settings: ${context_course.discussions_settings | n, dump_js_escaped_json}
});
</script>
% endif

% if user.is_authenticated:
<%static:webpack entry='js/sock'/>
% endif

<%block name='page_bundle'>
<script type="text/javascript">
require(['js/factories/base'], function () {
<%block name='requirejs'></%block>
});
</script>
<%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});
}
});
</%static:webpack>
</%block>

<div class="modal-cover"></div>
</body>
</html>
3 changes: 3 additions & 0 deletions cms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<action_name>[^/]+)$', xblock_actions_view,
name='xblock_actions_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/(?P<view_name>[^/]+)$', contentstore_views.xblock_view_handler,
name='xblock_view_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}?$', contentstore_views.xblock_handler,
Expand Down
Loading