Skip to content
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: Upstream Sync with Content Library Blocks #34925

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions cms/djangoapps/contentstore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from attrs import frozen, Factory
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
from opaque_keys.edx.keys import AssetKey, CourseKey, UsageKey
from opaque_keys.edx.locator import DefinitionLocator, LocalId
Expand All @@ -22,6 +23,7 @@
from xmodule.xml_block import XmlMixin

from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.lib.xblock.upstream_sync import BadUpstream, sync_from_upstream
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
import openedx.core.djangoapps.content_staging.api as content_staging_api
import openedx.core.djangoapps.content_tagging.api as content_tagging_api
Expand All @@ -30,6 +32,10 @@

log = logging.getLogger(__name__)


User = get_user_model()


# Note: Grader types are used throughout the platform but most usages are simply in-line
# strings. In addition, new grader types can be defined on the fly anytime one is needed
# (because they're just strings). This dict is an attempt to constrain the sprawl in Studio.
Expand Down Expand Up @@ -250,7 +256,9 @@ class StaticFileNotices:
error_files: list[str] = Factory(list)


def import_staged_content_from_user_clipboard(parent_key: UsageKey, request) -> tuple[XBlock | None, StaticFileNotices]:
def import_staged_content_from_user_clipboard(
parent_key: UsageKey, request, *, link_to_upstream: bool = False
) -> tuple[XBlock | None, StaticFileNotices]:
"""
Import a block (along with its children and any required static assets) from
the "staged" OLX in the user's clipboard.
Expand Down Expand Up @@ -282,18 +290,18 @@ def import_staged_content_from_user_clipboard(parent_key: UsageKey, request) ->
node,
parent_xblock,
store,
user_id=request.user.id,
user=request.user,
slug_hint=user_clipboard.source_usage_key.block_id,
copied_from_block=str(user_clipboard.source_usage_key),
tags=user_clipboard.content.tags,
link_to_upstream=link_to_upstream,
)
# Now handle static files that need to go into Files & Uploads:
notices = _import_files_into_course(
course_key=parent_key.context_key,
staged_content_id=user_clipboard.content.id,
static_files=static_files,
)

return new_xblock, notices


Expand All @@ -302,14 +310,16 @@ def _import_xml_node_to_parent(
parent_xblock: XBlock,
# The modulestore we're using
store,
# The ID of the user who is performing this operation
user_id: int,
# The user who is performing this operation
user: User,
# Hint to use as usage ID (block_id) for the new XBlock
slug_hint: str | None = None,
# UsageKey of the XBlock that this one is a copy of
copied_from_block: str | None = None,
# Content tags applied to the source XBlock(s)
tags: dict[str, str] | None = None,
*,
link_to_upstream: bool = False,
) -> XBlock:
"""
Given an XML node representing a serialized XBlock (OLX), import it into modulestore 'store' as a child of the
Expand Down Expand Up @@ -375,10 +385,21 @@ def _import_xml_node_to_parent(
if copied_from_block:
# Store a reference to where this block was copied from, in the 'copied_from_block' field (AuthoringMixin)
temp_xblock.copied_from_block = copied_from_block
if copied_from_block and link_to_upstream:
# If requested, link this block as a downstream of where it was copied from
temp_xblock.upstream = copied_from_block
try:
sync_from_upstream(temp_xblock, user, apply_updates=False)
except BadUpstream as exc:
log.exception(
"Pasting content with link_to_upstream=True, but copied content is not a valid upstream. Will not link."
)
temp_xblock.upstream = None

# Save the XBlock into modulestore. We need to save the block and its parent for this to work:
new_xblock = store.update_item(temp_xblock, user_id, allow_not_found=True)
new_xblock = store.update_item(temp_xblock, user.id, allow_not_found=True)
parent_xblock.children.append(new_xblock.location)
store.update_item(parent_xblock, user_id)
store.update_item(parent_xblock, user.id)

children_handled = False
if hasattr(new_xblock, 'studio_post_paste'):
Expand All @@ -394,7 +415,7 @@ def _import_xml_node_to_parent(
child_node,
new_xblock,
store,
user_id=user_id,
user=user,
copied_from_block=str(child_copied_from),
tags=tags,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ def get_assets_url(self, obj):
return None


class UpstreamInfoSerializer(serializers.Serializer):
"""
Serializer holding info for syncing a block with its upstream (eg, a library block).
"""
upstream_ref = serializers.CharField()
current_version = serializers.IntegerField(allow_null=True)
latest_version = serializers.IntegerField(allow_null=True)
warning = serializers.CharField(allow_null=True)
can_sync = serializers.BooleanField()


class ChildVerticalContainerSerializer(serializers.Serializer):
"""
Serializer for representing a xblock child of vertical container.
Expand All @@ -113,6 +124,7 @@ class ChildVerticalContainerSerializer(serializers.Serializer):
block_type = serializers.CharField()
user_partition_info = serializers.DictField()
user_partitions = serializers.ListField()
upstream_info = UpstreamInfoSerializer(allow_null=True)
actions = serializers.SerializerMethodField()
validation_messages = MessageValidation(many=True)
render_error = serializers.CharField()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ def setup_xblock(self):
parent=self.vertical.location,
category="html",
display_name="Html Content 2",
upstream="lb:FakeOrg:FakeLib:html:FakeBlock",
upstream_version=5,
)

def create_block(self, parent, category, display_name, **kwargs):
Expand Down Expand Up @@ -193,6 +195,7 @@ def test_children_content(self):
"name": self.html_unit_first.display_name_with_default,
"block_id": str(self.html_unit_first.location),
"block_type": self.html_unit_first.location.block_type,
"upstream_info": None,
"user_partition_info": expected_user_partition_info,
"user_partitions": expected_user_partitions,
"actions": {
Expand All @@ -218,12 +221,20 @@ def test_children_content(self):
"can_delete": True,
"can_manage_tags": True,
},
"upstream_info": {
"upstream_ref": "lb:FakeOrg:FakeLib:html:FakeBlock",
"current_version": 5,
"latest_version": None,
"can_sync": False,
"warning": "Linked library item was not found in the system",
},
"user_partition_info": expected_user_partition_info,
"user_partitions": expected_user_partitions,
"validation_messages": [],
"render_error": "",
},
]
self.maxDiff = None
self.assertEqual(response.data["children"], expected_response)

def test_not_valid_usage_key_string(self):
Expand Down
17 changes: 17 additions & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ContainerHandlerSerializer,
VerticalContainerSerializer,
)
from cms.lib.xblock.upstream_sync import inspect_upstream_link_as_json
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
Expand Down Expand Up @@ -198,6 +199,7 @@ def get(self, request: Request, usage_key_string: str):
"block_type": "drag-and-drop-v2",
"user_partition_info": {},
"user_partitions": {}
"upstream_info": null,
"actions": {
"can_copy": true,
"can_duplicate": true,
Expand All @@ -215,6 +217,13 @@ def get(self, request: Request, usage_key_string: str):
"block_type": "video",
"user_partition_info": {},
"user_partitions": {}
"upstream_info": {
"upstream_ref": "lb:org:mylib:video:404",
"current_version": 16
"latest_version": null,
"warning": "Linked library item not found: lb:org:mylib:video:404",
"can_sync": false,
},
"actions": {
"can_copy": true,
"can_duplicate": true,
Expand All @@ -232,6 +241,13 @@ def get(self, request: Request, usage_key_string: str):
"block_type": "html",
"user_partition_info": {},
"user_partitions": {},
"upstream_info": {
"upstream_ref": "lb:org:mylib:html:abcd",
"current_version": 43,
"latest_version": 49,
"warning": null,
"can_sync": true,
},
"actions": {
"can_copy": true,
"can_duplicate": true,
Expand Down Expand Up @@ -277,6 +293,7 @@ def get(self, request: Request, usage_key_string: str):
"block_type": child_info.location.block_type,
"user_partition_info": user_partition_info,
"user_partitions": user_partitions,
"upstream_info": inspect_upstream_link_as_json(child_info),
"validation_messages": validation_messages,
"render_error": render_error,
})
Expand Down
13 changes: 11 additions & 2 deletions cms/djangoapps/contentstore/rest_api/v2/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
"""Contenstore API v2 URLs."""

from django.urls import path
from django.conf import settings
from django.urls import path, re_path

from cms.djangoapps.contentstore.rest_api.v2.views import HomePageCoursesViewV2
from cms.djangoapps.contentstore.rest_api.v2.views import (
HomePageCoursesViewV2,
UpstreamSyncView,
)

app_name = "v2"

Expand All @@ -12,4 +16,9 @@
HomePageCoursesViewV2.as_view(),
name="courses",
),
re_path(
fr'^upstream_sync/{settings.USAGE_KEY_PATTERN}$',
UpstreamSyncView.as_view(),
name="upstream_sync"
),
]
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v2/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Module for v2 views."""

from cms.djangoapps.contentstore.rest_api.v2.views.home import HomePageCoursesViewV2
from cms.djangoapps.contentstore.rest_api.v2.views.upstream_sync import UpstreamSyncView
Loading
Loading