feat: add API to return list of downstream blocks for an upstream [FC-0076] (#36253)
Adds the API for listing downstream contexts and parents for a given publishable entity.
This commit is contained in:
@@ -202,6 +202,15 @@ class PublishableEntityLink(models.Model):
|
||||
"upstream_block__learning_package"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_by_upstream_usage_key(cls, upstream_usage_key: UsageKey) -> QuerySet["PublishableEntityLink"]:
|
||||
"""
|
||||
Get all downstream context keys for given upstream usage key
|
||||
"""
|
||||
return cls.objects.filter(
|
||||
upstream_usage_key=upstream_usage_key,
|
||||
)
|
||||
|
||||
|
||||
class LearningContextLinksStatusChoices(models.TextChoices):
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Module for v2 serializers."""
|
||||
|
||||
from cms.djangoapps.contentstore.rest_api.v2.serializers.downstreams import PublishableEntityLinksSerializer
|
||||
from cms.djangoapps.contentstore.rest_api.v2.serializers.downstreams import (
|
||||
PublishableEntityLinksSerializer,
|
||||
PublishableEntityLinksUsageKeySerializer,
|
||||
)
|
||||
from cms.djangoapps.contentstore.rest_api.v2.serializers.home import CourseHomeTabSerializerV2
|
||||
|
||||
@@ -26,3 +26,15 @@ class PublishableEntityLinksSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PublishableEntityLink
|
||||
exclude = ['upstream_block', 'uuid']
|
||||
|
||||
|
||||
class PublishableEntityLinksUsageKeySerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for returning a string list of the usage keys.
|
||||
"""
|
||||
def to_representation(self, instance: PublishableEntityLink) -> str:
|
||||
return str(instance.downstream_usage_key)
|
||||
|
||||
class Meta:
|
||||
model = PublishableEntityLink
|
||||
fields = ('downstream_usage_key')
|
||||
|
||||
@@ -29,6 +29,11 @@ urlpatterns = [
|
||||
downstreams.UpstreamListView.as_view(),
|
||||
name='upstream-list'
|
||||
),
|
||||
re_path(
|
||||
f'^upstream/{settings.USAGE_KEY_PATTERN}/downstream-links$',
|
||||
downstreams.DownstreamContextListView.as_view(),
|
||||
name='downstream-link-list'
|
||||
),
|
||||
re_path(
|
||||
fr'^downstreams/{settings.USAGE_KEY_PATTERN}/sync$',
|
||||
downstreams.SyncFromUpstreamView.as_view(),
|
||||
|
||||
@@ -40,6 +40,11 @@ https://github.com/openedx/edx-platform/issues/35653):
|
||||
400: Downstream block is not linked to upstream content.
|
||||
404: Downstream block not found or user lacks permission to edit it.
|
||||
|
||||
/api/contentstore/v2/upstream/{usage_key_string}/downstream-links
|
||||
|
||||
GET: List all downstream blocks linked to a library block.
|
||||
200: A list of downstream usage_keys linked to the library block.
|
||||
|
||||
# NOT YET IMPLEMENTED -- Will be needed for full Libraries Relaunch in ~Teak.
|
||||
/api/contentstore/v2/downstreams
|
||||
/api/contentstore/v2/downstreams?course_id=course-v1:A+B+C&ready_to_sync=true
|
||||
@@ -71,7 +76,10 @@ from xblock.core import XBlock
|
||||
|
||||
from cms.djangoapps.contentstore.helpers import import_static_assets_for_library_sync
|
||||
from cms.djangoapps.contentstore.models import PublishableEntityLink
|
||||
from cms.djangoapps.contentstore.rest_api.v2.serializers import PublishableEntityLinksSerializer
|
||||
from cms.djangoapps.contentstore.rest_api.v2.serializers import (
|
||||
PublishableEntityLinksSerializer,
|
||||
PublishableEntityLinksUsageKeySerializer,
|
||||
)
|
||||
from cms.lib.xblock.upstream_sync import (
|
||||
BadDownstream,
|
||||
BadUpstream,
|
||||
@@ -138,6 +146,27 @@ class UpstreamListView(DeveloperErrorViewMixin, APIView):
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@view_auth_classes()
|
||||
class DownstreamContextListView(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
Serves library block->downstream usage keys
|
||||
"""
|
||||
def get(self, request: _AuthenticatedRequest, usage_key_string: str) -> Response:
|
||||
"""
|
||||
Fetches downstream links for given publishable entity
|
||||
"""
|
||||
try:
|
||||
usage_key = UsageKey.from_string(usage_key_string)
|
||||
except InvalidKeyError as exc:
|
||||
raise ValidationError(detail=f"Malformed usage key: {usage_key_string}") from exc
|
||||
|
||||
links = PublishableEntityLink.get_by_upstream_usage_key(upstream_usage_key=usage_key)
|
||||
|
||||
serializer = PublishableEntityLinksUsageKeySerializer(links, many=True)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
class DownstreamView(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
|
||||
@@ -66,6 +66,20 @@ class _BaseDownstreamViewTestMixin:
|
||||
self.downstream_html_key = BlockFactory.create(
|
||||
category='html', parent=unit, upstream=MOCK_HTML_UPSTREAM_REF, upstream_version=1,
|
||||
).usage_key
|
||||
|
||||
self.another_course = CourseFactory.create(display_name="Another Course")
|
||||
another_chapter = BlockFactory.create(category="chapter", parent=self.another_course)
|
||||
another_sequential = BlockFactory.create(category="sequential", parent=another_chapter)
|
||||
another_unit = BlockFactory.create(category="vertical", parent=another_sequential)
|
||||
self.another_video_keys = []
|
||||
for _ in range(3):
|
||||
# Adds 3 videos linked to the same upstream
|
||||
self.another_video_keys.append(
|
||||
BlockFactory.create(
|
||||
category="video", parent=another_unit, upstream=MOCK_UPSTREAM_REF, upstream_version=123,
|
||||
).usage_key
|
||||
)
|
||||
|
||||
self.fake_video_key = self.course.id.make_usage_key("video", "NoSuchVideo")
|
||||
self.superuser = UserFactory(username="superuser", password="password", is_staff=True, is_superuser=True)
|
||||
self.learner = UserFactory(username="learner", password="password")
|
||||
@@ -341,3 +355,23 @@ class GetUpstreamViewTest(_BaseDownstreamViewTestMixin, SharedModuleStoreTestCas
|
||||
},
|
||||
]
|
||||
self.assertListEqual(data, expected)
|
||||
|
||||
|
||||
class GetDownstreamContextsTest(_BaseDownstreamViewTestMixin, SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test that `GET /api/v2/contentstore/upstream/:usage_key/downstream-links returns list of
|
||||
linked blocks usage_keys in given upstream entity (i.e. library block).
|
||||
"""
|
||||
def call_api(self, usage_key_string):
|
||||
return self.client.get(f"/api/contentstore/v2/upstream/{usage_key_string}/downstream-links")
|
||||
|
||||
def test_200_downstream_context_list(self):
|
||||
"""
|
||||
Returns all downstream courses for given library block
|
||||
"""
|
||||
self.client.login(username="superuser", password="password")
|
||||
response = self.call_api(MOCK_UPSTREAM_REF)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
expected = [str(self.downstream_video_key)] + [str(key) for key in self.another_video_keys]
|
||||
self.assertListEqual(data, expected)
|
||||
|
||||
@@ -2371,7 +2371,7 @@ def get_xblock_render_error(request, xblock):
|
||||
return ""
|
||||
|
||||
|
||||
def create_or_update_xblock_upstream_link(xblock, course_key: str | CourseKey, created: datetime | None = None):
|
||||
def create_or_update_xblock_upstream_link(xblock, course_key: str | CourseKey, created: datetime | None = None) -> None:
|
||||
"""
|
||||
Create or update upstream->downstream link in database for given xblock.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user