feat: copy/paste containers (units/subsections/sections) in Studio (#37008)
* feat: copy endpoint for Library Containers * fix: make source_usage_key optional and removing upstram info for xblock olx * test: add tests * refactor: remove unecessary changes to reduce diff * fix: change assert * feat: add `write_upstream` field to ContainerSerializer * fix: remove comment * refactor: change `source_usage_key` type and more * fix: try to infer the source version * fix: InvalidKeyError while copying container with assets * fix: read source_version from OLX * fix: remove store check * fix: change ident Co-authored-by: Braden MacDonald <mail@bradenm.com> * feat: fill source_version and make get_component_version_from_block public * refactor: rename `source_key` to `copied_from_block` * test: add test to `write_copied_from=false` * fix: removing unused fallback elif * fix: remove `copied_from_block` param --------- Co-authored-by: Braden MacDonald <mail@bradenm.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import gettext as _
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locator import DefinitionLocator, LocalId
|
||||
from openedx.core.djangoapps.content_tagging.types import TagValuesByObjectIdDict
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.runtime import IdGenerator
|
||||
@@ -286,6 +287,26 @@ class StaticFileNotices:
|
||||
error_files: list[str] = Factory(list)
|
||||
|
||||
|
||||
def _rewrite_static_asset_references(downstream_xblock: XBlock, substitutions: dict[str, str], user_id: int) -> None:
|
||||
"""
|
||||
Rewrite the static asset references in the OLX string to point to the new locations in the course.
|
||||
"""
|
||||
store = modulestore()
|
||||
if hasattr(downstream_xblock, "data"):
|
||||
data_with_substitutions = downstream_xblock.data
|
||||
for old_static_ref, new_static_ref in substitutions.items():
|
||||
data_with_substitutions = _replace_strings(
|
||||
data_with_substitutions,
|
||||
old_static_ref,
|
||||
new_static_ref,
|
||||
)
|
||||
downstream_xblock.data = data_with_substitutions
|
||||
store.update_item(downstream_xblock, user_id)
|
||||
|
||||
for child in downstream_xblock.get_children():
|
||||
_rewrite_static_asset_references(child, substitutions, user_id)
|
||||
|
||||
|
||||
def _insert_static_files_into_downstream_xblock(
|
||||
downstream_xblock: XBlock, staged_content_id: int, request
|
||||
) -> StaticFileNotices:
|
||||
@@ -308,21 +329,12 @@ def _insert_static_files_into_downstream_xblock(
|
||||
static_files=static_files,
|
||||
)
|
||||
|
||||
# Rewrite the OLX's static asset references to point to the new
|
||||
# locations for those assets. See _import_files_into_course for more
|
||||
# info on why this is necessary.
|
||||
store = modulestore()
|
||||
if hasattr(downstream_xblock, "data") and substitutions:
|
||||
data_with_substitutions = downstream_xblock.data
|
||||
for old_static_ref, new_static_ref in substitutions.items():
|
||||
data_with_substitutions = _replace_strings(
|
||||
data_with_substitutions,
|
||||
old_static_ref,
|
||||
new_static_ref,
|
||||
)
|
||||
downstream_xblock.data = data_with_substitutions
|
||||
if store is not None:
|
||||
store.update_item(downstream_xblock, request.user.id)
|
||||
if substitutions:
|
||||
# Rewrite the OLX's static asset references to point to the new
|
||||
# locations for those assets. See _import_files_into_course for more
|
||||
# info on why this is necessary.
|
||||
_rewrite_static_asset_references(downstream_xblock, substitutions, request.user.id)
|
||||
|
||||
return notices
|
||||
|
||||
|
||||
@@ -375,9 +387,10 @@ def import_staged_content_from_user_clipboard(parent_key: UsageKey, request) ->
|
||||
parent_xblock,
|
||||
store,
|
||||
user=request.user,
|
||||
slug_hint=user_clipboard.source_usage_key.block_id,
|
||||
copied_from_block=str(user_clipboard.source_usage_key),
|
||||
copied_from_version_num=user_clipboard.content.version_num,
|
||||
slug_hint=(
|
||||
user_clipboard.source_usage_key.block_id
|
||||
if isinstance(user_clipboard.source_usage_key, UsageKey) else None
|
||||
),
|
||||
tags=user_clipboard.content.tags,
|
||||
)
|
||||
|
||||
@@ -441,7 +454,7 @@ def _fetch_and_set_upstream_link(
|
||||
Fetch and set upstream link for the given xblock which is being pasted. This function handles following cases:
|
||||
* the xblock is copied from a v2 library; the library block is set as upstream.
|
||||
* the xblock is copied from a course; no upstream is set, only copied_from_block is set.
|
||||
* the xblock is copied from a course where the source block was imported from a library; the original libary block
|
||||
* the xblock is copied from a course where the source block was imported from a library; the original library block
|
||||
is set as upstream.
|
||||
"""
|
||||
# Try to link the pasted block (downstream) to the copied block (upstream).
|
||||
@@ -491,13 +504,8 @@ def _import_xml_node_to_parent(
|
||||
user: User,
|
||||
# Hint to use as usage ID (block_id) for the new XBlock
|
||||
slug_hint: str | None = None,
|
||||
# UsageKey of the XBlock that this one is a copy of
|
||||
copied_from_block: str | None = None,
|
||||
# Positive int version of source block, if applicable (e.g., library block).
|
||||
# Zero if not applicable (e.g., course block).
|
||||
copied_from_version_num: int = 0,
|
||||
# Content tags applied to the source XBlock(s)
|
||||
tags: dict[str, str] | None = None,
|
||||
tags: TagValuesByObjectIdDict | None = None,
|
||||
) -> XBlock:
|
||||
"""
|
||||
Given an XML node representing a serialized XBlock (OLX), import it into modulestore 'store' as a child of the
|
||||
@@ -508,6 +516,8 @@ def _import_xml_node_to_parent(
|
||||
runtime = parent_xblock.runtime
|
||||
parent_key = parent_xblock.scope_ids.usage_id
|
||||
block_type = node.tag
|
||||
node_copied_from = node.attrib.get('copied_from_block', None)
|
||||
node_copied_version = node.attrib.get('copied_from_version', None)
|
||||
|
||||
# Modulestore's IdGenerator here is SplitMongoIdManager which is assigned
|
||||
# by CachingDescriptorSystem Runtime and since we need our custom ImportIdGenerator
|
||||
@@ -565,8 +575,10 @@ def _import_xml_node_to_parent(
|
||||
|
||||
if xblock_class.has_children and temp_xblock.children:
|
||||
raise NotImplementedError("We don't yet support pasting XBlocks with children")
|
||||
if copied_from_block:
|
||||
_fetch_and_set_upstream_link(copied_from_block, copied_from_version_num, temp_xblock, user)
|
||||
|
||||
if node_copied_from:
|
||||
_fetch_and_set_upstream_link(node_copied_from, node_copied_version, temp_xblock, user)
|
||||
|
||||
# Save the XBlock into modulestore. We need to save the block and its parent for this to work:
|
||||
new_xblock = store.update_item(temp_xblock, user.id, allow_not_found=True)
|
||||
new_xblock.parent = parent_key
|
||||
@@ -582,26 +594,23 @@ def _import_xml_node_to_parent(
|
||||
|
||||
if not children_handled:
|
||||
for child_node in child_nodes:
|
||||
child_copied_from = _get_usage_key_from_node(child_node, copied_from_block) if copied_from_block else None
|
||||
_import_xml_node_to_parent(
|
||||
child_node,
|
||||
new_xblock,
|
||||
store,
|
||||
user=user,
|
||||
copied_from_block=str(child_copied_from),
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
# Copy content tags to the new xblock
|
||||
if new_xblock.upstream:
|
||||
# If this block is synced from an upstream (e.g. library content),
|
||||
# copy the tags from the upstream as ready-only
|
||||
content_tagging_api.copy_tags_as_read_only(
|
||||
new_xblock.upstream,
|
||||
new_xblock.location,
|
||||
)
|
||||
elif copied_from_block and tags:
|
||||
object_tags = tags.get(str(copied_from_block))
|
||||
elif tags and node_copied_from:
|
||||
object_tags = tags.get(node_copied_from)
|
||||
if object_tags:
|
||||
content_tagging_api.set_all_object_tags(
|
||||
content_key=new_xblock.location,
|
||||
@@ -794,27 +803,6 @@ def is_item_in_course_tree(item):
|
||||
return ancestor is not None
|
||||
|
||||
|
||||
def _get_usage_key_from_node(node, parent_id: str) -> UsageKey | None:
|
||||
"""
|
||||
Returns the UsageKey for the given node and parent ID.
|
||||
|
||||
If the parent_id is not a valid UsageKey, or there's no "url_name" attribute in the node, then will return None.
|
||||
"""
|
||||
parent_key = UsageKey.from_string(parent_id)
|
||||
parent_context = parent_key.context_key
|
||||
usage_key = None
|
||||
block_id = node.attrib.get("url_name")
|
||||
block_type = node.tag
|
||||
|
||||
if parent_context and block_id and block_type:
|
||||
usage_key = parent_context.make_usage_key(
|
||||
block_type=block_type,
|
||||
block_id=block_id,
|
||||
)
|
||||
|
||||
return usage_key
|
||||
|
||||
|
||||
def concat_static_file_notices(notices: list[StaticFileNotices]) -> StaticFileNotices:
|
||||
"""Combines multiple static file notices into a single object
|
||||
|
||||
|
||||
Reference in New Issue
Block a user