diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 5f6fa2124678..5635d4655622 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -12,10 +12,17 @@ from django.dispatch import receiver from edx_toggles.toggles import SettingToggle from opaque_keys.edx.keys import CourseKey -from openedx_events.content_authoring.data import CourseCatalogData, CourseData, CourseScheduleData, XBlockData +from openedx_events.content_authoring.data import ( + CourseCatalogData, + CourseData, + CourseScheduleData, + LibraryBlockData, + XBlockData, +) from openedx_events.content_authoring.signals import ( COURSE_CATALOG_INFO_CHANGED, COURSE_IMPORT_COMPLETED, + LIBRARY_BLOCK_DELETED, XBLOCK_CREATED, XBLOCK_DELETED, XBLOCK_UPDATED, @@ -38,7 +45,11 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from ..models import PublishableEntityLink -from ..tasks import create_or_update_upstream_links, handle_create_or_update_xblock_upstream_link +from ..tasks import ( + create_or_update_upstream_links, + handle_create_or_update_xblock_upstream_link, + handle_unlink_upstream_block, +) from .signals import GRADING_POLICY_CHANGED log = logging.getLogger(__name__) @@ -287,3 +298,16 @@ def handle_new_course_import(**kwargs): force=True, replace=True ) + + +@receiver(LIBRARY_BLOCK_DELETED) +def unlink_upstream_block_handler(**kwargs): + """ + Handle unlinking the upstream (library) block from any downstream (course) blocks. + """ + library_block = kwargs.get("library_block", None) + if not library_block or not isinstance(library_block, LibraryBlockData): + log.error("Received null or incorrect data for event") + return + + handle_unlink_upstream_block.delay(str(library_block.usage_key)) diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 68e0c7827948..c2803f3e2751 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -67,6 +67,7 @@ from common.djangoapps.util.monitoring import monitor_import_failure from openedx.core.djangoapps.content.learning_sequences.api import key_supports_outlines from openedx.core.djangoapps.content_libraries import api as v2contentlib_api +from openedx.core.djangoapps.content_tagging.api import make_copied_tags_editable from openedx.core.djangoapps.course_apps.toggles import exams_ida_enabled from openedx.core.djangoapps.discussions.config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider @@ -1466,3 +1467,23 @@ def create_or_update_upstream_links( for xblock in xblocks: create_or_update_xblock_upstream_link(xblock, course_key_str, created) course_status.update_status(LearningContextLinksStatusChoices.COMPLETED) + + +@shared_task +@set_code_owner_attribute +def handle_unlink_upstream_block(upstream_usage_key_string: str) -> None: + """ + Handle updates needed to downstream blocks when the upstream link is severed. + """ + ensure_cms("handle_unlink_upstream_block may only be executed in a CMS context") + + try: + upstream_usage_key = UsageKey.from_string(upstream_usage_key_string) + except (InvalidKeyError): + LOGGER.exception(f'Invalid upstream usage_key: {upstream_usage_key_string}') + return + + for link in PublishableEntityLink.objects.filter( + upstream_usage_key=upstream_usage_key, + ): + make_copied_tags_editable(str(link.downstream_usage_key)) diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 3b39f2918a36..3fae9d996fd2 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -6,6 +6,13 @@ import ddt from opaque_keys.edx.keys import UsageKey from rest_framework.test import APIClient +from openedx_events.content_authoring.signals import ( + LIBRARY_BLOCK_DELETED, + XBLOCK_CREATED, + XBLOCK_DELETED, + XBLOCK_UPDATED, +) +from openedx_events.tests.utils import OpenEdxEventsTestMixin from openedx_tagging.core.tagging.models import Tag from organizations.models import Organization from xmodule.modulestore.django import contentstore, modulestore @@ -393,10 +400,16 @@ def test_paste_with_assets(self): assert source_pic2_hash != dest_pic2_hash # Because there was a conflict, this file was unchanged. -class ClipboardPasteFromV2LibraryTestCase(ModuleStoreTestCase): +class ClipboardPasteFromV2LibraryTestCase(OpenEdxEventsTestMixin, ModuleStoreTestCase): """ Test Clipboard Paste functionality with a "new" (as of Sumac) library """ + ENABLED_OPENEDX_EVENTS = [ + LIBRARY_BLOCK_DELETED.event_type, + XBLOCK_CREATED.event_type, + XBLOCK_DELETED.event_type, + XBLOCK_UPDATED.event_type, + ] def setUp(self): """ @@ -477,6 +490,16 @@ def test_paste_from_library_read_only_tags(self): assert object_tag.value in self.lib_block_tags assert object_tag.is_copied + # If we delete the upstream library block... + library_api.delete_library_block(self.lib_block_key) + + # ...the copied tags remain, but should no longer be marked as "copied" + object_tags = tagging_api.get_object_tags(new_block_key) + assert len(object_tags) == len(self.lib_block_tags) + for object_tag in object_tags: + assert object_tag.value in self.lib_block_tags + assert not object_tag.is_copied + def test_paste_from_library_copies_asset(self): """ Assets from a library component copied into a subdir of Files & Uploads. diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index 8a06e483ab78..96f6886b622b 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -441,3 +441,4 @@ def tag_object( get_object_tags = oel_tagging.get_object_tags add_tag_to_taxonomy = oel_tagging.add_tag_to_taxonomy copy_tags_as_read_only = oel_tagging.copy_tags +make_copied_tags_editable = oel_tagging.unmark_copied_tags diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 3d9f7c08e88b..a28688eedf45 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -131,7 +131,7 @@ optimizely-sdk<5.0 # Date: 2023-09-18 # pinning this version to avoid updates while the library is being developed # Issue for unpinning: https://github.com/openedx/edx-platform/issues/35269 -openedx-learning==0.18.2 +openedx-learning==0.18.3 # Date: 2023-11-29 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index f76379c69237..ba81c34446d6 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -827,7 +827,7 @@ openedx-filters==1.12.0 # ora2 openedx-forum==0.1.6 # via -r requirements/edx/kernel.in -openedx-learning==0.18.2 +openedx-learning==0.18.3 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 2635c6c3c7fa..3f1e197bf5ac 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1381,7 +1381,7 @@ openedx-forum==0.1.6 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -openedx-learning==0.18.2 +openedx-learning==0.18.3 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index cd68ea1c26bf..00d75a6029d7 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1000,7 +1000,7 @@ openedx-filters==1.12.0 # ora2 openedx-forum==0.1.6 # via -r requirements/edx/base.txt -openedx-learning==0.18.2 +openedx-learning==0.18.3 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 21adf5f8bde0..7610aa060284 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1048,7 +1048,7 @@ openedx-filters==1.12.0 # ora2 openedx-forum==0.1.6 # via -r requirements/edx/base.txt -openedx-learning==0.18.2 +openedx-learning==0.18.3 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt