diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py index 616035473e7e..92da28bde989 100644 --- a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py +++ b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py @@ -2,6 +2,7 @@ Unit tests for /api/contentstore/v2/downstreams/* JSON APIs. """ from unittest.mock import patch +from django.conf import settings from cms.lib.xblock.upstream_sync import UpstreamLink, BadUpstream from common.djangoapps.student.tests.factories import UserFactory @@ -12,7 +13,13 @@ from .. import downstreams as downstreams_views -MOCK_UPSTREAM_REF = "mock-upstream-ref" +MOCK_LIB_KEY = "lib:OpenedX:CSPROB3" +MOCK_UPSTREAM_REF = "lb:OpenedX:CSPROB3:html:843b4c73-1e2d-4ced-a0ff-24e503cdb3e4" +MOCK_UPSTREAM_LINK = "{mfe_url}/library/{lib_key}/components?usageKey={usage_key}".format( + mfe_url=settings.COURSE_AUTHORING_MICROFRONTEND_URL, + lib_key=MOCK_LIB_KEY, + usage_key=MOCK_UPSTREAM_REF, +) MOCK_UPSTREAM_ERROR = "your LibraryGPT subscription has expired" @@ -92,6 +99,7 @@ def test_200_good_upstream(self): assert response.data['upstream_ref'] == MOCK_UPSTREAM_REF assert response.data['error_message'] is None assert response.data['ready_to_sync'] is True + assert response.data['upstream_link'] == MOCK_UPSTREAM_LINK @patch.object(UpstreamLink, "get_for_block", _get_upstream_link_bad) def test_200_bad_upstream(self): @@ -104,6 +112,7 @@ def test_200_bad_upstream(self): assert response.data['upstream_ref'] == MOCK_UPSTREAM_REF assert response.data['error_message'] == MOCK_UPSTREAM_ERROR assert response.data['ready_to_sync'] is False + assert response.data['upstream_link'] is None def test_200_no_upstream(self): """ @@ -115,6 +124,7 @@ def test_200_no_upstream(self): assert response.data['upstream_ref'] is None assert "is not linked" in response.data['error_message'] assert response.data['ready_to_sync'] is False + assert response.data['upstream_link'] is None class PutDownstreamViewTest(_DownstreamViewTestMixin, SharedModuleStoreTestCase): diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 2405af2479ec..46f2dd322efa 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -68,7 +68,7 @@ "add-xblock-component-support-legend", "add-xblock-component-support-level", "add-xblock-component-menu-problem", "xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history", "tag-list", "unit-outline", "container-message", "container-access", "license-selector", "copy-clipboard-button", - "edit-title-button", + "edit-title-button", "edit-upstream-alert", ] diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py index d0d414cbb46a..df8d8dc6251f 100644 --- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py +++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py @@ -587,6 +587,10 @@ def _create_block(request): boilerplate=request.json.get("boilerplate"), ) + response = { + "locator": str(created_block.location), + "courseKey": str(created_block.location.course_key), + } # If it contains library_content_key, the block is being imported from a v2 library # so it needs to be synced with upstream block. if upstream_ref := request.json.get("library_content_key"): @@ -602,13 +606,9 @@ def _create_block(request): ) return JsonResponse({"error": str(exc)}, status=400) modulestore().update_item(created_block, request.user.id) + response['upstreamRef'] = upstream_ref - return JsonResponse( - { - "locator": str(created_block.location), - "courseKey": str(created_block.location.course_key), - } - ) + return JsonResponse(response) def _get_source_index(source_usage_key, source_parent): diff --git a/cms/lib/xblock/upstream_sync.py b/cms/lib/xblock/upstream_sync.py index 2b1082fa7aeb..0d95931ce29d 100644 --- a/cms/lib/xblock/upstream_sync.py +++ b/cms/lib/xblock/upstream_sync.py @@ -15,6 +15,7 @@ import typing as t from dataclasses import dataclass, asdict +from django.conf import settings from django.core.exceptions import PermissionDenied from django.utils.translation import gettext_lazy as _ from rest_framework.exceptions import NotFound @@ -90,6 +91,19 @@ def ready_to_sync(self) -> bool: self.version_available > (self.version_declined or 0) ) + @property + def upstream_link(self) -> str | None: + """ + Link to edit/view upstream block in library. + """ + if self.version_available is None or self.upstream_ref is None: + return None + try: + usage_key = LibraryUsageLocatorV2.from_string(self.upstream_ref) + except InvalidKeyError: + return None + return _get_library_xblock_url(usage_key) + def to_json(self) -> dict[str, t.Any]: """ Get an JSON-API-friendly representation of this upstream link. @@ -97,6 +111,7 @@ def to_json(self) -> dict[str, t.Any]: return { **asdict(self), "ready_to_sync": self.ready_to_sync, + "upstream_link": self.upstream_link, } @classmethod @@ -349,6 +364,17 @@ def sever_upstream_link(downstream: XBlock) -> None: setattr(downstream, fetched_upstream_field, None) # Null out upstream_display_name, et al. +def _get_library_xblock_url(usage_key: LibraryUsageLocatorV2): + """ + Gets authoring url for given library_key. + """ + library_url = None + if mfe_base_url := settings.COURSE_AUTHORING_MICROFRONTEND_URL: # type: ignore + library_key = usage_key.lib_key + library_url = f'{mfe_base_url}/library/{library_key}/components?usageKey={usage_key}' + return library_url + + class UpstreamSyncMixin(XBlockMixin): """ Allows an XBlock in the CMS to be associated & synced with an upstream. diff --git a/cms/static/js/spec_helpers/edit_helpers.js b/cms/static/js/spec_helpers/edit_helpers.js index acfdeff32344..4c7e7d5a5815 100644 --- a/cms/static/js/spec_helpers/edit_helpers.js +++ b/cms/static/js/spec_helpers/edit_helpers.js @@ -92,6 +92,7 @@ installEditTemplates = function(append) { TemplateHelpers.installTemplate('edit-xblock-modal'); TemplateHelpers.installTemplate('editor-mode-button'); TemplateHelpers.installTemplate('edit-title-button'); + TemplateHelpers.installTemplate('edit-upstream-alert'); // Add templates needed by the settings editor TemplateHelpers.installTemplate('metadata-editor'); diff --git a/cms/static/js/views/modals/edit_xblock.js b/cms/static/js/views/modals/edit_xblock.js index 7182d8b0e51a..b5b69c721b10 100644 --- a/cms/static/js/views/modals/edit_xblock.js +++ b/cms/static/js/views/modals/edit_xblock.js @@ -75,6 +75,27 @@ function($, _, Backbone, gettext, BaseModal, ViewUtils, XBlockViewUtils, XBlockE this.$('.modal-window-title').html(this.loadTemplate('edit-title-button')({title: title})); }, + createWarningToast: function(upstreamLink) { + // xss-lint: disable=javascript-jquery-insertion + this.$('.modal-header').before(this.loadTemplate('edit-upstream-alert')({ + upstreamLink: upstreamLink, + })); + }, + + getXBlockUpstreamLink: function() { + const usageKey = this.xblockElement.data('locator'); + $.ajax({ + url: '/api/contentstore/v2/downstreams/' + usageKey, + type: 'GET', + success: function(data) { + if (data?.upstream_link) { + this.createWarningToast(data.upstream_link); + } + }.bind(this), + notifyOnError: false, + }) + }, + onDisplayXBlock: function() { var editorView = this.editorView, title = this.getTitle(), @@ -101,6 +122,7 @@ function($, _, Backbone, gettext, BaseModal, ViewUtils, XBlockViewUtils, XBlockE } else { this.$('.modal-window-title').text(title); } + this.getXBlockUpstreamLink(); // If the xblock is not using custom buttons then choose which buttons to show if (!editorView.hasCustomButtons()) { diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index 69b28e920bdb..be7088746ff8 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -408,7 +408,13 @@ function($, _, Backbone, gettext, BasePage, || (useNewVideoEditor === 'True' && blockType === 'video') || (useNewProblemEditor === 'True' && blockType === 'problem') ) { - var destinationUrl = primaryHeader.attr('authoring_MFE_base_url') + '/' + blockType + '/' + encodeURI(primaryHeader.attr('data-usage-id')); + var destinationUrl = primaryHeader.attr('authoring_MFE_base_url') + + '/' + blockType + + '/' + encodeURI(primaryHeader.attr('data-usage-id')); + var upstreamRef = primaryHeader.attr('data-upstream-ref'); + if(upstreamRef) { + destinationUrl += '?upstreamLibRef=' + upstreamRef; + } window.location.href = destinationUrl; return; } @@ -806,9 +812,10 @@ function($, _, Backbone, gettext, BasePage, var matchBlockTypeFromLocator = /\@(.*?)\+/; var blockType = data.locator.match(matchBlockTypeFromLocator); } - if((useNewTextEditor === 'True' && blockType.includes('html')) + // open mfe editors for new blocks only and not for content imported from libraries + if(!data.hasOwnProperty('upstreamRef') && ((useNewTextEditor === 'True' && blockType.includes('html')) || (useNewVideoEditor === 'True' && blockType.includes('video')) - || (useNewProblemEditor === 'True' && blockType.includes('problem')) + || (useNewProblemEditor === 'True' && blockType.includes('problem'))) ){ var destinationUrl; if (useVideoGalleryFlow === "True" && blockType.includes("video")) { diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss index 6f017ca2c188..9737782e8305 100644 --- a/cms/static/sass/views/_container.scss +++ b/cms/static/sass/views/_container.scss @@ -595,3 +595,38 @@ } } } + +.modal-alert { + display: flex; + align-items: center; + padding: $baseline; + margin-bottom: $baseline; + + &.warning { + background-color: $state-warning-bg; + + .fa-warning { + color: $orange; + margin-right: $baseline; + } + } + + .action-btn { + text-wrap: nowrap; + background-color: $black; + color: $white; + margin-left: auto; + padding: ($baseline/2); + + .fa { + margin-left: ($baseline/4); + color: $white + } + + &:hover, + &:focus, + &:active { + background-color: lighten($black, 20%); + } + } +} diff --git a/cms/templates/js/edit-upstream-alert.underscore b/cms/templates/js/edit-upstream-alert.underscore new file mode 100644 index 000000000000..d791fdb5d381 --- /dev/null +++ b/cms/templates/js/edit-upstream-alert.underscore @@ -0,0 +1,18 @@ +