From c3f483a9cc0ffc55b5487a7c04f541a887bf31af Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 16 Oct 2024 18:44:27 +0530 Subject: [PATCH 1/8] feat: show alert while editing v2 library content --- .../v2/views/tests/test_downstreams.py | 8 +++++- .../contentstore/views/component.py | 2 +- cms/lib/xblock/upstream_sync.py | 26 +++++++++++++++++++ cms/static/js/spec_helpers/edit_helpers.js | 1 + cms/static/js/views/modals/edit_xblock.js | 21 +++++++++++++++ cms/templates/js/basic-modal.underscore | 2 ++ .../js/edit-upstream-alert.underscore | 20 ++++++++++++++ 7 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 cms/templates/js/edit-upstream-alert.underscore 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..d86a69de289d 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,9 @@ 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 = settings.COURSE_AUTHORING_MICROFRONTEND_URL + '/library/' + MOCK_LIB_KEY MOCK_UPSTREAM_ERROR = "your LibraryGPT subscription has expired" @@ -92,6 +95,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 +108,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 +120,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 b89bef0f6709..f5b562203c72 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -67,7 +67,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/lib/xblock/upstream_sync.py b/cms/lib/xblock/upstream_sync.py index 2b1082fa7aeb..a34d5f7de95b 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: + """ + Link to edit/view upstream block in library. + """ + if self.version_available is None: + return None + try: + usage_key = LibraryUsageLocatorV2.from_string(self.upstream_ref) + except InvalidKeyError: + return None + return _get_library_authoring_url(usage_key.lib_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_authoring_url(library_key: str): + """ + Gets authoring url for given library_key. + """ + library_url = None + mfe_base_url = settings.COURSE_AUTHORING_MICROFRONTEND_URL + if mfe_base_url: + library_url = f'{mfe_base_url}/library/{library_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..327dec4bcaa9 100644 --- a/cms/static/js/views/modals/edit_xblock.js +++ b/cms/static/js/views/modals/edit_xblock.js @@ -75,6 +75,26 @@ function($, _, Backbone, gettext, BaseModal, ViewUtils, XBlockViewUtils, XBlockE this.$('.modal-window-title').html(this.loadTemplate('edit-title-button')({title: title})); }, + createWarningToast: function(upstreamLink) { + this.$('.modal-window-alerts').html(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 +121,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/templates/js/basic-modal.underscore b/cms/templates/js/basic-modal.underscore index 4273fe4f9956..1adcf1b3b5b7 100644 --- a/cms/templates/js/basic-modal.underscore +++ b/cms/templates/js/basic-modal.underscore @@ -5,6 +5,8 @@