Skip to content
Open
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
11 changes: 10 additions & 1 deletion openedx/core/djangoapps/content_staging/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Serializers for the content libraries REST API
"""
from rest_framework import serializers
from xblock.plugin import PluginMissingError

from common.djangoapps.student.auth import has_studio_read_access
from xmodule.modulestore.django import modulestore
Expand Down Expand Up @@ -36,7 +37,15 @@ def get_block_type_display(self, obj):
""" Get the friendly name for this XBlock/component type """
from cms.djangoapps.contentstore.helpers import xblock_type_display_name

return xblock_type_display_name(obj.block_type)
try:
return xblock_type_display_name(obj.block_type)
except PluginMissingError:
# The clipboard holds a *copy* of a block, so the XBlock may no longer be installed (e.g. its
# plugin was removed from the platform after the content was copied). Fall back to the raw block
# type instead of raising, so that one stale clipboard cannot break unrelated requests (the
# clipboard is embedded in the course index response for every course). This mirrors
# openedx.core.djangoapps.xblock.api.xblock_type_display_name, which already does this.
return obj.block_type


class UserClipboardSerializer(serializers.Serializer):
Expand Down
29 changes: 29 additions & 0 deletions openedx/core/djangoapps/content_staging/tests/test_clipboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.authz.tests.mixins import CourseAuthoringAuthzTestMixin
from openedx.core.djangoapps.content_staging import api as python_api
from openedx.core.djangoapps.content_staging.models import StagedContent
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, upload_file_to_course
from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory, ToyCourseFactory
Expand Down Expand Up @@ -133,6 +134,34 @@ def test_copy_video_python_get(self) -> None:
assert olx_data is not None
self.assertXmlEqual(olx_data, SAMPLE_VIDEO_OLX)

def test_uninstalled_xblock_in_clipboard(self) -> None:
"""
A clipboard may hold a *copy* of a block whose XBlock plugin is no longer installed. Reading the
clipboard must still work (it is embedded in the course index response for every course), falling
back to the raw block type for the display name.
"""
course_key, client = self._setup_course()

# Copy the video to the clipboard:
video_key = course_key.make_usage_key("video", "sample_video")
response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(video_key)}, format="json")
assert response.status_code == 200
staged_content_id = response.json()["content"]["id"]
assert response.json()["content"]["block_type_display"] == "Video"

# Pretend the plugin for this block type was uninstalled after the content was copied:
StagedContent.objects.filter(pk=staged_content_id).update(block_type="an_uninstalled_xblock")

# The REST API should answer 200, using the raw block type as the display name:
response = client.get(CLIPBOARD_ENDPOINT)
assert response.status_code == 200
response_data = response.json()
assert response_data["content"]["block_type"] == "an_uninstalled_xblock"
assert response_data["content"]["block_type_display"] == "an_uninstalled_xblock"

# ...and so should the python API, which is what the course index response uses:
assert python_api.get_user_clipboard_json(self.user.id, response.wsgi_request) == response_data

def test_copy_html(self) -> None:
"""
Test copying an HTML XBlock from the course
Expand Down
Loading