From c02e567201dd60944dbdbb915fa5d334c2dc08d1 Mon Sep 17 00:00:00 2001 From: Jillian Date: Fri, 14 Feb 2025 03:23:05 +1030 Subject: [PATCH] Make copied tags editable again after breaking the upstream link to library content [FC-0076] (#36228) When deleting an upstream library block, ensure that any tags that may have been copied to downstream blocks are made editable again. This is achieved by un-setting the `is_copied` flag on the downstream tags. --- .../contentstore/signals/handlers.py | 28 +++++++++++++++++-- cms/djangoapps/contentstore/tasks.py | 21 ++++++++++++++ .../views/tests/test_clipboard_paste.py | 25 ++++++++++++++++- .../core/djangoapps/content_tagging/api.py | 1 + requirements/constraints.txt | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 9 files changed, 77 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 5f6fa21246..5635d46556 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -12,10 +12,17 @@ from django.db import transaction 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.django import SignalHandler, modulestore 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 68e0c78279..c2803f3e27 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -67,6 +67,7 @@ from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRol 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 3b39f2918a..3fae9d996f 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 @@ APIs. 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 @@ class ClipboardPasteTestCase(ModuleStoreTestCase): 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 @@ class ClipboardPasteFromV2LibraryTestCase(ModuleStoreTestCase): 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 8a06e483ab..96f6886b62 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -441,3 +441,4 @@ resync_object_tags = oel_tagging.resync_object_tags 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 3d9f7c08e8..a28688eedf 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 f76379c692..ba81c34446 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 2635c6c3c7..3f1e197bf5 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 cd68ea1c26..00d75a6029 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 21adf5f8bd..7610aa0602 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