-
Notifications
You must be signed in to change notification settings - Fork 4.1k
feat: container links api #36911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: container links api #36911
Changes from all commits
2904d8a
7a656b1
cb348b1
a462864
1d97610
6e79cd4
4dd2fed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ | |
from cms.lib.xblock.upstream_sync import BadUpstream, UpstreamLink | ||
from cms.djangoapps.contentstore.tests.utils import CourseTestCase | ||
from cms.djangoapps.contentstore.xblock_storage_handlers import view_handlers as xblock_view_handlers | ||
from opaque_keys.edx.keys import UsageKey | ||
from opaque_keys.edx.keys import ContainerKey, UsageKey | ||
from common.djangoapps.student.tests.factories import UserFactory | ||
from xmodule.modulestore.django import modulestore | ||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase | ||
|
@@ -28,6 +28,9 @@ | |
URL_LIB_BLOCKS = URL_PREFIX + '{lib_key}/blocks/' | ||
URL_LIB_BLOCK_PUBLISH = URL_PREFIX + 'blocks/{block_key}/publish/' | ||
URL_LIB_BLOCK_OLX = URL_PREFIX + 'blocks/{block_key}/olx/' | ||
URL_LIB_CONTAINER = URL_PREFIX + 'containers/{container_key}/' # Get a container in this library | ||
URL_LIB_CONTAINERS = URL_PREFIX + '{lib_key}/containers/' # Create a new container in this library | ||
URL_LIB_CONTAINER_PUBLISH = URL_LIB_CONTAINER + 'publish/' # Publish changes to the specified container + children | ||
|
||
|
||
def _get_upstream_link_good_and_syncable(downstream): | ||
|
@@ -75,8 +78,14 @@ def setUp(self): | |
)["id"] | ||
self.html_lib_id = self._add_block_to_library(self.library_id, "html", "html-baz")["id"] | ||
self.video_lib_id = self._add_block_to_library(self.library_id, "video", "video-baz")["id"] | ||
self.unit_id = self._create_container(self.library_id, "unit", "unit-1", "Unit 1")["id"] | ||
self.subsection_id = self._create_container(self.library_id, "subsection", "subsection-1", "Subsection 1")["id"] | ||
self.section_id = self._create_container(self.library_id, "section", "section-1", "Section 1")["id"] | ||
self._publish_library_block(self.html_lib_id) | ||
self._publish_library_block(self.video_lib_id) | ||
self._publish_container(self.unit_id) | ||
self._publish_container(self.subsection_id) | ||
self._publish_container(self.section_id) | ||
self.mock_upstream_link = f"{settings.COURSE_AUTHORING_MICROFRONTEND_URL}/library/{self.library_id}/components?usageKey={self.video_lib_id}" # pylint: disable=line-too-long # noqa: E501 | ||
self.course = CourseFactory.create() | ||
chapter = BlockFactory.create(category='chapter', parent=self.course) | ||
|
@@ -89,6 +98,15 @@ def setUp(self): | |
self.downstream_html_key = BlockFactory.create( | ||
category='html', parent=unit, upstream=self.html_lib_id, upstream_version=1, | ||
).usage_key | ||
self.downstream_chapter_key = BlockFactory.create( | ||
category='chapter', parent=self.course, upstream=self.section_id, upstream_version=1, | ||
).usage_key | ||
self.downstream_sequential_key = BlockFactory.create( | ||
category='sequential', parent=chapter, upstream=self.subsection_id, upstream_version=1, | ||
).usage_key | ||
self.downstream_unit_key = BlockFactory.create( | ||
category='vertical', parent=sequential, upstream=self.unit_id, upstream_version=1, | ||
).usage_key | ||
|
||
self.another_course = CourseFactory.create(display_name="Another Course") | ||
another_chapter = BlockFactory.create(category="chapter", parent=self.another_course) | ||
|
@@ -108,6 +126,8 @@ def setUp(self): | |
|
||
self.fake_video_key = self.course.id.make_usage_key("video", "NoSuchVideo") | ||
self.learner = UserFactory(username="learner", password="password") | ||
self._update_container(self.unit_id, display_name="Unit 2") | ||
self._publish_container(self.unit_id) | ||
self._set_library_block_olx(self.html_lib_id, "<html><b>Hello world!</b></html>") | ||
self._publish_library_block(self.html_lib_id) | ||
self._publish_library_block(self.video_lib_id) | ||
|
@@ -117,7 +137,7 @@ def _api(self, method, url, data, expect_response): | |
""" | ||
Call a REST API | ||
""" | ||
response = getattr(self.client, method)(url, data, format="json") | ||
response = getattr(self.client, method)(url, data, format="json", content_type="application/json") | ||
assert response.status_code == expect_response,\ | ||
'Unexpected response code {}:\n{}'.format(response.status_code, getattr(response, 'data', '(no data)')) | ||
return response.data | ||
|
@@ -148,13 +168,29 @@ def _publish_library_block(self, block_key, expect_response=200): | |
""" Publish changes from a specified XBlock """ | ||
return self._api('post', URL_LIB_BLOCK_PUBLISH.format(block_key=block_key), None, expect_response) | ||
|
||
def _publish_container(self, container_key: ContainerKey | str, expect_response=200): | ||
""" Publish all changes in the specified container + children """ | ||
return self._api('post', URL_LIB_CONTAINER_PUBLISH.format(container_key=container_key), None, expect_response) | ||
|
||
def _update_container(self, container_key: ContainerKey | str, display_name: str, expect_response=200): | ||
""" Update a container (unit etc.) """ | ||
data = {"display_name": display_name} | ||
return self._api('patch', URL_LIB_CONTAINER.format(container_key=container_key), data, expect_response) | ||
|
||
def _set_library_block_olx(self, block_key, new_olx, expect_response=200): | ||
""" Overwrite the OLX of a specific block in the library """ | ||
return self._api('post', URL_LIB_BLOCK_OLX.format(block_key=block_key), {"olx": new_olx}, expect_response) | ||
|
||
def call_api(self, usage_key_string): | ||
raise NotImplementedError | ||
|
||
def _create_container(self, lib_key, container_type, slug: str | None, display_name: str, expect_response=200): | ||
""" Create a container (unit etc.) """ | ||
data = {"container_type": container_type, "display_name": display_name} | ||
if slug: | ||
data["slug"] = slug | ||
return self._api('post', URL_LIB_CONTAINERS.format(lib_key=lib_key), data, expect_response) | ||
|
||
|
||
class SharedErrorTestCases(_BaseDownstreamViewTestMixin): | ||
""" | ||
|
@@ -444,9 +480,9 @@ class GetUpstreamViewTest( | |
""" | ||
def call_api( | ||
self, | ||
course_id: str = None, | ||
ready_to_sync: bool = None, | ||
upstream_usage_key: str = None, | ||
course_id: str | None = None, | ||
ready_to_sync: bool | None = None, | ||
upstream_usage_key: str | None = None, | ||
): | ||
data = {} | ||
if course_id is not None: | ||
|
@@ -562,3 +598,93 @@ def test_200_summary(self): | |
'last_published_at': self.now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), | ||
}] | ||
self.assertListEqual(data, expected) | ||
|
||
|
||
class GetContainerUpstreamViewTest( | ||
_BaseDownstreamViewTestMixin, | ||
SharedModuleStoreTestCase, | ||
): | ||
""" | ||
Test that `GET /api/v2/contentstore/downstream-containers?...` returns list of links based on the provided filter. | ||
""" | ||
def call_api( | ||
self, | ||
course_id: str | None = None, | ||
ready_to_sync: bool | None = None, | ||
upstream_container_key: str | None = None, | ||
): | ||
data = {} | ||
if course_id is not None: | ||
data["course_id"] = str(course_id) | ||
if ready_to_sync is not None: | ||
data["ready_to_sync"] = str(ready_to_sync) | ||
if upstream_container_key is not None: | ||
data["upstream_container_key"] = str(upstream_container_key) | ||
return self.client.get("/api/contentstore/v2/downstream-containers/", data=data) | ||
|
||
def test_200_all_container_downstreams_for_a_course(self): | ||
""" | ||
Returns all container links for given course | ||
""" | ||
self.client.login(username="superuser", password="password") | ||
response = self.call_api(course_id=self.course.id) | ||
assert response.status_code == 200 | ||
data = response.json() | ||
date_format = self.now.isoformat().split("+")[0] + 'Z' | ||
expected = [ | ||
{ | ||
'created': date_format, | ||
'downstream_context_key': str(self.course.id), | ||
'downstream_usage_key': str(self.downstream_chapter_key), | ||
'id': 1, | ||
'ready_to_sync': False, | ||
'updated': date_format, | ||
'upstream_context_key': self.library_id, | ||
'upstream_context_title': self.library_title, | ||
'upstream_container_key': self.section_id, | ||
'upstream_version': 1, | ||
'version_declined': None, | ||
'version_synced': 1, | ||
}, | ||
{ | ||
'created': date_format, | ||
'downstream_context_key': str(self.course.id), | ||
'downstream_usage_key': str(self.downstream_sequential_key), | ||
'id': 2, | ||
'ready_to_sync': False, | ||
'updated': date_format, | ||
'upstream_context_key': self.library_id, | ||
'upstream_context_title': self.library_title, | ||
'upstream_container_key': self.subsection_id, | ||
'upstream_version': 1, | ||
'version_declined': None, | ||
'version_synced': 1, | ||
}, | ||
{ | ||
'created': date_format, | ||
'downstream_context_key': str(self.course.id), | ||
'downstream_usage_key': str(self.downstream_unit_key), | ||
'id': 3, | ||
'ready_to_sync': True, | ||
'updated': date_format, | ||
'upstream_context_key': self.library_id, | ||
'upstream_context_title': self.library_title, | ||
'upstream_container_key': self.unit_id, | ||
'upstream_version': 2, | ||
'version_declined': None, | ||
'version_synced': 1 | ||
}, | ||
] | ||
self.assertListEqual(data["results"], expected) | ||
self.assertEqual(data["count"], 3) | ||
|
||
def test_200_all_downstreams_ready_to_sync(self): | ||
""" | ||
Returns all links that are syncable | ||
""" | ||
self.client.login(username="superuser", password="password") | ||
response = self.call_api(ready_to_sync=True) | ||
assert response.status_code == 200 | ||
data = response.json() | ||
self.assertTrue(all(o["ready_to_sync"] for o in data["results"])) | ||
self.assertEqual(data["count"], 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Unrelated to this line/file, but part of this issue) When I delete a Unit from a Subsection (or a Subsection from a Section), the container API correctly returns the updated child list, but the search index still shows the old child Unit/Subsection in the container Card preview. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @pomegranited Fixed it here: 1d97610 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops.. We should probably backport this one-line fix to Teak. Can you split it into a separate commit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pomegranited Good idea.