Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class CourseDetailsSerializer(serializers.Serializer):
pre_requisite_courses = serializers.ListField(child=CourseKeyField())
run = serializers.CharField()
self_paced = serializers.BooleanField()
has_changes = serializers.BooleanField()
short_description = serializers.CharField(allow_blank=True)
start_date = serializers.DateTimeField()
subtitle = serializers.CharField(allow_blank=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class UpstreamLinkSerializer(serializers.Serializer):
error_message = serializers.CharField(allow_null=True)
ready_to_sync = serializers.BooleanField()
downstream_customized = serializers.ListField(child=serializers.CharField(), allow_empty=True)
has_top_level_parent = serializers.BooleanField()
top_level_parent_key = serializers.CharField(allow_null=True)
ready_to_sync_children = UpstreamChildrenInfoSerializer(many=True, required=False)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ def test_children_content(self):
"version_declined": None,
"error_message": None,
"ready_to_sync": True,
"has_top_level_parent": False,
"top_level_parent_key": None,
"downstream_customized": [],
},
"user_partition_info": expected_user_partition_info,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _get_upstream_link_good_and_syncable(downstream):
version_declined=downstream.upstream_version_declined,
error_message=None,
downstream_customized=[],
has_top_level_parent=False,
top_level_parent_key=None,
upstream_name=downstream.upstream_display_name,
)

Expand Down
8 changes: 2 additions & 6 deletions cms/djangoapps/contentstore/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from olxcleaner.reporting import report_error_summary, report_errors
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocator, BlockUsageLocator
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocator
from openedx_events.content_authoring.data import CourseData
from openedx_events.content_authoring.signals import COURSE_RERUN_COMPLETED
from organizations.api import add_organization_course, ensure_organization
Expand Down Expand Up @@ -1641,11 +1641,7 @@ def handle_create_xblock_upstream_link(usage_key):
return
if xblock.top_level_downstream_parent_key is not None:
block_key = BlockKey.from_string(xblock.top_level_downstream_parent_key)
top_level_parent_usage_key = BlockUsageLocator(
xblock.course_id,
block_key.type,
block_key.id,
)
top_level_parent_usage_key = block_key.to_usage_key(xblock.course_id)
try:
ContainerLink.get_by_downstream_usage_key(top_level_parent_usage_key)
except ContainerLink.DoesNotExist:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,9 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
"edited_on": get_default_time_display(xblock.subtree_edited_on)
if xblock.subtree_edited_on
else None,
"edited_on_raw": str(xblock.subtree_edited_on)
if xblock.subtree_edited_on
else None,
"published": published,
"published_on": published_on,
"studio_url": xblock_studio_url(xblock, parent_xblock),
Expand Down Expand Up @@ -1331,7 +1334,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
# Disable adding or removing children component if xblock is imported from library
xblock_actions["childAddable"] = False
# Enable unlinking only for top level imported components
xblock_actions["unlinkable"] = not upstream_info["has_top_level_parent"]
xblock_actions["unlinkable"] = not upstream_info["top_level_parent_key"]

if is_xblock_unit:
# if xblock is a Unit we add the discussion_enabled option
Expand Down
16 changes: 12 additions & 4 deletions cms/lib/xblock/upstream_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class UpstreamLink:
version_declined: int | None # Latest version which the user has declined to sync with, if any.
error_message: str | None # If link is valid, None. Otherwise, a localized, human-friendly error message.
downstream_customized: list[str] | None # List of fields modified in downstream
has_top_level_parent: bool # True if this Upstream link has a top-level parent
top_level_parent_key: str | None # key of top-level parent if Upstream link has a one.

@property
def is_upstream_deleted(self) -> bool:
Expand Down Expand Up @@ -153,7 +153,7 @@ def ready_to_sync(self) -> bool:
from xmodule.modulestore.django import modulestore

# If this component/container has top-level parent, so we need to sync the parent
if self.has_top_level_parent:
if self.top_level_parent_key:
return False

if isinstance(self.upstream_key, LibraryUsageLocatorV2):
Expand Down Expand Up @@ -222,6 +222,10 @@ def try_get_for_block(cls, downstream: XBlock, log_error: bool = True) -> t.Self
downstream.usage_key,
downstream.upstream,
)
if top_level_parent_key := getattr(downstream, "top_level_downstream_parent_key", None):
top_level_parent_key = str(
BlockKey.from_string(top_level_parent_key).to_usage_key(downstream.usage_key.context_key)
)
return cls(
upstream_ref=getattr(downstream, "upstream", None),
upstream_name=getattr(downstream, "upstream_display_name", None),
Expand All @@ -232,7 +236,7 @@ def try_get_for_block(cls, downstream: XBlock, log_error: bool = True) -> t.Self
version_declined=None,
error_message=str(exc),
downstream_customized=getattr(downstream, "downstream_customized", []),
has_top_level_parent=getattr(downstream, "top_level_downstream_parent_key", None) is not None,
top_level_parent_key=top_level_parent_key,
)

@classmethod
Expand Down Expand Up @@ -306,6 +310,10 @@ def get_for_block(cls, downstream: XBlock) -> t.Self:
)
)

if top_level_parent_key := getattr(downstream, "top_level_downstream_parent_key", None):
top_level_parent_key = str(
BlockKey.from_string(top_level_parent_key).to_usage_key(downstream.usage_key.context_key)
)
result = cls(
upstream_ref=downstream.upstream,
upstream_key=upstream_key,
Expand All @@ -316,7 +324,7 @@ def get_for_block(cls, downstream: XBlock) -> t.Self:
version_declined=downstream.upstream_version_declined,
error_message=None,
downstream_customized=getattr(downstream, "downstream_customized", []),
has_top_level_parent=downstream.top_level_downstream_parent_key is not None,
top_level_parent_key=top_level_parent_key,
)

return result
Expand Down
2 changes: 1 addition & 1 deletion cms/templates/studio_xblock_wrapper.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
block_is_unit = is_unit(xblock)

upstream_info = UpstreamLink.try_get_for_block(xblock, log_error=False)
can_unlink = upstream_info.upstream_ref and not upstream_info.has_top_level_parent
can_unlink = upstream_info.upstream_ref and not upstream_info.top_level_parent_key
%>

<%namespace name='static' file='static_content.html'/>
Expand Down
2 changes: 2 additions & 0 deletions openedx/core/djangoapps/models/course_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def __init__(self, org, course_id, run):
self.self_paced = None
self.learning_info = []
self.instructor_info = []
self.has_changes = None

@classmethod
def fetch_about_attribute(cls, course_key, attribute):
Expand Down Expand Up @@ -127,6 +128,7 @@ def populate(cls, block):
course_details.video_thumbnail_image_asset_path = course_image_url(block, 'video_thumbnail_image')
course_details.language = block.language
course_details.self_paced = block.self_paced
course_details.has_changes = modulestore().has_changes(block)
course_details.learning_info = block.learning_info
course_details.instructor_info = block.instructor_info
course_details.title = block.display_name
Expand Down
24 changes: 20 additions & 4 deletions xmodule/tests/test_util_keys.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"""
Tests for xmodule/util/keys.py
"""
import ddt
import pytest
from unittest import TestCase
from unittest.mock import Mock

from opaque_keys.edx.locator import BlockUsageLocator
import ddt
import pytest
from opaque_keys.edx.keys import CourseKey
from xmodule.util.keys import BlockKey, derive_key
from opaque_keys.edx.locator import BlockUsageLocator

from xmodule.util.keys import BlockKey, derive_key

mock_block = Mock()
mock_block.id = CourseKey.from_string('course-v1:Beeper+B33P+BOOP')
Expand Down Expand Up @@ -70,3 +70,19 @@ def test_block_key_from_string_error(self, block_key_str):
@ddt.unpack
def test_block_key_to_string(self, block_key, block_key_str):
assert str(block_key) == block_key_str

@ddt.data(
[BlockKey('chapter', 'some-id'), BlockUsageLocator(
mock_block.id,
'chapter',
'some-id'
)],
[BlockKey('section', 'one-more-id'), BlockUsageLocator(
mock_block.id,
'section',
'one-more-id'
)]
)
@ddt.unpack
def test_block_key_to_usage_key(self, block_key: BlockKey, block_key_str):
assert block_key.to_usage_key(mock_block.id) == block_key_str
8 changes: 7 additions & 1 deletion xmodule/util/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import hashlib
from typing import NamedTuple, Self

from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.keys import CourseKey, UsageKey


class BlockKey(NamedTuple):
Expand Down Expand Up @@ -40,6 +40,12 @@ def from_string(cls, s: str) -> Self:
raise ValueError(f"Invalid string format for BlockKey: {s}")
return cls(parts[0], parts[1])

def to_usage_key(self, course_key: CourseKey) -> UsageKey:
"""
Converts this BlockKey into a UsageKey.
"""
return course_key.make_usage_key(self.type, self.id)


def derive_key(source: UsageKey, dest_parent: BlockKey) -> BlockKey:
"""
Expand Down
Loading