Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,22 @@ class ContainerChildrenSerializer(serializers.Serializer):
Serializer for representing a vertical container with state and children.
"""

class UpstreamReadyToSyncChildrenInfoSerializer(serializers.Serializer):
"""
Serializer used for the `upstream_ready_to_sync_children_info` field
"""
id = serializers.CharField()
name = serializers.CharField()
upstream = serializers.CharField()
block_type = serializers.CharField()
is_modified = serializers.BooleanField()

children = ContainerChildSerializer(many=True)
is_published = serializers.BooleanField()
can_paste_component = serializers.BooleanField()
display_name = serializers.CharField()
upstream_ready_to_sync_children_info = UpstreamReadyToSyncChildrenInfoSerializer(
many=True,
required=False,
help_text="List of dictionaries describing upstream child components readiness to sync."
)
22 changes: 22 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/course_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.fields import BooleanField

from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
from cms.djangoapps.contentstore.rest_api.v1.mixins import ContainerHandlerMixin
Expand Down Expand Up @@ -129,6 +130,11 @@ class ContainerChildrenView(APIView, ContainerHandlerMixin):
apidocs.ParameterLocation.PATH,
description="Container usage key",
),
apidocs.string_parameter(
"get_upstream_info",
apidocs.ParameterLocation.QUERY,
description="Gets the info of all ready to sync children",
),
],
responses={
200: ContainerChildrenSerializer,
Expand Down Expand Up @@ -210,6 +216,7 @@ def get(self, request: Request, usage_key_string: str):
"version_available": 49,
"error_message": null,
"ready_to_sync": true,
"is_ready_to_sync_individually": true,
},
"actions": {
"can_copy": true,
Expand All @@ -231,11 +238,19 @@ def get(self, request: Request, usage_key_string: str):
"is_published": false,
"can_paste_component": true,
"display_name": "Vertical block 1"
"upstream_ready_to_sync_children_info": [{
"name": "Text",
"upstream": "lb:org:mylib:html:abcd",
'block_type': "html",
'is_modified': true,
'id': "block-v1:org+101+101+type@html+block@3e3fa1f88adb4a108cd14e9002143690",
}]
}
```
"""
usage_key = self.get_object(usage_key_string)
current_xblock = get_xblock(usage_key, request.user)
get_upstream_info = BooleanField().to_internal_value(request.GET.get("get_upstream_info", False))
is_course = current_xblock.scope_ids.usage_id.context_key.is_course

with modulestore().bulk_operations(usage_key.course_key):
Expand Down Expand Up @@ -274,10 +289,17 @@ def get(self, request: Request, usage_key_string: str):
except ItemNotFoundError:
logging.error('Could not find any changes for block [%s]', usage_key)

upstream_ready_to_sync_children_info = []
if current_xblock.upstream and get_upstream_info:
upstream_link = UpstreamLink.get_for_block(current_xblock)
upstream_link_data = upstream_link.to_json(include_child_info=True)
upstream_ready_to_sync_children_info = upstream_link_data["ready_to_sync_children"]

container_data = {
"children": children,
"is_published": is_published,
"can_paste_component": is_course,
"upstream_ready_to_sync_children_info": upstream_ready_to_sync_children_info,
"display_name": current_xblock.display_name_with_default,
}
serializer = ContainerChildrenSerializer(container_data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from openedx.core.djangoapps.content_tagging.toggles import DISABLE_TAGGING_FEATURE
from openedx.core.djangoapps.content_libraries.tests import ContentLibrariesRestApiTest
from xmodule.partitions.partitions import (
ENROLLMENT_TRACK_PARTITION_ID,
Group,
Expand All @@ -27,7 +28,7 @@
) # lint-amnesty, pylint: disable=wrong-import-order


class BaseXBlockContainer(CourseTestCase):
class BaseXBlockContainer(CourseTestCase, ContentLibrariesRestApiTest):
"""
Base xBlock container handler.

Expand All @@ -48,6 +49,20 @@ def setup_xblock(self):
This method creates XBlock objects representing a course structure with chapters,
sequentials, verticals and others.
"""
self.lib = self._create_library(
slug="containers",
title="Container Test Library",
description="Units and more",
)
self.unit = self._create_container(self.lib["id"], "unit", display_name="Unit", slug=None)
self.html_block = self._add_block_to_library(self.lib["id"], "html", "Html1", can_stand_alone=False)
self._set_library_block_olx(
self.html_block["id"],
'<html display_name="Html1">updated content upstream 1</html>'
)
# Set version of html to 2
self._publish_library_block(self.html_block["id"])

self.chapter = self.create_block(
parent=self.course.location,
category="chapter",
Expand All @@ -60,7 +75,13 @@ def setup_xblock(self):
display_name="Lesson 1",
)

self.vertical = self.create_block(self.sequential.location, "vertical", "Unit")
self.vertical = self.create_block(
self.sequential.location,
"vertical",
"Unit",
upstream=self.unit["id"],
upstream_version=1,
)

self.html_unit_first = self.create_block(
parent=self.vertical.location,
Expand All @@ -72,8 +93,8 @@ def setup_xblock(self):
parent=self.vertical.location,
category="html",
display_name="Html Content 2",
upstream="lb:FakeOrg:FakeLib:html:FakeBlock",
upstream_version=5,
upstream=self.html_block["id"],
upstream_version=1,
)

def create_block(self, parent, category, display_name, **kwargs):
Expand Down Expand Up @@ -209,6 +230,27 @@ def test_success_response(self):
self.assertFalse(data["is_published"])
self.assertTrue(data["can_paste_component"])
self.assertEqual(data["display_name"], "Unit")
self.assertEqual(data["upstream_ready_to_sync_children_info"], [])

def test_success_response_with_upstream_info(self):
"""
Check that endpoint returns valid response data using `get_upstream_info` query param
"""
url = self.get_reverse_url(self.vertical.location)
response = self.client.get(f"{url}?get_upstream_info=true")
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.json()
self.assertEqual(len(data["children"]), 2)
self.assertFalse(data["is_published"])
self.assertTrue(data["can_paste_component"])
self.assertEqual(data["display_name"], "Unit")
self.assertEqual(data["upstream_ready_to_sync_children_info"], [{
"id": str(self.html_unit_second.usage_key),
"upstream": self.html_block["id"],
"block_type": "html",
"is_modified": False,
"name": "Html Content 2",
}])

def test_xblock_is_published(self):
"""
Expand Down Expand Up @@ -275,12 +317,12 @@ def test_children_content(self):
"can_manage_tags": True,
},
"upstream_link": {
"upstream_ref": "lb:FakeOrg:FakeLib:html:FakeBlock",
"version_synced": 5,
"version_available": None,
"upstream_ref": self.html_block["id"],
"version_synced": 1,
"version_available": 2,
"version_declined": None,
"error_message": "Linked upstream library block was not found in the system",
"ready_to_sync": False,
"error_message": None,
"ready_to_sync": True,
"has_top_level_parent": False,
"is_modified": False,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,8 @@ def test_200_single_upstream_container(self):
self.assertDictEqual(data['ready_to_sync_children'][0], {
'name': html_block.display_name,
'upstream': str(self.html_lib_id_2),
'block_type': 'html',
'is_modified': False,
'id': str(html_block.usage_key),
})

Expand Down
3 changes: 3 additions & 0 deletions cms/lib/xblock/upstream_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def _check_children_ready_to_sync(self, xblock_downstream: XBlock, return_fast:
child_info.append({
'name': child.display_name,
'upstream': getattr(child, 'upstream', None),
'block_type': child.usage_key.block_type,
'is_modified': child_upstream_link.is_modified,
'id': str(child.usage_key),
})
if return_fast:
Expand Down Expand Up @@ -180,6 +182,7 @@ def to_json(self, include_child_info=False) -> dict[str, t.Any]:
**asdict(self),
"ready_to_sync": self.ready_to_sync,
"upstream_link": self.upstream_link,
"is_ready_to_sync_individually": self.is_ready_to_sync_individually,
}
if (
include_child_info
Expand Down
Loading