Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 1 addition & 5 deletions cms/djangoapps/contentstore/tasks.py
Original file line number Diff line number Diff line change
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": 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
17 changes: 13 additions & 4 deletions cms/lib/xblock/upstream_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Python interface.
"""
from __future__ import annotations
from sympy.physics.units import pa

import logging
import typing as t
Expand Down Expand Up @@ -86,7 +87,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 +154,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 +223,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 +237,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 +311,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 +325,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
13 changes: 12 additions & 1 deletion xmodule/util/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

Consider moving these into opaque-keys if they generalize well.
"""
from opaque_keys.edx.locator import BlockUsageLocator
import hashlib
from typing import NamedTuple, Self

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


class BlockKey(NamedTuple):
Expand Down Expand Up @@ -40,6 +41,16 @@ 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 BlockUsageLocator(
course_key,
self.type,
self.id,
)


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