diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py index 2283036faf24..87a40304faef 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py @@ -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." + ) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py index 7ab14028cd50..f392c47a67a1 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_index.py @@ -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 @@ -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, @@ -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, @@ -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): @@ -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) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py index 5dea51b91d71..22f0cd0d54d4 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py @@ -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, @@ -27,7 +28,7 @@ ) # lint-amnesty, pylint: disable=wrong-import-order -class BaseXBlockContainer(CourseTestCase): +class BaseXBlockContainer(CourseTestCase, ContentLibrariesRestApiTest): """ Base xBlock container handler. @@ -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"], + 'updated content upstream 1' + ) + # 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", @@ -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, @@ -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): @@ -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): """ @@ -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, }, diff --git a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py index c6f24496f241..cf3838ac3719 100644 --- a/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py +++ b/cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py @@ -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), }) diff --git a/cms/lib/xblock/upstream_sync.py b/cms/lib/xblock/upstream_sync.py index 5e812f7035f5..78d14d01c246 100644 --- a/cms/lib/xblock/upstream_sync.py +++ b/cms/lib/xblock/upstream_sync.py @@ -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: @@ -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