Skip to content

Commit

Permalink
feat: Confirmation modal to preview and accept v2 library updates (#3…
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald authored Oct 18, 2024
1 parent ca80d10 commit 83827a3
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 11 deletions.
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 @@ -420,6 +422,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

0 comments on commit 83827a3

Please sign in to comment.