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

Confirmation modal to preview and accept v2 library updates [FC-0062] #35669

Merged
Merged
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
24 changes: 23 additions & 1 deletion cms/lib/xblock/test/test_upstream_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ def setUp(self):
upstream.data = "<html><body>Upstream content V2</body></html>"
upstream.save()

libs.publish_changes(self.library.key, self.user.id)

def test_sync_bad_downstream(self):
"""
Syncing into an unsupported downstream (such as a another Content Library block) raises BadDownstream, but
Expand Down Expand Up @@ -133,6 +135,16 @@ def test_sync_updates_happy_path(self):
upstream.data = "<html><body>Upstream content V3</body></html>"
upstream.save()

# Assert that un-published updates are not yet pulled into downstream
sync_from_upstream(downstream, self.user)
assert downstream.upstream_version == 2 # Library blocks start at version 2 (v1 is the empty new block)
assert downstream.upstream_display_name == "Upstream Title V2"
assert downstream.display_name == "Upstream Title V2"
assert downstream.data == "<html><body>Upstream content V2</body></html>"

# Publish changes
libs.publish_changes(self.library.key, self.user.id)

# Follow-up sync. Assert that updates are pulled into downstream.
sync_from_upstream(downstream, self.user)
assert downstream.upstream_version == 3
Expand All @@ -157,6 +169,7 @@ def test_sync_updates_to_modified_content(self):
upstream.display_name = "Upstream Title V3"
upstream.data = "<html><body>Upstream content V3</body></html>"
upstream.save()
libs.publish_changes(self.library.key, self.user.id)

# Downstream modifications
downstream.display_name = "Downstream Title Override" # "safe" customization
Expand Down Expand Up @@ -277,13 +290,21 @@ def test_prompt_and_decline_sync(self):
assert link.version_available == 2
assert link.ready_to_sync is False

# Upstream updated to V3
# Upstream updated to V3, but not yet published
upstream = xblock.load_block(self.upstream_key, self.user)
upstream.data = "<html><body>Upstream content V3</body></html>"
upstream.save()
link = UpstreamLink.get_for_block(downstream)
assert link.version_synced == 2
assert link.version_declined is None
assert link.version_available == 2
assert link.ready_to_sync is False

# Publish changes
libs.publish_changes(self.library.key, self.user.id)
link = UpstreamLink.get_for_block(downstream)
assert link.version_synced == 2
assert link.version_declined is None
assert link.version_available == 3
assert link.ready_to_sync is True

Expand All @@ -299,6 +320,7 @@ def test_prompt_and_decline_sync(self):
upstream = xblock.load_block(self.upstream_key, self.user)
upstream.data = "<html><body>Upstream content V4</body></html>"
upstream.save()
libs.publish_changes(self.library.key, self.user.id)
link = UpstreamLink.get_for_block(downstream)
assert link.version_synced == 2
assert link.version_declined == 3
Expand Down
15 changes: 8 additions & 7 deletions cms/lib/xblock/upstream_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,7 @@ def get_for_block(cls, downstream: XBlock) -> t.Self:
return cls(
upstream_ref=downstream.upstream,
version_synced=downstream.upstream_version,
version_available=(lib_meta.draft_version_num if lib_meta else None),
# TODO: Previous line is wrong. It should use the published version instead, but the
# LearningCoreXBlockRuntime APIs do not yet support published content yet.
# Will be fixed in a follow-up task: https://github.com/openedx/edx-platform/issues/35582
# version_available=(lib_meta.published_version_num if lib_meta else None),
version_available=(lib_meta.published_version_num if lib_meta else None),
version_declined=downstream.upstream_version_declined,
error_message=None,
)
Expand Down Expand Up @@ -213,9 +209,14 @@ def _load_upstream_link_and_block(downstream: XBlock, user: User) -> tuple[Upstr
"""
link = UpstreamLink.get_for_block(downstream) # can raise UpstreamLinkException
# We import load_block here b/c UpstreamSyncMixin is used by cms/envs, which loads before the djangoapps are ready.
from openedx.core.djangoapps.xblock.api import load_block # pylint: disable=wrong-import-order
from openedx.core.djangoapps.xblock.api import load_block, CheckPerm, LatestVersion # pylint: disable=wrong-import-order
try:
lib_block: XBlock = load_block(LibraryUsageLocatorV2.from_string(downstream.upstream), user)
lib_block: XBlock = load_block(
LibraryUsageLocatorV2.from_string(downstream.upstream),
user,
check_permission=CheckPerm.CAN_READ_AS_AUTHOR,
version=LatestVersion.PUBLISHED,
)
except (NotFound, PermissionDenied) as exc:
raise BadUpstream(_("Linked library item could not be loaded: {}").format(downstream.upstream)) from exc
return link, lib_block
Expand Down
112 changes: 112 additions & 0 deletions cms/static/js/views/modals/preview_v2_library_changes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* The PreviewLibraryChangesModal is a Backbone view that shows an iframe in a
* modal window. The iframe embeds a view from the Authoring MFE that allows
* authors to preview the new version of a library-sourced XBlock, and decide
* whether to accept ("sync") or reject ("ignore") the changes.
*/
define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal',
'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils'],
function($, _, gettext, BaseModal, ViewUtils, XBlockViewUtils) {
'use strict';

var PreviewLibraryChangesModal = BaseModal.extend({
events: _.extend({}, BaseModal.prototype.events, {
'click .action-accept': 'acceptChanges',
'click .action-ignore': 'ignoreChanges',
}),

options: $.extend({}, BaseModal.prototype.options, {
modalName: 'preview-lib-changes',
modalSize: 'med',
view: 'studio_view',
viewSpecificClasses: 'modal-lib-preview confirm',
// Translators: "title" is the name of the current component being edited.
titleFormat: gettext('Preview changes to: {title}'),
addPrimaryActionButton: false,
}),

initialize: function() {
BaseModal.prototype.initialize.call(this);
},

/**
* Adds the action buttons to the modal.
*/
addActionButtons: function() {
this.addActionButton('accept', gettext('Accept changes'), true);
this.addActionButton('ignore', gettext('Ignore changes'));
this.addActionButton('cancel', gettext('Cancel'));
},

/**
* Show an edit modal for the specified xblock
* @param xblockElement The element that contains the xblock to be edited.
* @param rootXBlockInfo An XBlockInfo model that describes the root xblock on the page.
* @param refreshFunction A function to refresh the block after it has been updated
*/
showPreviewFor: function(xblockElement, rootXBlockInfo, refreshFunction) {
this.xblockElement = xblockElement;
this.xblockInfo = XBlockViewUtils.findXBlockInfo(xblockElement, rootXBlockInfo);
this.courseAuthoringMfeUrl = rootXBlockInfo.attributes.course_authoring_url;
const headerElement = xblockElement.find('.xblock-header-primary');
this.downstreamBlockId = this.xblockInfo.get('id');
this.upstreamBlockId = headerElement.data('upstream-ref');
this.upstreamBlockVersionSynced = headerElement.data('version-synced');
this.refreshFunction = refreshFunction;

this.render();
this.show();
},

getContentHtml: function() {
return `
<iframe src="${this.courseAuthoringMfeUrl}/legacy/preview-changes/${this.upstreamBlockId}?old=${this.upstreamBlockVersionSynced}">
`;
},

getTitle: function() {
var displayName = this.xblockInfo.get('display_name');
if (!displayName) {
if (this.xblockInfo.isVertical()) {
displayName = gettext('Unit');
} else {
displayName = gettext('Component');
}
}
return edx.StringUtils.interpolate(
this.options.titleFormat, {
title: displayName
}
);
},

acceptChanges: function(event) {
event.preventDefault();
$.post(`/api/contentstore/v2/downstreams/${this.downstreamBlockId}/sync`).done(() => {
this.hide();
this.refreshFunction();
}); // Note: if this POST request fails, Studio will display an error toast automatically.
},

ignoreChanges: function(event) {
event.preventDefault();
ViewUtils.confirmThenRunOperation(
gettext('Ignore these changes?'),
gettext('Would you like to permanently ignore this updated version? If so, you won\'t be able to update this until a newer version is published (in the library).'),
gettext('Ignore'),
() => {
$.ajax({
type: 'DELETE',
url: `/api/contentstore/v2/downstreams/${this.downstreamBlockId}/sync`,
data: {},
}).done(() => {
this.hide();
this.refreshFunction();
}); // Note: if this DELETE request fails, Studio will display an error toast automatically.
}
);
},
});

return PreviewLibraryChangesModal;
});
18 changes: 16 additions & 2 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor',
'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils',
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
'js/views/utils/tagging_drawer_utils', 'js/utils/module',
'js/views/utils/tagging_drawer_utils', 'js/utils/module', 'js/views/modals/preview_v2_library_changes'
],
function($, _, Backbone, gettext, BasePage,
ViewUtils, ContainerView, XBlockView,
AddXBlockComponent, EditXBlockModal, MoveXBlockModal,
XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
ContainerSubviews, UnitOutlineView, XBlockUtils,
NotificationView, PromptView, TaggingDrawerUtils, ModuleUtils) {
NotificationView, PromptView, TaggingDrawerUtils, ModuleUtils,
PreviewLibraryChangesModal) {
'use strict';

var XBlockContainerPage = BasePage.extend({
Expand All @@ -28,6 +29,7 @@ function($, _, Backbone, gettext, BasePage,
'click .copy-button': 'copyXBlock',
'click .move-button': 'showMoveXBlockModal',
'click .delete-button': 'deleteXBlock',
'click .library-sync-button': 'showXBlockLibraryChangesPreview',
'click .show-actions-menu-button': 'showXBlockActionsMenu',
'click .new-component-button': 'scrollToNewComponentButtons',
'click .save-button': 'saveSelectedLibraryComponents',
Expand Down Expand Up @@ -396,6 +398,18 @@ function($, _, Backbone, gettext, BasePage,
});
},

showXBlockLibraryChangesPreview: function(event, options) {
event.preventDefault();

var xblockElement = this.findXBlockElement(event.target),
self = this,
modal = new PreviewLibraryChangesModal(options);

modal.showPreviewFor(xblockElement, this.model, function() {
self.refreshXBlock(xblockElement, false);
});
},

/**
* If the new "Actions" menu is enabled, most XBlock actions like
* Duplicate, Move, Delete, Manage Access, etc. are moved into this
Expand Down
14 changes: 14 additions & 0 deletions cms/static/sass/views/_container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,20 @@
}
}

// Modal for previewing changes to a library-sourced block
// cms/static/js/views/modals/preview_v2_library_changes.js
.modal-lib-preview {
.modal-content {
padding: 0 !important;

& > iframe {
width: 100%;
min-height: 350px;
background: url('#{$static-path}/images/spinner.gif') center center no-repeat;
}
}
}

.ltiLaunchFrame{
width:100%;
height:100%
Expand Down
5 changes: 4 additions & 1 deletion cms/templates/studio_xblock_wrapper.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@
authoring_MFE_base_url = ${get_editor_page_base_url(xblock.location.course_key)}
data-block-type = ${xblock.scope_ids.block_type}
data-usage-id = ${xblock.scope_ids.usage_id}
% if upstream_info.upstream_ref:
data-upstream-ref = ${upstream_info.upstream_ref}
data-version-synced = ${upstream_info.version_synced}
%endif
>
<div class="header-details">
% if show_inline:
Expand Down Expand Up @@ -137,7 +141,6 @@
<button
class="btn-default library-sync-button action-button"
data-tooltip="${_("Update available - click to sync")}"
onclick="$.post('/api/contentstore/v2/downstreams/${xblock.usage_key}/sync').done(() => { location.reload(); })"
>
<span class="icon fa fa-refresh" aria-hidden="true"></span>
<span>${_("Update available")}</span>
Expand Down
Loading