Skip to content

Commit

Permalink
feat: XBlock Mixin and Service for UpstreamSync + link_to_upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
kdmccormick committed Sep 24, 2024
1 parent 690a399 commit 86c987c
Show file tree
Hide file tree
Showing 7 changed files with 550 additions and 9 deletions.
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
37 changes: 37 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 @@ -2,6 +2,7 @@

import logging
import edx_api_doc_tools as apidocs
from dataclasses import asdict
from django.http import HttpResponseBadRequest
from rest_framework.request import Request
from rest_framework.response import Response
Expand All @@ -20,6 +21,7 @@
ContainerHandlerSerializer,
VerticalContainerSerializer,
)
from cms.lib.xblock.upstream_sync import BadUpstream, inspect_upstream
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 +200,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 +218,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 +242,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 @@ -265,6 +282,25 @@ def get(self, request: Request, usage_key_string: str):
if hasattr(current_xblock, "children"):
for child in current_xblock.children:
child_info = modulestore().get_item(child)
try:
upstream_info = inspect_upstream(child_info)
except BadUpstream as exc:
upstream_info_json = {
"upstream_ref": child_info.upstream,
"current_version": None,
"latest_version": None,
"can_sync": False,
"warning": str(exc),
}
else:
upstream_info_json = {
**asdict(upstream_info),
"can_sync": (
upstream_info.upstream and
upstream_info.latest_version > upstream_info.current_version
),
"warning": None,
}
user_partition_info = get_visibility_partition_info(child_info, course=course)
user_partitions = get_user_partition_info(child_info, course=course)
validation_messages = get_xblock_validation_messages(child_info)
Expand All @@ -277,6 +313,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": upstream_info_json,
"validation_messages": validation_messages,
"render_error": render_error,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,9 @@ def _create_block(request):
# Paste from the user's clipboard (content_staging app clipboard, not browser clipboard) into 'usage_key':
try:
created_xblock, notices = import_staged_content_from_user_clipboard(
parent_key=usage_key, request=request
parent_key=usage_key,
request=request,
link_to_upstream=request.json.get("link_to_upstream"),
)
except Exception: # pylint: disable=broad-except
log.exception(
Expand Down
1 change: 1 addition & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@
XModuleMixin,
EditInfoMixin,
AuthoringMixin,
'cms.lib.xblock.upstream_sync.UpstreamSyncMixin',
)

# .. setting_name: XBLOCK_EXTRA_MIXINS
Expand Down
Loading

0 comments on commit 86c987c

Please sign in to comment.