From 6fbfd3c3e4149e3d0821ab470fa4ca95480494b3 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 4 Oct 2024 21:35:46 -0500 Subject: [PATCH 01/15] feat: Copy tags when sync library --- .../core/djangoapps/content_libraries/tasks.py | 4 ++++ openedx/core/djangoapps/content_tagging/api.py | 1 + xmodule/tests/test_library_tools.py | 16 ++++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/content_libraries/tasks.py b/openedx/core/djangoapps/content_libraries/tasks.py index 9f4f7aaaf7dc..352a2d5c5802 100644 --- a/openedx/core/djangoapps/content_libraries/tasks.py +++ b/openedx/core/djangoapps/content_libraries/tasks.py @@ -39,6 +39,7 @@ from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.content_libraries import api as library_api from openedx.core.djangoapps.xblock.api import load_block +from openedx.core.djangoapps.content_tagging import api as tagging_api from openedx.core.lib import ensure_cms from xmodule.capa_block import ProblemBlock from xmodule.library_content_block import ANY_CAPA_TYPE_VALUE, LibraryContentBlock @@ -143,6 +144,9 @@ def generate_block_key(source_key, dest_parent_key): new_block.save() store.update_item(new_block, user_id) + # Copy/Sync tags of this block + tagging_api.copy_tags(source_key, new_block_key) + if new_block.has_children: # Delete existing children in the new block, which can be reimported again if they still exist in the # source library diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index f015770e5db8..0f5d0b4e1f0b 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -440,3 +440,4 @@ def tag_object( 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 = oel_tagging.copy_tags diff --git a/xmodule/tests/test_library_tools.py b/xmodule/tests/test_library_tools.py index f93066cd5c63..eab96dc5bada 100644 --- a/xmodule/tests/test_library_tools.py +++ b/xmodule/tests/test_library_tools.py @@ -17,6 +17,8 @@ from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangolib.testing.utils import skip_unless_cms from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest +from openedx.core.djangoapps.content_tagging import api as tagging_api +from openedx.core.djangoapps.content_tagging.tests.test_api import TestTaxonomyMixin from xmodule.library_tools import LibraryToolsService from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory from xmodule.modulestore.tests.utils import MixedSplitTestCase @@ -24,7 +26,7 @@ @skip_unless_cms @ddt.ddt -class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest): +class ContentLibraryToolsTest(TestTaxonomyMixin, MixedSplitTestCase, ContentLibrariesRestApiTest): """ Tests for LibraryToolsService. @@ -104,7 +106,8 @@ def test_update_children_for_v2_lib(self): library = self._create_library( slug="cool-v2-lib", title="The best Library", description="Spectacular description" ) - self._add_block_to_library(library["id"], "unit", "unit1_id") + block = self._add_block_to_library(library["id"], "unit", "unit1_id") + tagging_api.tag_object(block["id"], self.taxonomy_all_orgs, [self.tag_all_orgs.value]) course = CourseFactory.create(modulestore=self.store, user_id=self.user.id) CourseInstructorRole(course.id).add_users(self.user) @@ -126,6 +129,15 @@ def test_update_children_for_v2_lib(self): assert len(content_block.children) == 1 + # Verify that tags are copied to children + child_key = str(content_block.children[0]) + tags = tagging_api.get_object_tags(child_key) + assert len(tags) == 1 + object_tag = tags[0] + assert object_tag.value == self.tag_all_orgs.value + assert object_tag.taxonomy == self.taxonomy_all_orgs + assert object_tag.object_id == child_key + def test_update_children_for_v1_lib(self): """ Test update_children with V1 library as a source. From aed1998966373be3a08ed45f0ac1648df0edc390 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 8 Oct 2024 15:51:23 -0500 Subject: [PATCH 02/15] feat: Avoid delete object tag if is copied --- .../rest_api/v1/serializers.py | 18 +++++++++++++ .../rest_api/v1/tests/test_views.py | 25 +++++++++++++++++++ .../content_tagging/rest_api/v1/views.py | 8 +++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py index 8bd26230855a..2abecc5842a3 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py @@ -6,10 +6,13 @@ from rest_framework import serializers, fields +from opaque_keys.edx.keys import UsageKey from openedx_tagging.core.tagging.rest_api.v1.serializers import ( + ObjectTagMinimalSerializer, TaxonomyListQueryParamsSerializer, TaxonomySerializer, ) +from xmodule.modulestore.django import modulestore from organizations.models import Organization @@ -94,3 +97,18 @@ class Meta: model = TaxonomySerializer.Meta.model fields = TaxonomySerializer.Meta.fields + ["orgs", "all_orgs"] read_only_fields = ["orgs", "all_orgs"] + +class ObjectTagOrgMinimalSerializer(ObjectTagMinimalSerializer): + """ + Serializer for Object Tags + """ + + def get_can_delete_objecttag(self, instance): + """ + Verify if the user can delete the object tag. + """ + if instance.is_copied: + # The user can't delete copied tags. + return False + + return super().get_can_delete_objecttag(instance) diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py index e386ee234226..a5186ee26781 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py @@ -1850,6 +1850,31 @@ def test_get_tags(self): assert status.is_success(response3.status_code) assert response3.data[str(self.courseA)]["taxonomies"] == expected_tags + def test_get_copied_tags(self): + self.client.force_authenticate(user=self.staffB) + + object_id_1 = str(self.courseA) + object_id_2 = str(self.courseB) + tagging_api.tag_object(object_id=object_id_1, taxonomy=self.t1, tags=["android"]) + tagging_api.tag_object(object_id=object_id_2, taxonomy=self.t1, tags=["anvil"]) + tagging_api.copy_tags(object_id_1, object_id_2) + + expected_tags = [{ + 'name': self.t1.name, + 'taxonomy_id': self.t1.pk, + 'can_tag_object': True, + 'export_id': self.t1.export_id, + 'tags': [ + {'value': 'android', 'lineage': ['ALPHABET', 'android'], 'can_delete_objecttag': False}, + {'value': 'anvil', 'lineage': ['ALPHABET', 'anvil'], 'can_delete_objecttag': True} + ] + }] + + get_url = OBJECT_TAGS_URL.format(object_id=self.courseB) + response = self.client.get(get_url, format="json") + assert status.is_success(response.status_code) + assert response.data[str(object_id_2)]["taxonomies"] == expected_tags + @ddt.data( ('staff', 'courseA', 8), ('staff', 'libraryA', 8), diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py index 3fc99736bae9..e6a1b250ba10 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py @@ -31,7 +31,12 @@ ) from ...rules import get_admin_orgs from .filters import ObjectTagTaxonomyOrgFilterBackend, UserOrgFilterBackend -from .serializers import TaxonomyOrgListQueryParamsSerializer, TaxonomyOrgSerializer, TaxonomyUpdateOrgBodySerializer +from .serializers import ( + ObjectTagOrgMinimalSerializer, + TaxonomyOrgListQueryParamsSerializer, + TaxonomyOrgSerializer, + TaxonomyUpdateOrgBodySerializer, +) class TaxonomyOrgView(TaxonomyView): @@ -148,6 +153,7 @@ class ObjectTagOrgView(ObjectTagView): Refer to ObjectTagView docstring for usage details. """ + minimal_serilizer_class = ObjectTagOrgMinimalSerializer filter_backends = [ObjectTagTaxonomyOrgFilterBackend] def update(self, request, *args, **kwargs) -> Response: From 402551a8651f33ed2a009b203da4181d4f91c80a Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 8 Oct 2024 16:18:36 -0500 Subject: [PATCH 03/15] temp: Use opencraft branch of openedx-learning --- requirements/constraints.txt | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/kernel.in | 2 +- requirements/edx/testing.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 46d8fb4192e7..87eaca63ee4d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -93,7 +93,7 @@ libsass==0.10.0 click==8.1.6 # pinning this version to avoid updates while the library is being developed -openedx-learning==0.13.1 +openedx-learning==0.14.0 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. openai<=0.28.1 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 693d7bc73216..2a07a3e9cb81 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -824,7 +824,7 @@ openedx-filters==1.10.0 # -r requirements/edx/kernel.in # lti-consumer-xblock # ora2 -openedx-learning==0.13.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index bb2e335a9f93..45fe28f08cb1 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1373,7 +1373,7 @@ openedx-filters==1.10.0 # -r requirements/edx/testing.txt # lti-consumer-xblock # ora2 -openedx-learning==0.13.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 74453bc3659e..b9b758997075 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -983,7 +983,7 @@ openedx-filters==1.10.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-learning==0.13.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index a5b510742ac7..8497a8c60727 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -119,7 +119,7 @@ openedx-calc # Library supporting mathematical calculatio openedx-django-require openedx-events # Open edX Events from Hooks Extension Framework (OEP-50) openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50) -openedx-learning # Open edX Learning core (experimental) +git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags#egg=openedx-learning # Open edX Learning core (experimental) openedx-mongodbproxy openedx-django-wiki path diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 2a9a03160379..2880cfa71f93 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1034,7 +1034,7 @@ openedx-filters==1.10.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-learning==0.13.1 +openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt From 7b13690d048516868acf5592949c004c997100c5 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 9 Oct 2024 20:16:19 -0500 Subject: [PATCH 04/15] style: Fix lint on the code --- .../core/djangoapps/content_tagging/rest_api/v1/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py index 2abecc5842a3..5bd6a9b06a62 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py @@ -6,13 +6,11 @@ from rest_framework import serializers, fields -from opaque_keys.edx.keys import UsageKey from openedx_tagging.core.tagging.rest_api.v1.serializers import ( ObjectTagMinimalSerializer, TaxonomyListQueryParamsSerializer, TaxonomySerializer, ) -from xmodule.modulestore.django import modulestore from organizations.models import Organization @@ -98,6 +96,7 @@ class Meta: fields = TaxonomySerializer.Meta.fields + ["orgs", "all_orgs"] read_only_fields = ["orgs", "all_orgs"] + class ObjectTagOrgMinimalSerializer(ObjectTagMinimalSerializer): """ Serializer for Object Tags From d9bba2c1b9f6206074da6fce39b714228b738801 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 14 Oct 2024 20:11:00 -0500 Subject: [PATCH 05/15] refactor: Update class name to ObjectTagCopiedMinimalSerializer --- .../content_tagging/rest_api/v1/serializers.py | 9 +++++++-- .../core/djangoapps/content_tagging/rest_api/v1/views.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py index 5bd6a9b06a62..849d3891f207 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py @@ -97,14 +97,19 @@ class Meta: read_only_fields = ["orgs", "all_orgs"] -class ObjectTagOrgMinimalSerializer(ObjectTagMinimalSerializer): +class ObjectTagCopiedMinimalSerializer(ObjectTagMinimalSerializer): """ - Serializer for Object Tags + Serializer for Object Tags. + + This override `get_can_delete_objecttag` to avoid delete + object tags if is copied. """ def get_can_delete_objecttag(self, instance): """ Verify if the user can delete the object tag. + + Override to return `False` if the object tag is copied. """ if instance.is_copied: # The user can't delete copied tags. diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py index e6a1b250ba10..c2f79ef677db 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py @@ -32,7 +32,7 @@ from ...rules import get_admin_orgs from .filters import ObjectTagTaxonomyOrgFilterBackend, UserOrgFilterBackend from .serializers import ( - ObjectTagOrgMinimalSerializer, + ObjectTagCopiedMinimalSerializer, TaxonomyOrgListQueryParamsSerializer, TaxonomyOrgSerializer, TaxonomyUpdateOrgBodySerializer, @@ -153,7 +153,7 @@ class ObjectTagOrgView(ObjectTagView): Refer to ObjectTagView docstring for usage details. """ - minimal_serilizer_class = ObjectTagOrgMinimalSerializer + minimal_serializer_class = ObjectTagCopiedMinimalSerializer filter_backends = [ObjectTagTaxonomyOrgFilterBackend] def update(self, request, *args, **kwargs) -> Response: From 7f5eb934770346fc71956f46e85abc65dfccbdf1 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 15 Oct 2024 10:43:58 -0500 Subject: [PATCH 06/15] chore: Bump version of openedx-learning in constrains.txt --- requirements/constraints.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 87eaca63ee4d..c185014f251f 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -93,7 +93,7 @@ libsass==0.10.0 click==8.1.6 # pinning this version to avoid updates while the library is being developed -openedx-learning==0.14.0 +openedx-learning==0.16.0 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. openai<=0.28.1 From 14254c453108d2d14d6a9b13c5ab2bb1b612fdc5 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 16 Oct 2024 10:17:07 -0500 Subject: [PATCH 07/15] temp: Update openedx-learning to 0.15.0 --- requirements/constraints.txt | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/kernel.in | 2 +- requirements/edx/testing.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index c185014f251f..b3012494ebdc 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -93,7 +93,7 @@ libsass==0.10.0 click==8.1.6 # pinning this version to avoid updates while the library is being developed -openedx-learning==0.16.0 +openedx-learning==0.15.0 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. openai<=0.28.1 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 2a07a3e9cb81..7536e9710240 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -824,7 +824,7 @@ openedx-filters==1.10.0 # -r requirements/edx/kernel.in # lti-consumer-xblock # ora2 -openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags +openedx-learning==0.15.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 45fe28f08cb1..413e03f8c787 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1373,7 +1373,7 @@ openedx-filters==1.10.0 # -r requirements/edx/testing.txt # lti-consumer-xblock # ora2 -openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags +openedx-learning==0.15.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index b9b758997075..08d555e5ca78 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -983,7 +983,7 @@ openedx-filters==1.10.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags +openedx-learning==0.15.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 8497a8c60727..a5b510742ac7 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -119,7 +119,7 @@ openedx-calc # Library supporting mathematical calculatio openedx-django-require openedx-events # Open edX Events from Hooks Extension Framework (OEP-50) openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50) -git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags#egg=openedx-learning # Open edX Learning core (experimental) +openedx-learning # Open edX Learning core (experimental) openedx-mongodbproxy openedx-django-wiki path diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 2880cfa71f93..84a1760d58b5 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1034,7 +1034,7 @@ openedx-filters==1.10.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-learning @ git+https://github.com/open-craft/openedx-learning.git@chris/FAL-3616-copy-tags +openedx-learning==0.15.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt From 0783cf31c6d4e7a848df2b0a82ea99f6496b5ae8 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 16 Oct 2024 20:36:44 -0500 Subject: [PATCH 08/15] chore: Bump version of openedx-learning to 0.16.0 --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- xmodule/tests/test_library_tools.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 6ede5ed12302..857a277a8200 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -825,7 +825,7 @@ openedx-filters==1.10.0 # -r requirements/edx/kernel.in # lti-consumer-xblock # ora2 -openedx-learning==0.15.0 +openedx-learning==0.16.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 901a37c05522..9a50c7e130f0 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1374,7 +1374,7 @@ openedx-filters==1.10.0 # -r requirements/edx/testing.txt # lti-consumer-xblock # ora2 -openedx-learning==0.15.0 +openedx-learning==0.16.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 34e6275592b1..80ac8ebd264f 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -984,7 +984,7 @@ openedx-filters==1.10.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-learning==0.15.0 +openedx-learning==0.16.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 5658241be489..d08ccb4af623 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1035,7 +1035,7 @@ openedx-filters==1.10.0 # -r requirements/edx/base.txt # lti-consumer-xblock # ora2 -openedx-learning==0.15.0 +openedx-learning==0.16.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/xmodule/tests/test_library_tools.py b/xmodule/tests/test_library_tools.py index 5059ddb96459..30b007c3d963 100644 --- a/xmodule/tests/test_library_tools.py +++ b/xmodule/tests/test_library_tools.py @@ -21,7 +21,7 @@ @skip_unless_cms @ddt.ddt -class ContentLibraryToolsTest(TestTaxonomyMixin, MixedSplitTestCase, ContentLibrariesRestApiTest): +class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest): """ Tests for LegacyLibraryToolsService. """ From b0b32dacb78aa3a371be979bf38dbed5499b2567 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 17 Oct 2024 15:25:41 -0500 Subject: [PATCH 09/15] feat: Copy read only tags in library blocks --- cms/djangoapps/contentstore/helpers.py | 31 ++++++++++++++----- .../core/djangoapps/content_tagging/api.py | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index e9f599772d3e..c7de661eba5b 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -11,8 +11,9 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.utils.translation import gettext as _ +from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import AssetKey, CourseKey, UsageKey -from opaque_keys.edx.locator import DefinitionLocator, LocalId +from opaque_keys.edx.locator import DefinitionLocator, LocalId, LibraryUsageLocatorV2 from xblock.core import XBlock from xblock.fields import ScopeIds from xblock.runtime import IdGenerator @@ -267,7 +268,6 @@ def import_staged_content_from_user_clipboard(parent_key: UsageKey, request) -> empty, and (2) a summary of changes made to static files in the destination course. """ - from cms.djangoapps.contentstore.views.preview import _load_preview_block if not content_staging_api: @@ -428,14 +428,29 @@ def _import_xml_node_to_parent( tags=tags, ) + def copy_tags(): + if copied_from_block and tags: + object_tags = tags.get(str(copied_from_block)) + if object_tags: + content_tagging_api.set_all_object_tags( + content_key=new_xblock.location, + object_tags=object_tags, + ) + # Copy content tags to the new xblock - if copied_from_block and tags: - object_tags = tags.get(str(copied_from_block)) - if object_tags: - content_tagging_api.set_all_object_tags( - content_key=new_xblock.location, - object_tags=object_tags, + if (new_xblock.upstream): + # Verify if the upstream is a library component + # Copy the tags from library component upstream as ready only + try: + LibraryUsageLocatorV2.from_string(new_xblock.upstream) + content_tagging_api.copy_tags_as_read_only( + new_xblock.upstream, + new_xblock.location, ) + except InvalidKeyError: + copy_tags() + else: + copy_tags() return new_xblock diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index 0f5d0b4e1f0b..8a06e483ab78 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -440,4 +440,4 @@ def tag_object( 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 = oel_tagging.copy_tags +copy_tags_as_read_only = oel_tagging.copy_tags From b6bac86439d1e512299333f431f522afc443d328 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 17 Oct 2024 15:26:25 -0500 Subject: [PATCH 10/15] test: Tests for copy paste library blocks --- .../views/tests/test_clipboard_paste.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 5706b44e2cec..3827a48940a5 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -184,7 +184,7 @@ def _setup_tagged_content(self, course_key) -> dict: tagging_api.set_taxonomy_orgs(taxonomy_all_org, all_orgs=True) for tag_value in ('tag_1', 'tag_2', 'tag_3', 'tag_4', 'tag_5', 'tag_6', 'tag_7'): - Tag.objects.create(taxonomy=taxonomy_all_org, value=tag_value) + tagging_api.add_tag_to_taxonomy(taxonomy_all_org, tag_value) tagging_api.tag_object( object_id=str(unit_key), taxonomy=taxonomy_all_org, @@ -444,6 +444,18 @@ def setUp(self): self.course = CourseFactory.create(display_name='Course') + taxonomy_all_org = tagging_api.create_taxonomy( + "test_taxonomy", + "Test Taxonomy", + export_id="ALL_ORGS", + ) + tagging_api.set_taxonomy_orgs(taxonomy_all_org, all_orgs=True) + for tag_value in ('tag_1', 'tag_2', 'tag_3', 'tag_4', 'tag_5', 'tag_6', 'tag_7'): + tagging_api.add_tag_to_taxonomy(taxonomy_all_org, tag_value) + + self.lib_block_tags = ['tag_1', 'tag_5'] + tagging_api.tag_object(str(self.lib_block_key), taxonomy_all_org, ['tag_1', 'tag_5']) + def test_paste_from_library_creates_link(self): """ When we copy a v2 lib block into a course, the dest block should be linked up to the lib block. @@ -464,6 +476,31 @@ def test_paste_from_library_creates_link(self): assert new_block.upstream_display_name == "MCQ-draft" assert new_block.upstream_max_attempts == 5 + def test_paste_from_library_read_only_tags(self): + """ + When we copy a v2 lib block into a course, the dest block should have read-only copied tags. + """ + + copy_response = self.client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(self.lib_block_key)}, format="json") + assert copy_response.status_code == 200 + + paste_response = self.client.post(XBLOCK_ENDPOINT, { + "parent_locator": str(self.course.usage_key), + "staged_content": "clipboard", + }, format="json") + assert paste_response.status_code == 200 + + + new_block_key = paste_response.json()["locator"] + + object_tags = tagging_api.get_object_tags(new_block_key) + assert len(object_tags) == len(self.lib_block_tags) + print(object_tags) + for object_tag in object_tags: + print(object_tag) + assert object_tag.value in self.lib_block_tags + assert object_tag.is_copied + class ClipboardPasteFromV1LibraryTestCase(ModuleStoreTestCase): """ From 7a1797af86a4f85adaccc2716202466b1284b6ab Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 17 Oct 2024 20:10:49 -0500 Subject: [PATCH 11/15] feat: Sync tags when sync upstream --- cms/djangoapps/contentstore/helpers.py | 2 +- .../views/tests/test_clipboard_paste.py | 4 +- cms/lib/xblock/test/test_upstream_sync.py | 64 +++++++++++++++++++ cms/lib/xblock/upstream_sync.py | 18 +++++- 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index c7de661eba5b..05c79961c13d 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -438,7 +438,7 @@ def copy_tags(): ) # Copy content tags to the new xblock - if (new_xblock.upstream): + if new_xblock.upstream: # Verify if the upstream is a library component # Copy the tags from library component upstream as ready only try: diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 3827a48940a5..0f2ec95f7fcb 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -454,7 +454,7 @@ def setUp(self): tagging_api.add_tag_to_taxonomy(taxonomy_all_org, tag_value) self.lib_block_tags = ['tag_1', 'tag_5'] - tagging_api.tag_object(str(self.lib_block_key), taxonomy_all_org, ['tag_1', 'tag_5']) + tagging_api.tag_object(str(self.lib_block_key), taxonomy_all_org, self.lib_block_tags) def test_paste_from_library_creates_link(self): """ @@ -495,9 +495,7 @@ def test_paste_from_library_read_only_tags(self): object_tags = tagging_api.get_object_tags(new_block_key) assert len(object_tags) == len(self.lib_block_tags) - print(object_tags) for object_tag in object_tags: - print(object_tag) assert object_tag.value in self.lib_block_tags assert object_tag.is_copied diff --git a/cms/lib/xblock/test/test_upstream_sync.py b/cms/lib/xblock/test/test_upstream_sync.py index 5db020393eab..5596f3eb86a9 100644 --- a/cms/lib/xblock/test/test_upstream_sync.py +++ b/cms/lib/xblock/test/test_upstream_sync.py @@ -13,6 +13,7 @@ ) from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.content_libraries import api as libs +from openedx.core.djangoapps.content_tagging import api as tagging_api from openedx.core.djangoapps.xblock import api as xblock from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory @@ -48,6 +49,18 @@ def setUp(self): upstream.data = "Upstream content V2" upstream.save() + self.taxonomy_all_org = tagging_api.create_taxonomy( + "test_taxonomy", + "Test Taxonomy", + export_id="ALL_ORGS", + ) + tagging_api.set_taxonomy_orgs(self.taxonomy_all_org, all_orgs=True) + for tag_value in ('tag_1', 'tag_2', 'tag_3', 'tag_4', 'tag_5', 'tag_6', 'tag_7'): + tagging_api.add_tag_to_taxonomy(self.taxonomy_all_org, tag_value) + + self.upstream_tags = ['tag_1', 'tag_5'] + tagging_api.tag_object(str(self.upstream_key), self.taxonomy_all_org, self.upstream_tags) + def test_sync_bad_downstream(self): """ Syncing into an unsupported downstream (such as a another Content Library block) raises BadDownstream, but @@ -127,11 +140,19 @@ def test_sync_updates_happy_path(self): assert downstream.display_name == "Upstream Title V2" assert downstream.data == "Upstream content V2" + # Verify tags + object_tags = tagging_api.get_object_tags(str(downstream.location)) + assert len(object_tags) == len(self.upstream_tags) + for object_tag in object_tags: + assert object_tag.value in self.upstream_tags + # Upstream updates upstream = xblock.load_block(self.upstream_key, self.user) upstream.display_name = "Upstream Title V3" upstream.data = "Upstream content V3" upstream.save() + new_upstream_tags = self.upstream_tags + ['tag_2', 'tag_3'] + tagging_api.tag_object(str(self.upstream_key), self.taxonomy_all_org, new_upstream_tags) # Follow-up sync. Assert that updates are pulled into downstream. sync_from_upstream(downstream, self.user) @@ -140,6 +161,12 @@ def test_sync_updates_happy_path(self): assert downstream.display_name == "Upstream Title V3" assert downstream.data == "Upstream content V3" + # Verify tags + object_tags = tagging_api.get_object_tags(str(downstream.location)) + assert len(object_tags) == len(new_upstream_tags) + for object_tag in object_tags: + assert object_tag.value in new_upstream_tags + def test_sync_updates_to_modified_content(self): """ If we sync to modified content, will it preserve customizable fields, but overwrite the rest? @@ -335,3 +362,40 @@ def test_sever_upstream_link(self): # AND, we have recorded the old upstream as our copied_from_block. assert downstream.copied_from_block == str(self.upstream_key) + + def test_sync_library_block_tags(self): + upstream_lib_block_key = libs.create_library_block(self.library.key, "html", "upstream").usage_key + upstream_lib_block = xblock.load_block(upstream_lib_block_key, self.user) + upstream_lib_block.display_name = "Another lib block" + upstream_lib_block.data = "another lib block" + upstream_lib_block.save() + + expected_tags = self.upstream_tags + tagging_api.tag_object(str(upstream_lib_block_key), self.taxonomy_all_org, expected_tags) + + downstream = BlockFactory.create(category='html', parent=self.unit, upstream=str(upstream_lib_block_key)) + + # Initial sync + sync_from_upstream(downstream, self.user) + + # Verify tags + object_tags = tagging_api.get_object_tags(str(downstream.location)) + assert len(object_tags) == len(expected_tags) + for object_tag in object_tags: + assert object_tag.value in expected_tags + + # Upstream updates + upstream_lib_block.display_name = "Upstream Title V3" + upstream_lib_block.data = "Upstream content V3" + upstream_lib_block.save() + new_upstream_tags = self.upstream_tags + ['tag_2', 'tag_3'] + tagging_api.tag_object(str(upstream_lib_block_key), self.taxonomy_all_org, new_upstream_tags) + + # Follow-up sync. + sync_from_upstream(downstream, self.user) + + #Verify tags + object_tags = tagging_api.get_object_tags(str(downstream.location)) + assert len(object_tags) == len(new_upstream_tags) + for object_tag in object_tags: + assert object_tag.value in new_upstream_tags diff --git a/cms/lib/xblock/upstream_sync.py b/cms/lib/xblock/upstream_sync.py index 785cc7dc7e36..fb0199788f12 100644 --- a/cms/lib/xblock/upstream_sync.py +++ b/cms/lib/xblock/upstream_sync.py @@ -28,7 +28,6 @@ if t.TYPE_CHECKING: from django.contrib.auth.models import User # pylint: disable=imported-auth-user - logger = logging.getLogger(__name__) @@ -187,6 +186,7 @@ def sync_from_upstream(downstream: XBlock, user: User) -> None: link, upstream = _load_upstream_link_and_block(downstream, user) _update_customizable_fields(upstream=upstream, downstream=downstream, only_fetch=False) _update_non_customizable_fields(upstream=upstream, downstream=downstream) + _update_tags(upstream=upstream, downstream=downstream) downstream.upstream_version = link.version_available @@ -284,6 +284,22 @@ def _update_non_customizable_fields(*, upstream: XBlock, downstream: XBlock) -> setattr(downstream, field_name, new_upstream_value) +def _update_tags(*, upstream: XBlock, downstream: XBlock) -> None: + """ + Update tags from `upstream` to `downstream` + """ + from openedx.core.djangoapps.content_tagging.api import copy_object_tags, copy_tags_as_read_only + if isinstance(upstream.location, LibraryUsageLocatorV2): + # If is a library component, then update the tags as read_only + # This keeps tags added locally. + copy_tags_as_read_only( + str(upstream.location), + str(downstream.location), + ) + else: + copy_object_tags(upstream.location, downstream.location) + + def _get_synchronizable_fields(upstream: XBlock, downstream: XBlock) -> set[str]: """ The syncable fields are the ones which are content- or settings-scoped AND are defined on both (up,down)stream. From 441895103b94d7dc8e4d00621ce27afe8b908991 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 17 Oct 2024 21:05:53 -0500 Subject: [PATCH 12/15] style: Nits on the code --- cms/djangoapps/contentstore/helpers.py | 2 ++ .../contentstore/views/tests/test_clipboard_paste.py | 3 +-- cms/lib/xblock/upstream_sync.py | 3 ++- .../djangoapps/content_tagging/rest_api/v1/tests/test_views.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index 05c79961c13d..841b32ded1e8 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -324,6 +324,8 @@ def _import_xml_node_to_parent( Given an XML node representing a serialized XBlock (OLX), import it into modulestore 'store' as a child of the specified parent block. Recursively copy children as needed. """ + # pylint: disable=too-many-statements + runtime = parent_xblock.runtime parent_key = parent_xblock.scope_ids.usage_id block_type = node.tag diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 0f2ec95f7fcb..a864b29025e0 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -490,9 +490,8 @@ def test_paste_from_library_read_only_tags(self): }, format="json") assert paste_response.status_code == 200 - new_block_key = paste_response.json()["locator"] - + 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: diff --git a/cms/lib/xblock/upstream_sync.py b/cms/lib/xblock/upstream_sync.py index fb0199788f12..fa8e7bce62ee 100644 --- a/cms/lib/xblock/upstream_sync.py +++ b/cms/lib/xblock/upstream_sync.py @@ -28,6 +28,7 @@ if t.TYPE_CHECKING: from django.contrib.auth.models import User # pylint: disable=imported-auth-user + logger = logging.getLogger(__name__) @@ -298,7 +299,7 @@ def _update_tags(*, upstream: XBlock, downstream: XBlock) -> None: ) else: copy_object_tags(upstream.location, downstream.location) - + def _get_synchronizable_fields(upstream: XBlock, downstream: XBlock) -> set[str]: """ diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py index a5186ee26781..463ea42d08e9 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py @@ -1857,7 +1857,7 @@ def test_get_copied_tags(self): object_id_2 = str(self.courseB) tagging_api.tag_object(object_id=object_id_1, taxonomy=self.t1, tags=["android"]) tagging_api.tag_object(object_id=object_id_2, taxonomy=self.t1, tags=["anvil"]) - tagging_api.copy_tags(object_id_1, object_id_2) + tagging_api.copy_tags_as_read_only(object_id_1, object_id_2) expected_tags = [{ 'name': self.t1.name, From 845f124a85a59ca34cde37bf36c94656cdc0c90f Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Sat, 19 Oct 2024 13:03:58 -0500 Subject: [PATCH 13/15] refactor: Sync upstream read-only tags with all blocks --- cms/djangoapps/contentstore/helpers.py | 32 ++++++++++---------------- cms/lib/xblock/upstream_sync.py | 17 ++++++-------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index 841b32ded1e8..d928d8bc73a6 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -430,29 +430,21 @@ def _import_xml_node_to_parent( tags=tags, ) - def copy_tags(): - if copied_from_block and tags: - object_tags = tags.get(str(copied_from_block)) - if object_tags: - content_tagging_api.set_all_object_tags( - content_key=new_xblock.location, - object_tags=object_tags, - ) - # Copy content tags to the new xblock if new_xblock.upstream: - # Verify if the upstream is a library component - # Copy the tags from library component upstream as ready only - try: - LibraryUsageLocatorV2.from_string(new_xblock.upstream) - content_tagging_api.copy_tags_as_read_only( - new_xblock.upstream, - new_xblock.location, + # If this block is synced from an upstream (e.g. library content), + # copy the tags from the upstream as ready-only + content_tagging_api.copy_tags_as_read_only( + new_xblock.upstream, + new_xblock.location, + ) + elif copied_from_block and tags: + object_tags = tags.get(str(copied_from_block)) + if object_tags: + content_tagging_api.set_all_object_tags( + content_key=new_xblock.location, + object_tags=object_tags, ) - except InvalidKeyError: - copy_tags() - else: - copy_tags() return new_xblock diff --git a/cms/lib/xblock/upstream_sync.py b/cms/lib/xblock/upstream_sync.py index 280610f0d167..2b1082fa7aeb 100644 --- a/cms/lib/xblock/upstream_sync.py +++ b/cms/lib/xblock/upstream_sync.py @@ -290,16 +290,13 @@ def _update_tags(*, upstream: XBlock, downstream: XBlock) -> None: """ Update tags from `upstream` to `downstream` """ - from openedx.core.djangoapps.content_tagging.api import copy_object_tags, copy_tags_as_read_only - if isinstance(upstream.location, LibraryUsageLocatorV2): - # If is a library component, then update the tags as read_only - # This keeps tags added locally. - copy_tags_as_read_only( - str(upstream.location), - str(downstream.location), - ) - else: - copy_object_tags(upstream.location, downstream.location) + from openedx.core.djangoapps.content_tagging.api import copy_tags_as_read_only + # For any block synced with an upstream, copy the tags as read_only + # This keeps tags added locally. + copy_tags_as_read_only( + str(upstream.location), + str(downstream.location), + ) def _get_synchronizable_fields(upstream: XBlock, downstream: XBlock) -> set[str]: From 7219d2ee09a8c4a976c687f706b9e61379476bcd Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Sat, 19 Oct 2024 13:04:22 -0500 Subject: [PATCH 14/15] style: Nits on the code --- cms/djangoapps/contentstore/helpers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index d928d8bc73a6..bc8f03f186bf 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -11,9 +11,8 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.utils.translation import gettext as _ -from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import AssetKey, CourseKey, UsageKey -from opaque_keys.edx.locator import DefinitionLocator, LocalId, LibraryUsageLocatorV2 +from opaque_keys.edx.locator import DefinitionLocator, LocalId from xblock.core import XBlock from xblock.fields import ScopeIds from xblock.runtime import IdGenerator From 6ce459b312c2ff809444b8bee3b1fc7adc095928 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Sat, 19 Oct 2024 13:19:51 -0500 Subject: [PATCH 15/15] fix: Broken tests --- cms/lib/xblock/test/test_upstream_sync.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cms/lib/xblock/test/test_upstream_sync.py b/cms/lib/xblock/test/test_upstream_sync.py index f219bf452297..cc3d661ca6e9 100644 --- a/cms/lib/xblock/test/test_upstream_sync.py +++ b/cms/lib/xblock/test/test_upstream_sync.py @@ -392,6 +392,8 @@ def test_sync_library_block_tags(self): upstream_lib_block.data = "another lib block" upstream_lib_block.save() + libs.publish_changes(self.library.key, self.user.id) + expected_tags = self.upstream_tags tagging_api.tag_object(str(upstream_lib_block_key), self.taxonomy_all_org, expected_tags)