feat: Multiple updates to handle children upstream info [FC-0097] (#37433)

* Which edX user roles will this change impact? "Developer"".
* Added `upstream_ready_to_sync_children_info` in `ContainerChildrenSerializer`
* Now, the `ContainerChildrenView` can return the `upstream_ready_to_sync_children_info`
* Update the child info in `UpstreamLink._check_children_ready_to_sync`
This commit is contained in:
Chris Chávez
2025-10-15 19:16:51 -05:00
committed by GitHub
parent da2daf255e
commit 7e1a17a707
5 changed files with 93 additions and 9 deletions

View File

@@ -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."
)

View File

@@ -8,6 +8,7 @@ from opaque_keys.edx.keys import CourseKey
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 @@ class ContainerChildrenView(APIView, ContainerHandlerMixin):
"version_available": 49,
"error_message": null,
"ready_to_sync": true,
"is_ready_to_sync_individually": true,
},
"actions": {
"can_copy": true,
@@ -231,11 +238,19 @@ class ContainerChildrenView(APIView, ContainerHandlerMixin):
"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 @@ class ContainerChildrenView(APIView, ContainerHandlerMixin):
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)

View File

@@ -11,6 +11,7 @@ from xblock.validation import ValidationMessage
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 @@ from xmodule.modulestore import (
) # lint-amnesty, pylint: disable=wrong-import-order
class BaseXBlockContainer(CourseTestCase):
class BaseXBlockContainer(CourseTestCase, ContentLibrariesRestApiTest):
"""
Base xBlock container handler.
@@ -48,6 +49,20 @@ class BaseXBlockContainer(CourseTestCase):
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",
@@ -60,7 +75,13 @@ class BaseXBlockContainer(CourseTestCase):
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 @@ class BaseXBlockContainer(CourseTestCase):
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 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
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 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
"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,
},

View File

@@ -643,6 +643,8 @@ class GetUpstreamViewTest(
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),
})

View File

@@ -121,6 +121,8 @@ class UpstreamLink:
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 @@ class UpstreamLink:
**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