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

feat: show alert while editing v2 library content [FC-0062] #35700

Merged
merged 9 commits into from
Oct 23, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"


Expand Down Expand Up @@ -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):
Expand All @@ -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):
"""
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand All @@ -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):
Expand Down
26 changes: 26 additions & 0 deletions cms/lib/xblock/upstream_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -90,13 +91,27 @@ 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.
"""
return {
**asdict(self),
"ready_to_sync": self.ready_to_sync,
"upstream_link": self.upstream_link,
}

@classmethod
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions cms/static/js/spec_helpers/edit_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
22 changes: 22 additions & 0 deletions cms/static/js/views/modals/edit_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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()) {
Expand Down
13 changes: 10 additions & 3 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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")) {
Expand Down
35 changes: 35 additions & 0 deletions cms/static/sass/views/_container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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%);
}
}
}
18 changes: 18 additions & 0 deletions cms/templates/js/edit-upstream-alert.underscore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="modal-alert warning" role="alert">
<span class="fa fa-lg fa-warning" aria-hidden="true"/><span class="sr"><%- gettext("Warning") %></span>
<div class="message-content">
<strong><%- gettext("You are editing content from a Library.") %></strong>
<p>
<%- gettext("Edits made here will only be reflected in this course. These edits may be overridden later if updates are accepted.") %>
</p>
</div>
<a
href="<%- upstreamLink %>"
target="_blank"
class="btn action-btn"
>
<%- gettext('View in Library') %>
<span class="fa fa-external-link" aria-hidden="true" />
</a>
</div>

Loading