refactor: course container children api [FC-0097] (#37375)
* feat: course container children view * refactor: rename * refactor: include children info in upstream info of container children * fix: tests * fix: test * refactor: children check
This commit is contained in:
@@ -18,7 +18,7 @@ from .proctoring import (
|
||||
)
|
||||
from .settings import CourseSettingsSerializer
|
||||
from .textbooks import CourseTextbooksSerializer
|
||||
from .vertical_block import ContainerHandlerSerializer, VerticalContainerSerializer
|
||||
from .vertical_block import ContainerHandlerSerializer, ContainerChildrenSerializer
|
||||
from .videos import (
|
||||
CourseVideosSerializer,
|
||||
VideoDownloadSerializer,
|
||||
|
||||
@@ -105,6 +105,15 @@ class ContainerHandlerSerializer(serializers.Serializer):
|
||||
return None
|
||||
|
||||
|
||||
class UpstreamChildrenInfoSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer holding the information about the children of an xblock that is syncing.
|
||||
"""
|
||||
name = serializers.CharField()
|
||||
upstream = serializers.CharField(allow_null=True)
|
||||
id = serializers.CharField()
|
||||
|
||||
|
||||
class UpstreamLinkSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer holding info for syncing a block with its upstream (eg, a library block).
|
||||
@@ -115,9 +124,12 @@ class UpstreamLinkSerializer(serializers.Serializer):
|
||||
version_declined = serializers.IntegerField(allow_null=True)
|
||||
error_message = serializers.CharField(allow_null=True)
|
||||
ready_to_sync = serializers.BooleanField()
|
||||
is_modified = serializers.BooleanField()
|
||||
has_top_level_parent = serializers.BooleanField()
|
||||
ready_to_sync_children = UpstreamChildrenInfoSerializer(many=True, required=False)
|
||||
|
||||
|
||||
class ChildVerticalContainerSerializer(serializers.Serializer):
|
||||
class ContainerChildSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for representing a xblock child of vertical container.
|
||||
"""
|
||||
@@ -160,11 +172,11 @@ class ChildVerticalContainerSerializer(serializers.Serializer):
|
||||
return actions
|
||||
|
||||
|
||||
class VerticalContainerSerializer(serializers.Serializer):
|
||||
class ContainerChildrenSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for representing a vertical container with state and children.
|
||||
"""
|
||||
|
||||
children = ChildVerticalContainerSerializer(many=True)
|
||||
children = ContainerChildSerializer(many=True)
|
||||
is_published = serializers.BooleanField()
|
||||
can_paste_component = serializers.BooleanField()
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
""" Contenstore API v1 URLs. """
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import re_path, path
|
||||
from django.urls import path, re_path
|
||||
|
||||
from openedx.core.constants import COURSE_ID_PATTERN
|
||||
|
||||
from .views import (
|
||||
ContainerChildrenView,
|
||||
ContainerHandlerView,
|
||||
CourseCertificatesView,
|
||||
CourseDetailsView,
|
||||
CourseTeamView,
|
||||
CourseTextbooksView,
|
||||
CourseIndexView,
|
||||
CourseGradingView,
|
||||
CourseGroupConfigurationsView,
|
||||
CourseIndexView,
|
||||
CourseRerunView,
|
||||
CourseSettingsView,
|
||||
CourseTeamView,
|
||||
CourseTextbooksView,
|
||||
CourseVideosView,
|
||||
CourseWaffleFlagsView,
|
||||
HomePageView,
|
||||
HelpUrlsView,
|
||||
HomePageCoursesView,
|
||||
HomePageLibrariesView,
|
||||
HomePageView,
|
||||
ProctoredExamSettingsView,
|
||||
ProctoringErrorsView,
|
||||
HelpUrlsView,
|
||||
VideoUsageView,
|
||||
VideoDownloadView,
|
||||
VerticalContainerView,
|
||||
VideoUsageView,
|
||||
vertical_container_children_redirect_view,
|
||||
)
|
||||
|
||||
app_name = 'v1'
|
||||
@@ -127,11 +128,17 @@ urlpatterns = [
|
||||
ContainerHandlerView.as_view(),
|
||||
name="container_handler"
|
||||
),
|
||||
# Deprecated url, please use `container_children` url below
|
||||
re_path(
|
||||
fr'^container/vertical/{settings.USAGE_KEY_PATTERN}/children$',
|
||||
VerticalContainerView.as_view(),
|
||||
vertical_container_children_redirect_view,
|
||||
name="container_vertical"
|
||||
),
|
||||
re_path(
|
||||
fr'^container/{settings.USAGE_KEY_PATTERN}/children$',
|
||||
ContainerChildrenView.as_view(),
|
||||
name="container_children"
|
||||
),
|
||||
re_path(
|
||||
fr'^course_waffle_flags(?:/{COURSE_ID_PATTERN})?$',
|
||||
CourseWaffleFlagsView.as_view(),
|
||||
|
||||
@@ -3,10 +3,10 @@ Views for v1 contentstore API.
|
||||
"""
|
||||
from .certificates import CourseCertificatesView
|
||||
from .course_details import CourseDetailsView
|
||||
from .course_index import CourseIndexView
|
||||
from .course_index import ContainerChildrenView, CourseIndexView
|
||||
from .course_rerun import CourseRerunView
|
||||
from .course_waffle_flags import CourseWaffleFlagsView
|
||||
from .course_team import CourseTeamView
|
||||
from .course_waffle_flags import CourseWaffleFlagsView
|
||||
from .grading import CourseGradingView
|
||||
from .group_configurations import CourseGroupConfigurationsView
|
||||
from .help_urls import HelpUrlsView
|
||||
@@ -14,7 +14,7 @@ from .home import HomePageCoursesView, HomePageLibrariesView, HomePageView
|
||||
from .proctoring import ProctoredExamSettingsView, ProctoringErrorsView
|
||||
from .settings import CourseSettingsView
|
||||
from .textbooks import CourseTextbooksView
|
||||
from .vertical_block import ContainerHandlerView, VerticalContainerView
|
||||
from .vertical_block import ContainerHandlerView, vertical_container_children_redirect_view
|
||||
from .videos import (
|
||||
CourseVideosView,
|
||||
VideoDownloadView,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""API Views for course index"""
|
||||
|
||||
import logging
|
||||
|
||||
import edx_api_doc_tools as apidocs
|
||||
from django.conf import settings
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -8,10 +10,24 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from cms.djangoapps.contentstore.config.waffle import CUSTOM_RELATIVE_DATES
|
||||
from cms.djangoapps.contentstore.rest_api.v1.serializers import CourseIndexSerializer
|
||||
from cms.djangoapps.contentstore.utils import get_course_index_context
|
||||
from cms.djangoapps.contentstore.rest_api.v1.mixins import ContainerHandlerMixin
|
||||
from cms.djangoapps.contentstore.rest_api.v1.serializers import (
|
||||
CourseIndexSerializer,
|
||||
ContainerChildrenSerializer,
|
||||
)
|
||||
from cms.djangoapps.contentstore.utils import (
|
||||
get_course_index_context,
|
||||
get_user_partition_info,
|
||||
get_visibility_partition_info,
|
||||
get_xblock_render_error,
|
||||
get_xblock_validation_messages,
|
||||
)
|
||||
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import get_xblock
|
||||
from cms.lib.xblock.upstream_sync import UpstreamLink
|
||||
from common.djangoapps.student.auth import has_studio_read_access
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
@@ -98,3 +114,169 @@ class CourseIndexView(DeveloperErrorViewMixin, APIView):
|
||||
|
||||
serializer = CourseIndexSerializer(course_index_context)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
class ContainerChildrenView(APIView, ContainerHandlerMixin):
|
||||
"""
|
||||
View for container xblock requests to get state and children data.
|
||||
"""
|
||||
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.string_parameter(
|
||||
"usage_key_string",
|
||||
apidocs.ParameterLocation.PATH,
|
||||
description="Container usage key",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: ContainerChildrenSerializer,
|
||||
401: "The requester is not authenticated.",
|
||||
404: "The requested locator does not exist.",
|
||||
},
|
||||
)
|
||||
def get(self, request: Request, usage_key_string: str):
|
||||
"""
|
||||
Get an object containing vertical state with children data.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/contentstore/v1/container/{usage_key_string}/children
|
||||
|
||||
**Response Values**
|
||||
|
||||
If the request is successful, an HTTP 200 "OK" response is returned.
|
||||
|
||||
The HTTP 200 response contains a single dict that contains keys that
|
||||
are the vertical's container children data.
|
||||
|
||||
**Example Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"name": "Drag and Drop",
|
||||
"block_id": "block-v1:org+101+101+type@drag-and-drop-v2+block@7599275ace6b46f5a482078a2954ca16",
|
||||
"block_type": "drag-and-drop-v2",
|
||||
"user_partition_info": {},
|
||||
"user_partitions": {}
|
||||
"upstream_link": null,
|
||||
"actions": {
|
||||
"can_copy": true,
|
||||
"can_duplicate": true,
|
||||
"can_move": true,
|
||||
"can_manage_access": true,
|
||||
"can_delete": true,
|
||||
"can_manage_tags": true,
|
||||
},
|
||||
"has_validation_error": false,
|
||||
"validation_errors": [],
|
||||
},
|
||||
{
|
||||
"name": "Video",
|
||||
"block_id": "block-v1:org+101+101+type@video+block@0e3d39b12d7c4345981bda6b3511a9bf",
|
||||
"block_type": "video",
|
||||
"user_partition_info": {},
|
||||
"user_partitions": {}
|
||||
"upstream_link": {
|
||||
"upstream_ref": "lb:org:mylib:video:404",
|
||||
"version_synced": 16
|
||||
"version_available": null,
|
||||
"error_message": "Linked library item not found: lb:org:mylib:video:404",
|
||||
"ready_to_sync": false,
|
||||
},
|
||||
"actions": {
|
||||
"can_copy": true,
|
||||
"can_duplicate": true,
|
||||
"can_move": true,
|
||||
"can_manage_access": true,
|
||||
"can_delete": true,
|
||||
"can_manage_tags": true,
|
||||
}
|
||||
"validation_messages": [],
|
||||
"render_error": "",
|
||||
},
|
||||
{
|
||||
"name": "Text",
|
||||
"block_id": "block-v1:org+101+101+type@html+block@3e3fa1f88adb4a108cd14e9002143690",
|
||||
"block_type": "html",
|
||||
"user_partition_info": {},
|
||||
"user_partitions": {},
|
||||
"upstream_link": {
|
||||
"upstream_ref": "lb:org:mylib:html:abcd",
|
||||
"version_synced": 43,
|
||||
"version_available": 49,
|
||||
"error_message": null,
|
||||
"ready_to_sync": true,
|
||||
},
|
||||
"actions": {
|
||||
"can_copy": true,
|
||||
"can_duplicate": true,
|
||||
"can_move": true,
|
||||
"can_manage_access": true,
|
||||
"can_delete": true,
|
||||
"can_manage_tags": true,
|
||||
},
|
||||
"validation_messages": [
|
||||
{
|
||||
"text": "This component's access settings contradict its parent's access settings.",
|
||||
"type": "error"
|
||||
}
|
||||
],
|
||||
"render_error": "Unterminated control keyword: 'if' in file '../problem.html'",
|
||||
},
|
||||
],
|
||||
"is_published": false,
|
||||
"can_paste_component": true,
|
||||
}
|
||||
```
|
||||
"""
|
||||
usage_key = self.get_object(usage_key_string)
|
||||
current_xblock = get_xblock(usage_key, request.user)
|
||||
is_course = current_xblock.scope_ids.usage_id.context_key.is_course
|
||||
|
||||
with modulestore().bulk_operations(usage_key.course_key):
|
||||
# load course once to reuse it for user_partitions query
|
||||
course = modulestore().get_course(current_xblock.location.course_key)
|
||||
children = []
|
||||
if current_xblock.has_children:
|
||||
for child in current_xblock.children:
|
||||
child_info = modulestore().get_item(child)
|
||||
user_partition_info = get_visibility_partition_info(child_info, course=course)
|
||||
user_partitions = get_user_partition_info(child_info, course=course)
|
||||
upstream_link = UpstreamLink.try_get_for_block(child_info, log_error=False)
|
||||
validation_messages = get_xblock_validation_messages(child_info)
|
||||
render_error = get_xblock_render_error(request, child_info)
|
||||
|
||||
children.append({
|
||||
"xblock": child_info,
|
||||
"name": child_info.display_name_with_default,
|
||||
"block_id": child_info.location,
|
||||
"block_type": child_info.location.block_type,
|
||||
"user_partition_info": user_partition_info,
|
||||
"user_partitions": user_partitions,
|
||||
"upstream_link": (
|
||||
# If the block isn't linked to an upstream (which is by far the most common case) then just
|
||||
# make this field null, which communicates the same info, but with less noise.
|
||||
upstream_link.to_json(include_child_info=True) if upstream_link.upstream_ref
|
||||
else None
|
||||
),
|
||||
"validation_messages": validation_messages,
|
||||
"render_error": render_error,
|
||||
})
|
||||
|
||||
is_published = False
|
||||
try:
|
||||
is_published = not modulestore().has_changes(current_xblock)
|
||||
except ItemNotFoundError:
|
||||
logging.error('Could not find any changes for block [%s]', usage_key)
|
||||
|
||||
container_data = {
|
||||
"children": children,
|
||||
"is_published": is_published,
|
||||
"can_paste_component": is_course,
|
||||
}
|
||||
serializer = ContainerChildrenSerializer(container_data)
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -195,7 +195,7 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
|
||||
Unit tests for the ContainerVerticalViewTest.
|
||||
"""
|
||||
|
||||
view_name = "container_vertical"
|
||||
view_name = "container_children"
|
||||
|
||||
def test_success_response(self):
|
||||
"""
|
||||
@@ -279,6 +279,8 @@ class ContainerVerticalViewTest(BaseXBlockContainer):
|
||||
"version_declined": None,
|
||||
"error_message": "Linked upstream library block was not found in the system",
|
||||
"ready_to_sync": False,
|
||||
"has_top_level_parent": False,
|
||||
"is_modified": False,
|
||||
},
|
||||
"user_partition_info": expected_user_partition_info,
|
||||
"user_partitions": expected_user_partitions,
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
""" API Views for unit page """
|
||||
|
||||
import logging
|
||||
|
||||
import edx_api_doc_tools as apidocs
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.http import HttpResponseBadRequest, HttpResponsePermanentRedirect
|
||||
from django.urls import reverse
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from cms.djangoapps.contentstore.utils import (
|
||||
get_container_handler_context,
|
||||
get_user_partition_info,
|
||||
get_visibility_partition_info,
|
||||
get_xblock_validation_messages,
|
||||
get_xblock_render_error,
|
||||
)
|
||||
from cms.djangoapps.contentstore.views.component import _get_item_in_course
|
||||
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import get_xblock
|
||||
from cms.djangoapps.contentstore.rest_api.v1.mixins import ContainerHandlerMixin
|
||||
from cms.djangoapps.contentstore.rest_api.v1.serializers import (
|
||||
ContainerHandlerSerializer,
|
||||
VerticalContainerSerializer,
|
||||
)
|
||||
from cms.lib.xblock.upstream_sync import UpstreamLink
|
||||
from cms.djangoapps.contentstore.utils import (
|
||||
get_container_handler_context,
|
||||
)
|
||||
from cms.djangoapps.contentstore.views.component import _get_item_in_course
|
||||
from openedx.core.lib.api.view_utils import view_auth_classes
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from cms.djangoapps.contentstore.rest_api.v1.mixins import ContainerHandlerMixin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -154,167 +147,16 @@ class ContainerHandlerView(APIView, ContainerHandlerMixin):
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
@view_auth_classes(is_authenticated=True)
|
||||
class VerticalContainerView(APIView, ContainerHandlerMixin):
|
||||
"""
|
||||
View for container xblock requests to get vertical state and children data.
|
||||
def vertical_container_children_redirect_view(request: Request, usage_key_string: str):
|
||||
"""
|
||||
Redirects GET requests to container_children url.
|
||||
|
||||
@apidocs.schema(
|
||||
parameters=[
|
||||
apidocs.string_parameter(
|
||||
"usage_key_string",
|
||||
apidocs.ParameterLocation.PATH,
|
||||
description="Vertical usage key",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: VerticalContainerSerializer,
|
||||
401: "The requester is not authenticated.",
|
||||
404: "The requested locator does not exist.",
|
||||
},
|
||||
This view will return a 301 Moved Permanently response to the provided
|
||||
usage_key string and automatically redirect the browser to the correct URL.
|
||||
"""
|
||||
redirect_location = reverse(
|
||||
'cms.djangoapps.contentstore:v1:container_children',
|
||||
kwargs={'usage_key_string': usage_key_string},
|
||||
)
|
||||
def get(self, request: Request, usage_key_string: str):
|
||||
"""
|
||||
Get an object containing vertical state with children data.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/contentstore/v1/container/vertical/{usage_key_string}/children
|
||||
|
||||
**Response Values**
|
||||
|
||||
If the request is successful, an HTTP 200 "OK" response is returned.
|
||||
|
||||
The HTTP 200 response contains a single dict that contains keys that
|
||||
are the vertical's container children data.
|
||||
|
||||
**Example Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"name": "Drag and Drop",
|
||||
"block_id": "block-v1:org+101+101+type@drag-and-drop-v2+block@7599275ace6b46f5a482078a2954ca16",
|
||||
"block_type": "drag-and-drop-v2",
|
||||
"user_partition_info": {},
|
||||
"user_partitions": {}
|
||||
"upstream_link": null,
|
||||
"actions": {
|
||||
"can_copy": true,
|
||||
"can_duplicate": true,
|
||||
"can_move": true,
|
||||
"can_manage_access": true,
|
||||
"can_delete": true,
|
||||
"can_manage_tags": true,
|
||||
},
|
||||
"has_validation_error": false,
|
||||
"validation_errors": [],
|
||||
},
|
||||
{
|
||||
"name": "Video",
|
||||
"block_id": "block-v1:org+101+101+type@video+block@0e3d39b12d7c4345981bda6b3511a9bf",
|
||||
"block_type": "video",
|
||||
"user_partition_info": {},
|
||||
"user_partitions": {}
|
||||
"upstream_link": {
|
||||
"upstream_ref": "lb:org:mylib:video:404",
|
||||
"version_synced": 16
|
||||
"version_available": null,
|
||||
"error_message": "Linked library item not found: lb:org:mylib:video:404",
|
||||
"ready_to_sync": false,
|
||||
},
|
||||
"actions": {
|
||||
"can_copy": true,
|
||||
"can_duplicate": true,
|
||||
"can_move": true,
|
||||
"can_manage_access": true,
|
||||
"can_delete": true,
|
||||
"can_manage_tags": true,
|
||||
}
|
||||
"validation_messages": [],
|
||||
"render_error": "",
|
||||
},
|
||||
{
|
||||
"name": "Text",
|
||||
"block_id": "block-v1:org+101+101+type@html+block@3e3fa1f88adb4a108cd14e9002143690",
|
||||
"block_type": "html",
|
||||
"user_partition_info": {},
|
||||
"user_partitions": {},
|
||||
"upstream_link": {
|
||||
"upstream_ref": "lb:org:mylib:html:abcd",
|
||||
"version_synced": 43,
|
||||
"version_available": 49,
|
||||
"error_message": null,
|
||||
"ready_to_sync": true,
|
||||
},
|
||||
"actions": {
|
||||
"can_copy": true,
|
||||
"can_duplicate": true,
|
||||
"can_move": true,
|
||||
"can_manage_access": true,
|
||||
"can_delete": true,
|
||||
"can_manage_tags": true,
|
||||
},
|
||||
"validation_messages": [
|
||||
{
|
||||
"text": "This component's access settings contradict its parent's access settings.",
|
||||
"type": "error"
|
||||
}
|
||||
],
|
||||
"render_error": "Unterminated control keyword: 'if' in file '../problem.html'",
|
||||
},
|
||||
],
|
||||
"is_published": false,
|
||||
"can_paste_component": true,
|
||||
}
|
||||
```
|
||||
"""
|
||||
usage_key = self.get_object(usage_key_string)
|
||||
current_xblock = get_xblock(usage_key, request.user)
|
||||
is_course = current_xblock.scope_ids.usage_id.context_key.is_course
|
||||
|
||||
with modulestore().bulk_operations(usage_key.course_key):
|
||||
# load course once to reuse it for user_partitions query
|
||||
course = modulestore().get_course(current_xblock.location.course_key)
|
||||
children = []
|
||||
if hasattr(current_xblock, "children"):
|
||||
for child in current_xblock.children:
|
||||
child_info = modulestore().get_item(child)
|
||||
user_partition_info = get_visibility_partition_info(child_info, course=course)
|
||||
user_partitions = get_user_partition_info(child_info, course=course)
|
||||
upstream_link = UpstreamLink.try_get_for_block(child_info, log_error=False)
|
||||
validation_messages = get_xblock_validation_messages(child_info)
|
||||
render_error = get_xblock_render_error(request, child_info)
|
||||
|
||||
children.append({
|
||||
"xblock": child_info,
|
||||
"name": child_info.display_name_with_default,
|
||||
"block_id": child_info.location,
|
||||
"block_type": child_info.location.block_type,
|
||||
"user_partition_info": user_partition_info,
|
||||
"user_partitions": user_partitions,
|
||||
"upstream_link": (
|
||||
# If the block isn't linked to an upstream (which is by far the most common case) then just
|
||||
# make this field null, which communicates the same info, but with less noise.
|
||||
upstream_link.to_json() if upstream_link.upstream_ref
|
||||
else None
|
||||
),
|
||||
"validation_messages": validation_messages,
|
||||
"render_error": render_error,
|
||||
})
|
||||
|
||||
is_published = False
|
||||
try:
|
||||
is_published = not modulestore().has_changes(current_xblock)
|
||||
except ItemNotFoundError:
|
||||
logging.error('Could not find any changes for block [%s]', usage_key)
|
||||
|
||||
container_data = {
|
||||
"children": children,
|
||||
"is_published": is_published,
|
||||
"can_paste_component": is_course,
|
||||
}
|
||||
serializer = VerticalContainerSerializer(container_data)
|
||||
return Response(serializer.data)
|
||||
http_response = HttpResponsePermanentRedirect(redirect_location)
|
||||
return http_response
|
||||
|
||||
@@ -183,7 +183,6 @@ class UpstreamLink:
|
||||
}
|
||||
if (
|
||||
include_child_info
|
||||
and self.ready_to_sync
|
||||
and isinstance(self.upstream_key, LibraryContainerLocator)
|
||||
and self.downstream_key is not None
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user