feat: paste tags when pasting xblocks with tag data (#34270)

This commit is contained in:
Rômulo Penido
2024-03-08 17:03:43 -03:00
committed by GitHub
parent 4a2e0f7df5
commit cb6801dbfd
17 changed files with 320 additions and 49 deletions

View File

@@ -18,7 +18,6 @@ from xblock.runtime import IdGenerator
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError
from xmodule.library_content_block import LibraryContentBlock
from xmodule.modulestore.django import modulestore
from xmodule.xml_block import XmlMixin
@@ -367,14 +366,18 @@ def _import_xml_node_to_parent(
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)
if isinstance(new_xblock, LibraryContentBlock):
# Special case handling for library content. If we need this for other blocks in the future, it can be made into
# an API, and we'd call new_block.studio_post_paste() instead of this code.
# In this case, we want to pull the children from the library and let library_tools assign their IDs.
new_xblock.sync_from_library(upgrade_to_latest=False)
else:
children_handled = False
if hasattr(new_xblock, 'studio_post_paste'):
# Allow an XBlock to do anything fancy it may need to when pasted from the clipboard.
# These blocks may handle their own children or parenting if needed. Let them return booleans to
# let us know if we need to handle these or not.
children_handed = new_xblock.studio_post_paste(store, node)
if not children_handled:
for child_node in child_nodes:
_import_xml_node_to_parent(child_node, new_xblock, store, user_id=user_id)
return new_xblock

View File

@@ -73,6 +73,7 @@ from common.djangoapps.xblock_django.models import (
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
from lms.djangoapps.lms_xblock.mixin import NONSENSICAL_ACCESS_RESTRICTION
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
from openedx.core.djangoapps.content_tagging import api as tagging_api
from ..component import component_handler, get_component_templates
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import (
@@ -1106,6 +1107,59 @@ class TestDuplicateItem(ItemTest, DuplicateHelper, OpenEdxEventsTestMixin):
assert dupe_html_2.display_name == "HTML 2 Title (Lib Update)"
assert dupe_html_2.data == "HTML 2 Content (Lib Update)"
def test_duplicate_tags(self):
"""
Test that duplicating a tagged XBlock also duplicates its content tags.
"""
source_course = CourseFactory()
user = UserFactory.create()
source_chapter = BlockFactory(
parent=source_course, category="chapter", display_name="Source Chapter"
)
source_block = BlockFactory(parent=source_chapter, category="html", display_name="Child")
# Create a couple of taxonomies with tags
taxonomyA = tagging_api.create_taxonomy(name="A", export_id="A")
taxonomyB = tagging_api.create_taxonomy(name="B", export_id="B")
tagging_api.set_taxonomy_orgs(taxonomyA, all_orgs=True)
tagging_api.set_taxonomy_orgs(taxonomyB, all_orgs=True)
tagging_api.add_tag_to_taxonomy(taxonomyA, "one")
tagging_api.add_tag_to_taxonomy(taxonomyA, "two")
tagging_api.add_tag_to_taxonomy(taxonomyB, "three")
tagging_api.add_tag_to_taxonomy(taxonomyB, "four")
# Tag the chapter
tagging_api.tag_object(str(source_chapter.location), taxonomyA, ["one", "two"])
tagging_api.tag_object(str(source_chapter.location), taxonomyB, ["three", "four"])
# Tag the child block
tagging_api.tag_object(str(source_block.location), taxonomyA, ["two"],)
# Duplicate the chapter (and its children)
dupe_location = duplicate_block(
parent_usage_key=source_course.location,
duplicate_source_usage_key=source_chapter.location,
user=user,
)
dupe_chapter = self.store.get_item(dupe_location)
self.assertEqual(len(dupe_chapter.get_children()), 1)
dupe_block = dupe_chapter.get_children()[0]
# Check that the duplicated blocks also duplicated tags
expected_chapter_tags = [
f'<ObjectTag> {str(dupe_chapter.location)}: A=one',
f'<ObjectTag> {str(dupe_chapter.location)}: A=two',
f'<ObjectTag> {str(dupe_chapter.location)}: B=four',
f'<ObjectTag> {str(dupe_chapter.location)}: B=three',
]
dupe_chapter_tags = [str(object_tag) for object_tag in tagging_api.get_object_tags(str(dupe_chapter.location))]
assert dupe_chapter_tags == expected_chapter_tags
expected_block_tags = [
f'<ObjectTag> {str(dupe_block.location)}: A=two',
]
dupe_block_tags = [str(object_tag) for object_tag in tagging_api.get_object_tags(str(dupe_block.location))]
assert dupe_block_tags == expected_block_tags
@ddt.ddt
class TestMoveItem(ItemTest):

View File

@@ -6,6 +6,7 @@ APIs.
import ddt
from opaque_keys.edx.keys import UsageKey
from rest_framework.test import APIClient
from openedx_tagging.core.tagging.models import Tag
from organizations.models import Organization
from xmodule.modulestore.django import contentstore, modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, upload_file_to_course
@@ -13,6 +14,7 @@ from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory, Toy
from cms.djangoapps.contentstore.utils import reverse_usage_url
from openedx.core.djangoapps.content_libraries import api as library_api
from openedx.core.djangoapps.content_tagging import api as tagging_api
CLIPBOARD_ENDPOINT = "/api/content-staging/v1/clipboard/"
XBLOCK_ENDPOINT = "/xblock/"
@@ -141,6 +143,69 @@ class ClipboardPasteTestCase(ModuleStoreTestCase):
# The new block should store a reference to where it was copied from
assert dest_block.copied_from_block == str(source_block.location)
def test_copy_and_paste_unit_with_tags(self):
"""
Test copying a unit (vertical) with tags from one course into another
"""
course_key, client = self._setup_course()
dest_course = CourseFactory.create(display_name='Destination Course')
with self.store.bulk_operations(dest_course.id):
dest_chapter = BlockFactory.create(parent=dest_course, category='chapter', display_name='Section')
dest_sequential = BlockFactory.create(parent=dest_chapter, category='sequential', display_name='Subsection')
unit_key = course_key.make_usage_key("vertical", "vertical_test")
# Add tags to the unit
taxonomy_all_org = tagging_api.create_taxonomy("test_taxonomy", "Test Taxonomy")
tagging_api.set_taxonomy_orgs(taxonomy_all_org, all_orgs=True)
Tag.objects.create(taxonomy=taxonomy_all_org, value="tag_1")
Tag.objects.create(taxonomy=taxonomy_all_org, value="tag_2")
tagging_api.tag_object(
object_id=str(unit_key),
taxonomy=taxonomy_all_org,
tags=["tag_1", "tag_2"],
)
taxonomy_all_org_removed = tagging_api.create_taxonomy("test_taxonomy_removed", "Test Taxonomy Removed")
tagging_api.set_taxonomy_orgs(taxonomy_all_org_removed, all_orgs=True)
Tag.objects.create(taxonomy=taxonomy_all_org_removed, value="tag_1")
Tag.objects.create(taxonomy=taxonomy_all_org_removed, value="tag_2")
tagging_api.tag_object(
object_id=str(unit_key),
taxonomy=taxonomy_all_org_removed,
tags=["tag_1", "tag_2"],
)
tagging_api.get_object_tags(str(unit_key))
taxonomy_no_org = tagging_api.create_taxonomy("test_taxonomy_no_org", "Test Taxonomy No Org")
Tag.objects.create(taxonomy=taxonomy_no_org, value="tag_1")
Tag.objects.create(taxonomy=taxonomy_no_org, value="tag_2")
tagging_api.tag_object(
object_id=str(unit_key),
taxonomy=taxonomy_no_org,
tags=["tag_1", "tag_2"],
)
# Copy the unit
copy_response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(unit_key)}, format="json")
assert copy_response.status_code == 200
taxonomy_all_org_removed.delete()
# Paste the unit
paste_response = client.post(XBLOCK_ENDPOINT, {
"parent_locator": str(dest_sequential.location),
"staged_content": "clipboard",
}, format="json")
assert paste_response.status_code == 200
dest_unit_key = UsageKey.from_string(paste_response.json()["locator"])
# Only tags from the taxonomy that is associated with the dest org should be copied
tags = list(tagging_api.get_object_tags(str(dest_unit_key)))
assert len(tags) == 2
assert str(tags[0]) == f'<ObjectTag> {dest_unit_key}: test_taxonomy=tag_1'
assert str(tags[1]) == f'<ObjectTag> {dest_unit_key}: test_taxonomy=tag_2'
def test_paste_with_assets(self):
"""
When pasting into a different course, any required static assets should

View File

@@ -1,5 +1,7 @@
# lint-amnesty, pylint: disable=missing-module-docstring
from urllib.parse import quote
"""
Content tagging functionality for XBlocks.
"""
from urllib.parse import quote, unquote
class TaggedBlockMixin:
@@ -7,9 +9,32 @@ class TaggedBlockMixin:
Mixin containing XML serializing and parsing functionality for tagged blocks
"""
def serialize_tag_data(self):
def __init__(self, *args, **kwargs):
"""
Serialize block's tag data to include in the xml, escaping special characters
Initialize the tagged xblock.
"""
# We store tags internally, without an XBlock field, because we don't want tags to be stored to the modulestore.
# But we do want them persisted on duplicate, copy, or export/import.
self.tags_v1 = ""
@property
def tags_v1(self) -> str:
"""
Returns the serialized tags.
"""
return self._tags_v1
@tags_v1.setter
def tags_v1(self, tags: str) -> None:
"""
Returns the serialized tags.
"""
self._tags_v1 = tags
@classmethod
def serialize_tag_data(cls, usage_id):
"""
Serialize a block's tag data to include in the xml, escaping special characters
Example tags:
LightCast Skills Taxonomy: ["Typing", "Microsoft Office"]
@@ -21,7 +46,7 @@ class TaggedBlockMixin:
# This import is done here since we import and use TaggedBlockMixin in the cms settings, but the
# content_tagging app wouldn't have loaded yet, so importing it outside causes an error
from openedx.core.djangoapps.content_tagging.api import get_object_tags
content_tags = get_object_tags(self.scope_ids.usage_id)
content_tags = get_object_tags(usage_id)
serialized_tags = []
taxonomies_and_tags = {}
@@ -45,13 +70,63 @@ class TaggedBlockMixin:
"""
Serialize and add tag data (if any) to node
"""
tag_data = self.serialize_tag_data()
tag_data = self.serialize_tag_data(self.scope_ids.usage_id)
if tag_data:
node.set('tags-v1', tag_data)
def add_tags_from_field(self):
"""
Parse tags_v1 data and create tags for this block.
"""
# This import is done here since we import and use TaggedBlockMixin in the cms settings, but the
# content_tagging app wouldn't have loaded yet, so importing it outside causes an error
from openedx.core.djangoapps.content_tagging.api import set_object_tags
tag_data = self.tags_v1
if not tag_data:
return
serialized_tags = tag_data.split(';')
taxonomy_and_tags_dict = {}
for serialized_tag in serialized_tags:
taxonomy_export_id, tags = serialized_tag.split(':')
tags = tags.split(',')
tag_values = [unquote(tag) for tag in tags]
taxonomy_and_tags_dict[taxonomy_export_id] = tag_values
set_object_tags(self.usage_key, taxonomy_and_tags_dict)
def add_xml_to_node(self, node):
"""
Include the serialized tag data in XML when adding to node
"""
super().add_xml_to_node(node)
self.add_tags_to_node(node)
def studio_post_duplicate(self, store, source_item) -> bool:
"""
Duplicates content tags from the source_item.
Returns False to indicate the children have not been handled.
"""
if hasattr(super(), 'studio_post_duplicate'):
super().studio_post_duplicate()
self.tags_v1 = self.serialize_tag_data(source_item.scope_ids.usage_id)
self.add_tags_from_field()
return False
def studio_post_paste(self, store, source_node) -> bool:
"""
Copies content tags from the source_node.
Returns False to indicate the children have not been handled.
"""
if hasattr(super(), 'studio_post_paste'):
super().studio_post_paste()
if 'tags-v1' in source_node.attrib:
self.tags_v1 = str(source_node.attrib['tags-v1'])
self.add_tags_from_field()
return False

View File

@@ -13,7 +13,8 @@ from openedx_tagging.core.tagging.models import ObjectTag, Taxonomy
from organizations.models import Organization
from .models import TaxonomyOrg
from .types import ObjectTagByObjectIdDict, TaxonomyDict
from .types import ContentKey, ObjectTagByObjectIdDict, TagValuesByTaxonomyExportIdDict, TaxonomyDict
from .utils import check_taxonomy_context_key_org, get_context_key_from_key
def create_taxonomy(
@@ -161,16 +162,42 @@ def get_all_object_tags(
for object_id, block_tags in groupby(all_object_tags, lambda x: x.object_id):
grouped_object_tags[object_id] = {}
for taxonomy_id, taxonomy_tags in groupby(block_tags, lambda x: x.tag.taxonomy_id):
for taxonomy_id, taxonomy_tags in groupby(block_tags, lambda x: x.tag.taxonomy_id if x.tag else 0):
object_tags_list = list(taxonomy_tags)
grouped_object_tags[object_id][taxonomy_id] = object_tags_list
if taxonomy_id not in taxonomies:
assert object_tags_list[0].tag
assert object_tags_list[0].tag.taxonomy
taxonomies[taxonomy_id] = object_tags_list[0].tag.taxonomy
return grouped_object_tags, taxonomies
def set_object_tags(
content_key: ContentKey,
object_tags: TagValuesByTaxonomyExportIdDict,
) -> None:
"""
Sets the tags for the given content object.
"""
context_key = get_context_key_from_key(content_key)
for taxonomy_export_id, tags_values in object_tags.items():
taxonomy = oel_tagging.get_taxonomy_by_export_id(taxonomy_export_id)
if not taxonomy:
continue
if not check_taxonomy_context_key_org(taxonomy, context_key):
continue
oel_tagging.tag_object(
object_id=str(content_key),
taxonomy=taxonomy,
tags=tags_values,
)
# Expose the oel_tagging APIs
get_taxonomy = oel_tagging.get_taxonomy
@@ -181,3 +208,4 @@ delete_object_tags = oel_tagging.delete_object_tags
resync_object_tags = oel_tagging.resync_object_tags
get_object_tags = oel_tagging.get_object_tags
tag_object = oel_tagging.tag_object
add_tag_to_taxonomy = oel_tagging.add_tag_to_taxonomy

View File

@@ -20,10 +20,9 @@ from common.djangoapps.student.roles import (
)
from .models import TaxonomyOrg
from .utils import get_context_key_from_key_string, TaggingRulesCache
from .utils import check_taxonomy_context_key_org, get_context_key_from_key_string, rules_cache
rules_cache = TaggingRulesCache()
UserType = Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]
@@ -288,19 +287,12 @@ def can_change_object_tag(
"""
if oel_tagging.can_change_object_tag(user, perm_obj):
if perm_obj and perm_obj.taxonomy and perm_obj.object_id:
# can_change_object_tag_objectid already checked that object_id is valid and has an org,
# so these statements will not fail. But we need to assert to keep the type checker happy.
try:
context_key = get_context_key_from_key_string(perm_obj.object_id)
assert context_key.org
except (ValueError, AssertionError):
except ValueError:
return False # pragma: no cover
is_all_org, taxonomy_orgs = TaxonomyOrg.get_organizations(perm_obj.taxonomy)
if not is_all_org:
# Ensure the object_id's org is among the allowed taxonomy orgs
object_org = rules_cache.get_orgs([context_key.org])
return bool(object_org) and object_org[0] in taxonomy_orgs
return check_taxonomy_context_key_org(perm_obj.taxonomy, context_key)
return True
return False

View File

@@ -10,7 +10,9 @@ from opaque_keys.edx.locator import LibraryLocatorV2
from openedx_tagging.core.tagging.models import ObjectTag, Taxonomy
ContentKey = Union[LibraryLocatorV2, CourseKey, UsageKey]
ContextKey = Union[LibraryLocatorV2, CourseKey]
ObjectTagByTaxonomyIdDict = Dict[int, List[ObjectTag]]
ObjectTagByObjectIdDict = Dict[str, ObjectTagByTaxonomyIdDict]
TaxonomyDict = Dict[int, Taxonomy]
TagValuesByTaxonomyExportIdDict = Dict[str, List[str]]

View File

@@ -7,11 +7,13 @@ from edx_django_utils.cache import RequestCache
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryLocatorV2
from openedx_tagging.core.tagging.models import Taxonomy
from organizations.models import Organization
from openedx.core.djangoapps.content_libraries.api import get_libraries_for_user
from .types import ContentKey
from .types import ContentKey, ContextKey
from .models import TaxonomyOrg
def get_content_key_from_string(key_str: str) -> ContentKey:
@@ -30,11 +32,10 @@ def get_content_key_from_string(key_str: str) -> ContentKey:
raise ValueError("object_id must be a CourseKey, LibraryLocatorV2 or a UsageKey") from usage_key_error
def get_context_key_from_key_string(key_str: str) -> CourseKey | LibraryLocatorV2:
def get_context_key_from_key(content_key: ContentKey) -> ContextKey:
"""
Get context key from an key string
Returns the context key from a given content key.
"""
content_key = get_content_key_from_string(key_str)
# If the content key is a CourseKey or a LibraryLocatorV2, return it
if isinstance(content_key, (CourseKey, LibraryLocatorV2)):
return content_key
@@ -48,6 +49,31 @@ def get_context_key_from_key_string(key_str: str) -> CourseKey | LibraryLocatorV
raise ValueError("context must be a CourseKey or a LibraryLocatorV2")
def get_context_key_from_key_string(key_str: str) -> ContextKey:
"""
Get context key from an key string
"""
content_key = get_content_key_from_string(key_str)
return get_context_key_from_key(content_key)
def check_taxonomy_context_key_org(taxonomy: Taxonomy, context_key: ContextKey) -> bool:
"""
Returns True if the given taxonomy can tag a object with the given context_key.
"""
if not context_key.org:
return False
is_all_org, taxonomy_orgs = TaxonomyOrg.get_organizations(taxonomy)
if is_all_org:
return True
# Ensure the object_id's org is among the allowed taxonomy orgs
object_org = rules_cache.get_orgs([context_key.org])
return bool(object_org) and object_org[0] in taxonomy_orgs
class TaggingRulesCache:
"""
Caches data required for computing rules for the duration of the request.
@@ -57,7 +83,7 @@ class TaggingRulesCache:
"""
Initializes the request cache.
"""
self.request_cache = RequestCache('openedx.core.djangoapps.content_tagging.rules')
self.request_cache = RequestCache('openedx.core.djangoapps.content_tagging.utils')
def get_orgs(self, org_names: list[str] | None = None) -> list[Organization]:
"""
@@ -102,3 +128,6 @@ class TaggingRulesCache:
return [
library_orgs[org_name] for org_name in org_names if org_name in library_orgs
]
rules_cache = TaggingRulesCache()

View File

@@ -89,6 +89,11 @@ class XBlockSerializer:
with filesystem.open(file_path, 'rb') as fh:
data = fh.read()
self.static_files.append(StaticFile(name=unit_file.name, data=data, url=None))
# Serialize and add tag data if any
if isinstance(block, TaggedBlockMixin):
block.add_tags_to_node(olx_node)
if block.has_children:
self._serialize_children(block, olx_node)
return olx_node

View File

@@ -108,14 +108,14 @@ libsass==0.10.0
click==8.1.6
# pinning this version to avoid updates while the library is being developed
openedx-learning==0.6.2
openedx-learning==0.6.3
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
openai<=0.28.1
# optimizely-sdk 5.0.0 is breaking following test with segmentation fault
# common/djangoapps/third_party_auth/tests/test_views.py::SAMLMetadataTest::test_secure_key_configuration
# needs to be fixed in the follow up issue
# needs to be fixed in the follow up issue
# https://github.com/openedx/edx-platform/issues/34103
optimizely-sdk<5.0

View File

@@ -786,7 +786,7 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/kernel.in
# lti-consumer-xblock
openedx-learning==0.6.2
openedx-learning==0.6.3
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
@@ -796,7 +796,7 @@ optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/bundled.in
ora2==6.0.34
ora2==6.4.0
# via -r requirements/edx/bundled.in
packaging==23.2
# via

View File

@@ -1308,7 +1308,7 @@ openedx-filters==1.6.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# lti-consumer-xblock
openedx-learning==0.6.2
openedx-learning==0.6.3
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
@@ -1322,7 +1322,7 @@ optimizely-sdk==4.1.1
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
ora2==6.0.34
ora2==6.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt

View File

@@ -921,7 +921,7 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
openedx-learning==0.6.2
openedx-learning==0.6.3
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -931,7 +931,7 @@ optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
ora2==6.0.34
ora2==6.4.0
# via -r requirements/edx/base.txt
packaging==23.2
# via

View File

@@ -978,7 +978,7 @@ openedx-filters==1.6.0
# via
# -r requirements/edx/base.txt
# lti-consumer-xblock
openedx-learning==0.6.2
openedx-learning==0.6.3
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
@@ -988,7 +988,7 @@ optimizely-sdk==4.1.1
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
ora2==6.0.34
ora2==6.4.0
# via -r requirements/edx/base.txt
packaging==23.2
# via

View File

@@ -593,10 +593,23 @@ class LibraryContentBlock(
Otherwise we'll end up losing data on the next refresh.
"""
if hasattr(super(), 'studio_post_duplicate'):
super().studio_post_duplicate(store, source_block)
self._validate_sync_permissions()
self.get_tools(to_read_library_content=True).trigger_duplication(source_block=source_block, dest_block=self)
return True # Children have been handled.
def studio_post_paste(self, store, source_node) -> bool:
"""
Pull the children from the library and let library_tools assign their IDs.
"""
if hasattr(super(), 'studio_post_paste'):
super().studio_post_paste(store, source_node)
self.sync_from_library(upgrade_to_latest=False)
return True # Children have been handled
def _validate_library_version(self, validation, lib_tools, version, library_key):
"""
Validates library version

View File

@@ -49,7 +49,7 @@ class StudioEditableBlock(XBlockMixin):
"""
return AUTHOR_VIEW if has_author_view(block) else STUDENT_VIEW
# Some parts of the code use getattr to dynamically check for the following three methods on subclasses.
# Some parts of the code use getattr to dynamically check for the following methods on subclasses.
# We'd like to refactor so that we can actually declare them here as overridable methods.
# For now, we leave them here as documentation.
# See https://github.com/openedx/edx-platform/issues/33715.
@@ -68,7 +68,7 @@ class StudioEditableBlock(XBlockMixin):
# By default, is a no-op. Can be overriden in subclasses.
# """
#
# def studio_post_duplicate(self, dest_block) -> bool: # pylint: disable=unused-argument
# def studio_post_duplicate(self, store, source_block) -> bool: # pylint: disable=unused-argument
# """
# Called when a the block is duplicated. Can be used, e.g., for special handling of child duplication.
#
@@ -78,6 +78,17 @@ class StudioEditableBlock(XBlockMixin):
# By default, is a no-op. Can be overriden in subclasses.
# """
# return False
#
# def studio_post_paste(self, store, source_node) -> bool: # pylint: disable=unused-argument
# """
# Called after a block is copy-pasted. Can be used, e.g., for special handling of child duplication.
#
# Returns 'True' if children have been handled and thus shouldn't be handled by the standard
# duplication logic.
#
# By default, is a no-op. Can be overriden in subclasses.
# """
# return False
def has_author_view(block):

View File

@@ -428,8 +428,6 @@ class XmlMixin:
"""
For exporting, set data on `node` from ourselves.
"""
# Importing here to avoid circular import
from cms.lib.xblock.tagging.tagged_block_mixin import TaggedBlockMixin
# Get the definition
xml_object = self.definition_to_xml(self.runtime.export_fs)
@@ -500,10 +498,6 @@ class XmlMixin:
node.set('org', self.location.org)
node.set('course', self.location.course)
# Serialize and add tag data if any
if isinstance(self, TaggedBlockMixin):
self.add_tags_to_node(node)
def definition_to_xml(self, resource_fs):
"""
Return a new etree Element object created from this modules definition.