diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index c8ac9b89dc2d..742db9f5bc20 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -73,6 +73,7 @@ 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,57 @@ def test_duplicate_library_content_block(self): # pylint: disable=too-many-stat 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(taxonomyA, ["one", "two"], str(source_chapter.location)) + tagging_api.tag_object(taxonomyB, ["three", "four"], str(source_chapter.location)) + + # Tag the child block + tagging_api.tag_object(taxonomyA, ["two"], str(source_block.location)) + + # Refresh. + source_chapter = self.store.get_item(source_chapter.location) + expected_chapter_tags = 'A:one,two;B:four,three' + assert source_chapter.serialize_tag_data() == expected_chapter_tags + + source_block = self.store.get_item(source_block.location) + expected_block_tags = 'A:two' + assert source_block.serialize_tag_data() == expected_block_tags + + # 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 + assert dupe_chapter.serialize_tag_data() == expected_chapter_tags + assert dupe_block.serialize_tag_data() == expected_block_tags + @ddt.ddt class TestMoveItem(ItemTest): diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 12f620b41e14..07c06e960f53 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -203,10 +203,8 @@ def test_copy_and_paste_unit_with_tags(self): # 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]) == ' ' \ - 'block-v1:org.2025+course_2025+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_1' - assert str(tags[1]) == ' ' \ - 'block-v1:org.2025+course_2025+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_2' + assert str(tags[0]) == f' {dest_unit_key}: test_taxonomy=tag_1' + assert str(tags[1]) == f' {dest_unit_key}: test_taxonomy=tag_2' def test_paste_with_assets(self): """ diff --git a/cms/lib/xblock/tagging/tagged_block_mixin.py b/cms/lib/xblock/tagging/tagged_block_mixin.py index 5c536f480994..da2ef3c8b57f 100644 --- a/cms/lib/xblock/tagging/tagged_block_mixin.py +++ b/cms/lib/xblock/tagging/tagged_block_mixin.py @@ -1,4 +1,6 @@ -# lint-amnesty, pylint: disable=missing-module-docstring +""" +Content tagging functionality for XBlocks. +""" from urllib.parse import quote, unquote @@ -6,6 +8,17 @@ class TaggedBlockMixin: """ Mixin containing XML serializing and parsing functionality for tagged blocks """ + def studio_post_duplicate(self, store, source_item): + """ + Duplicates content tags from the source_item. + """ + if hasattr(super(), 'studio_post_duplicate'): + super().studio_post_duplicate() + + if hasattr(source_item, 'serialize_tag_data'): + tags = source_item.serialize_tag_data() + self.xml_attributes['tags-v1'] = tags + self.add_tags_from_xml() def serialize_tag_data(self): """ diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index ebb5c376d346..b9b7f47a7039 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -203,3 +203,4 @@ def set_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