Skip to content

Commit

Permalink
feat: adds Library Content (v2) button to Studio Unit page (#35670)
Browse files Browse the repository at this point in the history
Requires that v2 libraries are enabled.
  • Loading branch information
navinkarkera authored Oct 21, 2024
1 parent e2d6765 commit 7e8fb43
Show file tree
Hide file tree
Showing 16 changed files with 220 additions and 45 deletions.
2 changes: 2 additions & 0 deletions cms/djangoapps/contentstore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ def xblock_type_display_name(xblock, default_display_name=None):
# description like "Multiple Choice Problem", but that won't work if our 'block' argument is just the block_type
# string ("problem").
return _('Problem')
elif category == 'library_v2':
return _('Library Content')
component_class = XBlock.load_class(category)
if hasattr(component_class, 'display_name') and component_class.display_name.default:
return _(component_class.display_name.default) # lint-amnesty, pylint: disable=translation-of-non-string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class ContainerHandlerSerializer(serializers.Serializer):
unit_block_id = serializers.CharField(source="unit.location.block_id")
subsection_location = serializers.CharField(source="subsection.location")
course_sequence_ids = serializers.ListField(child=serializers.CharField())
library_content_picker_url = serializers.CharField()

def get_assets_url(self, obj):
"""
Expand Down
8 changes: 5 additions & 3 deletions cms/djangoapps/contentstore/rest_api/v2/views/downstreams.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,13 @@ def put(self, request: _AuthenticatedRequest, usage_key_string: str) -> Response
# Note that, if this fails and we raise a 4XX, then we will not call modulstore().update_item,
# thus preserving the former value of `downstream.upstream`.
downstream.upstream = new_upstream_ref
sync_param = request.data.get("sync", "false").lower()
if sync_param not in ["true", "false"]:
sync_param = request.data.get("sync", "false")
if isinstance(sync_param, str):
sync_param = sync_param.lower()
if sync_param not in ["true", "false", True, False]:
raise ValidationError({"sync": "must be 'true' or 'false'"})
try:
if sync_param == "true":
if sync_param == "true" or sync_param is True:
sync_from_upstream(downstream=downstream, user=request.user)
else:
# Even if we're not syncing (i.e., updating the downstream's values with the upstream's), we still need
Expand Down
57 changes: 34 additions & 23 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,28 @@
exam_setting_view_enabled,
libraries_v1_enabled,
libraries_v2_enabled,
split_library_view_on_dashboard,
use_new_advanced_settings_page,
use_new_course_outline_page,
use_new_certificates_page,
use_new_export_page,
use_new_files_uploads_page,
use_new_grading_page,
use_new_group_configurations_page,
use_new_course_team_page,
use_new_home_page,
use_new_import_page,
use_new_schedule_details_page,
use_new_text_editor,
use_new_textbooks_page,
use_new_unit_page,
use_new_updates_page,
use_new_video_editor,
use_new_video_uploads_page,
use_new_custom_pages,
)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from common.djangoapps.course_action_state.models import CourseRerunUIStateManager, CourseRerunState
from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError
from common.djangoapps.course_modes.models import CourseMode
Expand Down Expand Up @@ -79,29 +100,6 @@
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.content_type_gating.partitions import CONTENT_TYPE_GATING_SCHEME
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
from cms.djangoapps.contentstore.toggles import (
split_library_view_on_dashboard,
use_new_advanced_settings_page,
use_new_course_outline_page,
use_new_certificates_page,
use_new_export_page,
use_new_files_uploads_page,
use_new_grading_page,
use_new_group_configurations_page,
use_new_course_team_page,
use_new_home_page,
use_new_import_page,
use_new_schedule_details_page,
use_new_text_editor,
use_new_textbooks_page,
use_new_unit_page,
use_new_updates_page,
use_new_video_editor,
use_new_video_uploads_page,
use_new_custom_pages,
)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.models.settings.course_metadata import CourseMetadata
from xmodule.library_tools import LegacyLibraryToolsService
from xmodule.course_block import DEFAULT_START_DATE # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.data import CertificatesDisplayBehaviors
Expand Down Expand Up @@ -431,6 +429,18 @@ def get_course_outline_url(course_locator) -> str:
return course_outline_url


def get_library_content_picker_url(course_locator) -> str:
"""
Gets course authoring microfrontend library content picker URL for the given parent block.
"""
content_picker_url = None
if libraries_v2_enabled():
mfe_base_url = get_course_authoring_url(course_locator)
content_picker_url = f'{mfe_base_url}/component-picker'

return content_picker_url


def get_unit_url(course_locator, unit_locator) -> str:
"""
Gets course authoring microfrontend URL for unit page view.
Expand Down Expand Up @@ -2045,6 +2055,7 @@ def get_container_handler_context(request, usage_key, course, xblock): # pylint
'user_clipboard': user_clipboard,
'is_fullwidth_content': is_library_xblock,
'course_sequence_ids': course_sequence_ids,
'library_content_picker_url': get_library_content_picker_url(course.id),
}
return context

Expand Down
2 changes: 2 additions & 0 deletions cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ def xblock_handler(request, usage_key_string=None):
if duplicate_source_locator is not present
:staged_content: use "clipboard" to paste from the OLX user's clipboard. (Incompatible with all other
fields except parent_locator)
:library_content_key: the key of the library content to add. (Incompatible with
all other fields except parent_locator)
The locator (unicode representation of a UsageKey) for the created xblock (minus children) is returned.
"""
return handle_xblock(request, usage_key_string)
Expand Down
24 changes: 20 additions & 4 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks
from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag
from cms.djangoapps.contentstore.helpers import is_unit
from cms.djangoapps.contentstore.toggles import use_new_problem_editor, use_new_unit_page
from cms.djangoapps.contentstore.toggles import libraries_v2_enabled, use_new_problem_editor, use_new_unit_page
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio
from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
Expand All @@ -43,7 +43,16 @@
log = logging.getLogger(__name__)

# NOTE: This list is disjoint from ADVANCED_COMPONENT_TYPES
COMPONENT_TYPES = ['discussion', 'library', 'html', 'openassessment', 'problem', 'video', 'drag-and-drop-v2']
COMPONENT_TYPES = [
'discussion',
'library',
'library_v2', # Not an XBlock
'html',
'openassessment',
'problem',
'video',
'drag-and-drop-v2',
]

ADVANCED_COMPONENT_TYPES = sorted({name for name, class_ in XBlock.load_classes()} - set(COMPONENT_TYPES))

Expand Down Expand Up @@ -97,6 +106,10 @@ def _load_mixed_class(category):
"""
Load an XBlock by category name, and apply all defined mixins
"""
# Libraries v2 content doesn't have an XBlock.
if category == 'library_v2':
return None

component_class = XBlock.load_class(category)
mixologist = Mixologist(settings.XBLOCK_MIXINS)
return mixologist.mix(component_class)
Expand Down Expand Up @@ -247,7 +260,8 @@ def create_support_legend_dict():
'problem': _("Problem"),
'video': _("Video"),
'openassessment': _("Open Response"),
'library': _("Library Content"),
'library': _("Legacy Library"),
'library_v2': _("Library Content"),
'drag-and-drop-v2': _("Drag and Drop"),
}

Expand Down Expand Up @@ -277,7 +291,7 @@ def create_support_legend_dict():
templates_for_category = []
component_class = _load_mixed_class(category)

if support_level_without_template and category != 'library':
if support_level_without_template and category not in ['library']:
# add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington)
Expand Down Expand Up @@ -472,6 +486,8 @@ def _filter_disabled_blocks(all_blocks):
Filter out disabled xblocks from the provided list of xblock names.
"""
disabled_block_names = [block.name for block in disabled_xblocks()]
if not libraries_v2_enabled():
disabled_block_names.append('library_v2')
return [block_name for block_name in all_blocks if block_name not in disabled_block_names]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from cms.djangoapps.contentstore.toggles import ENABLE_DEFAULT_ADVANCED_PROBLEM_EDITOR_FLAG
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.lib.ai_aside_summary_config import AiAsideSummaryConfig
from cms.lib.xblock.upstream_sync import BadUpstream, sync_from_upstream
from common.djangoapps.static_replace import replace_static_urls
from common.djangoapps.student.auth import (
has_studio_read_access,
Expand Down Expand Up @@ -586,6 +587,18 @@ def _create_block(request):
boilerplate=request.json.get("boilerplate"),
)

# 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"):
try:
# Set `created_block.upstream` and then sync this with the upstream (library) version.
created_block.upstream = upstream_ref
sync_from_upstream(downstream=created_block, user=request.user)
except BadUpstream:
_delete_item(created_block.location, request.user)
return JsonResponse({"error": _("Invalid library xblock reference.")}, status=400)
modulestore().update_item(created_block, request.user.id)

return JsonResponse(
{
"locator": str(created_block.location),
Expand Down
Binary file added cms/static/images/large-library_v2-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 68 additions & 0 deletions cms/static/js/views/components/add_library_content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Provides utilities to open and close the library content picker.
*
*/
define(['jquery', 'underscore', 'gettext', 'js/views/modals/base_modal'],
function($, _, gettext, BaseModal) {
'use strict';

var AddLibraryContent = BaseModal.extend({
options: $.extend({}, BaseModal.prototype.options, {
modalName: 'add-component-from-library',
modalSize: 'lg',
view: 'studio_view',
viewSpecificClasses: 'modal-add-component-picker confirm',
// Translators: "title" is the name of the current component being edited.
titleFormat: gettext('Add library content'),
addPrimaryActionButton: false,
}),

initialize: function() {
BaseModal.prototype.initialize.call(this);
// Add event listen to close picker when the iframe tells us to
const handleMessage = (event) => {
if (event.data?.type === 'pickerComponentSelected') {
var requestData = {
library_content_key: event.data.usageKey,
category: event.data.category,
}
this.refreshFunction(requestData);
this.hide();
}
};
this.messageListener = window.addEventListener("message", handleMessage);
this.cleanupListener = () => { window.removeEventListener("message", handleMessage) };
},

hide: function() {
BaseModal.prototype.hide.call(this);
this.cleanupListener();
},

/**
* Adds the action buttons to the modal.
*/
addActionButtons: function() {
this.addActionButton('cancel', gettext('Cancel'));
},

/**
* Show a component picker modal from library.
* @param contentPickerUrl Url for component picker
* @param refreshFunction A function to refresh the block after it has been updated
*/
showComponentPicker: function(contentPickerUrl, refreshFunction) {
this.contentPickerUrl = contentPickerUrl;
this.refreshFunction = refreshFunction;

this.render();
this.show();
},

getContentHtml: function() {
return `<iframe src="${this.contentPickerUrl}" onload="this.contentWindow.focus()" frameborder="0" style="width: 100%; height: 100%;"/>`;
},
});

return AddLibraryContent;
});
37 changes: 28 additions & 9 deletions cms/static/js/views/components/add_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
*/
define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils',
'js/views/components/add_xblock_button', 'js/views/components/add_xblock_menu',
'js/views/components/add_library_content',
'edx-ui-toolkit/js/utils/html-utils'],
function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, HtmlUtils) {
function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, AddLibraryContent, HtmlUtils) {
'use strict';

var AddXBlockComponent = BaseView.extend({
Expand Down Expand Up @@ -67,14 +68,32 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Htm
oldOffset = ViewUtils.getScrollOffset(this.$el);
event.preventDefault();
this.closeNewComponent(event);
ViewUtils.runOperationShowingMessage(
gettext('Adding'),
_.bind(this.options.createComponent, this, saveData, $element)
).always(function() {
// Restore the scroll position of the buttons so that the new
// component appears above them.
ViewUtils.setScrollOffset(self.$el, oldOffset);
});

if (saveData.type === 'library_v2') {
var modal = new AddLibraryContent();
modal.showComponentPicker(
this.options.libraryContentPickerUrl,
function(data) {
ViewUtils.runOperationShowingMessage(
gettext('Adding'),
_.bind(this.options.createComponent, this, data, $element),
).always(function() {
// Restore the scroll position of the buttons so that the new
// component appears above them.
ViewUtils.setScrollOffset(self.$el, oldOffset);
});
}.bind(this)
);
} else {
ViewUtils.runOperationShowingMessage(
gettext('Adding'),
_.bind(this.options.createComponent, this, saveData, $element),
).always(function() {
// Restore the scroll position of the buttons so that the new
// component appears above them.
ViewUtils.setScrollOffset(self.$el, oldOffset);
});
}
}
});

Expand Down
7 changes: 4 additions & 3 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ function($, _, Backbone, gettext, BasePage,
var component = new AddXBlockComponent({
el: element,
createComponent: _.bind(self.createComponent, self),
collection: self.options.templates
collection: self.options.templates,
libraryContentPickerUrl: self.options.libraryContentPickerUrl,
});
component.render();
});
Expand All @@ -224,7 +225,7 @@ function($, _, Backbone, gettext, BasePage,
},

initializePasteButton() {
if (this.options.canEdit && !self.options.isIframeEmbed) {
if (this.options.canEdit && !this.options.isIframeEmbed) {
// We should have the user's clipboard status.
const data = this.options.clipboardData;
this.refreshPasteButton(data);
Expand All @@ -241,7 +242,7 @@ function($, _, Backbone, gettext, BasePage,
refreshPasteButton(data) {
// Do not perform any changes on paste button since they are not
// rendered on Library or LibraryContent pages
if (!this.isLibraryPage && !this.isLibraryContentPage && !self.options.isIframeEmbed) {
if (!this.isLibraryPage && !this.isLibraryContentPage && !this.options.isIframeEmbed) {
// 'data' is the same data returned by the "get clipboard status" API endpoint
// i.e. /api/content-staging/v1/clipboard/
if (this.options.canEdit && data.content) {
Expand Down
7 changes: 7 additions & 0 deletions cms/static/sass/assets/_graphics.scss
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,10 @@
height: ($baseline*3);
background: url('#{$static-path}/images/large-library-icon.png') center no-repeat;
}

.large-library_v2-icon {
display: inline-block;
width: ($baseline*3);
height: ($baseline*3);
background: url('#{$static-path}/images/large-library_v2-icon.png') center no-repeat;
}
Loading

0 comments on commit 7e8fb43

Please sign in to comment.