From a2f7643005ca93ea2631c6426d7b75171cad484b Mon Sep 17 00:00:00 2001 From: Taimoor Ahmed Date: Mon, 20 Jul 2026 00:28:27 +0500 Subject: [PATCH] feat: accept library_content_key on the v1 Xblock create payload (FC-0118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v1 `XblockViewSet` validates every request body against the strict `XblockSerializer`, which returns 400 on any unlisted top-level field. Studio's real create payload includes `library_content_key` when a course block is created by importing a component from a v2 library — `create_xblock_response` reads it to set the block's upstream reference — so the strict serializer must declare the field or it rejects the legitimate request. This unblocks migrating the frontend-app-authoring block-creation caller to the standardized `/api/contentstore/v1/xblock/` endpoint (ADR 0028). Adds a regression test asserting a create body carrying `library_content_key` reaches the handler instead of being rejected with a 400. Refs openedx/frontend-app-authoring#3127 Co-Authored-By: Claude Opus 4.8 --- .../rest_api/v0/serializers/xblock.py | 5 +++++ .../v1/views/tests/test_xblock_viewset.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/cms/djangoapps/contentstore/rest_api/v0/serializers/xblock.py b/cms/djangoapps/contentstore/rest_api/v0/serializers/xblock.py index 48ef9f82d580..88dd92ad49fa 100644 --- a/cms/djangoapps/contentstore/rest_api/v0/serializers/xblock.py +++ b/cms/djangoapps/contentstore/rest_api/v0/serializers/xblock.py @@ -78,4 +78,9 @@ class XblockSerializer(StrictSerializer): target_index = serializers.IntegerField(required=False, allow_null=True) boilerplate = serializers.JSONField(required=False, allow_null=True) staged_content = serializers.CharField(required=False, allow_null=True) + # Present when a course block is created by importing a component from a v2 + # library. Consumed by the create handler (``create_xblock_response`` reads + # ``library_content_key`` to set the block's upstream reference). Declared + # here so the strict serializer does not 400 the real Studio create payload. + library_content_key = serializers.CharField(required=False, allow_null=True) hide_from_toc = serializers.BooleanField(required=False, allow_null=True) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_xblock_viewset.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_xblock_viewset.py index 638b0ce2eb35..a6262b7430cb 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_xblock_viewset.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_xblock_viewset.py @@ -59,6 +59,24 @@ def test_post_calls_create_xblock_response(self, mock_fn): mock_fn.assert_called_once() assert mock_fn.call_args[0][0].method == "POST" + @patch(f"{_VIEW_MODULE}.create_xblock_response", return_value=_MOCK_RESPONSE) + def test_create_accepts_library_content_key(self, mock_fn): + """ + Creating a course block by importing a component from a v2 library sends + ``library_content_key`` on the create body (consumed by + ``create_xblock_response`` to set the upstream reference). The strict + XblockSerializer must accept it so the request reaches the handler rather + than returning a 400. + """ + data = { + "parent_locator": PARENT_LOCATOR, + "category": "library_content", + "library_content_key": "lct:edX:testlib:unit:u1", + } + response = self.client.post(_list_url(), data=data, format="json") + assert response.status_code == status.HTTP_200_OK + mock_fn.assert_called_once() + @patch(f"{_VIEW_MODULE}.retrieve_xblock_response", return_value=_MOCK_RESPONSE) def test_get_calls_retrieve_xblock_response(self, mock_fn): response = self.client.get(_detail_url())