diff --git a/cms/lib/xblock/test/test_upstream_sync.py b/cms/lib/xblock/test/test_upstream_sync.py index 5db020393eab..6a3af7455b5d 100644 --- a/cms/lib/xblock/test/test_upstream_sync.py +++ b/cms/lib/xblock/test/test_upstream_sync.py @@ -48,6 +48,8 @@ def setUp(self): upstream.data = "Upstream content V2" 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 @@ -133,6 +135,16 @@ def test_sync_updates_happy_path(self): upstream.data = "Upstream content V3" 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 == "Upstream content V2" + + # 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 @@ -157,6 +169,7 @@ def test_sync_updates_to_modified_content(self): upstream.display_name = "Upstream Title V3" upstream.data = "Upstream content V3" upstream.save() + libs.publish_changes(self.library.key, self.user.id) # Downstream modifications downstream.display_name = "Downstream Title Override" # "safe" customization @@ -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 = "Upstream content V3" 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 @@ -299,6 +320,7 @@ def test_prompt_and_decline_sync(self): upstream = xblock.load_block(self.upstream_key, self.user) upstream.data = "Upstream content V4" 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 diff --git a/cms/lib/xblock/upstream_sync.py b/cms/lib/xblock/upstream_sync.py index 785cc7dc7e36..2ee22319a238 100644 --- a/cms/lib/xblock/upstream_sync.py +++ b/cms/lib/xblock/upstream_sync.py @@ -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, ) @@ -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 diff --git a/cms/static/js/views/modals/preview_v2_library_changes.js b/cms/static/js/views/modals/preview_v2_library_changes.js new file mode 100644 index 000000000000..e80c40e3ea3e --- /dev/null +++ b/cms/static/js/views/modals/preview_v2_library_changes.js @@ -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 ` +