From 8b999e1263231496147dffd070b1e1db106fb6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 22 Feb 2024 18:26:56 -0300 Subject: [PATCH 01/23] feat: paste tags when pasting xblocks with tag data --- cms/djangoapps/contentstore/helpers.py | 8 ++ .../views/tests/test_clipboard_paste.py | 74 +++++++++++++++++++ cms/lib/xblock/tagging/tagged_block_mixin.py | 24 +++++- .../core/djangoapps/content_tagging/api.py | 27 ++++++- .../core/djangoapps/content_tagging/rules.py | 14 +--- .../core/djangoapps/content_tagging/types.py | 2 + .../core/djangoapps/content_tagging/utils.py | 39 ++++++++-- 7 files changed, 170 insertions(+), 18 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index 9fd09193b59f..5f9ea300a64f 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -345,6 +345,14 @@ def _import_xml_node_to_parent( else: for child_node in child_nodes: _import_xml_node_to_parent(child_node, new_xblock, store, user_id=user_id) + + # ToDo: Check the better place to call it + # I tried to call it in the xml_block.py in the parse_xml() function, + # but the usage_key is not persited yet there + from cms.lib.xblock.tagging.tagged_block_mixin import TaggedBlockMixin + if isinstance(new_xblock, TaggedBlockMixin): + new_xblock.add_tags_from_xml() + return new_xblock diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 011c9a10e561..7f439a7dcd21 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -7,6 +7,7 @@ from django.test import LiveServerTestCase 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 @@ -15,6 +16,7 @@ from cms.djangoapps.contentstore.utils import reverse_usage_url from openedx.core.lib.blockstore_api.tests.base import BlockstoreAppTestMixin from openedx.core.djangoapps.content_libraries import api as library_api +from openedx.core.djangoapps.content_tagging import api as tagging_api from blockstore.apps import api as blockstore_api CLIPBOARD_ENDPOINT = "/api/content-staging/v1/clipboard/" @@ -144,6 +146,78 @@ def test_copy_and_paste_component(self, block_args): # 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") + + 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") + # Removing a tag is causing tag_object to fail + # tag_removed = Tag.objects.create(taxonomy=taxonomy, value="tag_removed") + tagging_api.tag_object( + object_id=str(unit_key), + taxonomy=taxonomy_all_org, + # tags=["tag_1", "tag_2", "tag_removed"], + 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"], + ) + all_org_tags = 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 + + # tag_removed.delete() + 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]) == ' ' \ + 'block-v1:org.1+course_1+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_1' + assert str(tags[1]) == ' ' \ + 'block-v1:org.1+course_1+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_2' + + def test_paste_with_assets(self): """ When pasting into a different course, any required static assets should diff --git a/cms/lib/xblock/tagging/tagged_block_mixin.py b/cms/lib/xblock/tagging/tagged_block_mixin.py index dba1a16c8856..e062f911397a 100644 --- a/cms/lib/xblock/tagging/tagged_block_mixin.py +++ b/cms/lib/xblock/tagging/tagged_block_mixin.py @@ -1,5 +1,5 @@ # lint-amnesty, pylint: disable=missing-module-docstring -from urllib.parse import quote +from urllib.parse import quote, unquote class TaggedBlockMixin: @@ -55,3 +55,25 @@ def add_xml_to_node(self, node): """ super().add_xml_to_node(node) self.add_tags_to_node(node) + + def add_tags_from_xml(self): + """ + Parse and add tag data from xml + """ + # 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.xml_attributes.get('tags-v1', None) if self.xml_attributes else None + 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) diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index 4f62469220a5..457f1b4d7cd7 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -12,7 +12,8 @@ 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( @@ -166,6 +167,30 @@ def get_all_object_tags( 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 diff --git a/openedx/core/djangoapps/content_tagging/rules.py b/openedx/core/djangoapps/content_tagging/rules.py index a89e3618d715..863a5baaa1b1 100644 --- a/openedx/core/djangoapps/content_tagging/rules.py +++ b/openedx/core/djangoapps/content_tagging/rules.py @@ -20,10 +20,9 @@ ) 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 diff --git a/openedx/core/djangoapps/content_tagging/types.py b/openedx/core/djangoapps/content_tagging/types.py index 685df7b3afb3..cd5d2aeed33a 100644 --- a/openedx/core/djangoapps/content_tagging/types.py +++ b/openedx/core/djangoapps/content_tagging/types.py @@ -10,7 +10,9 @@ 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]] diff --git a/openedx/core/djangoapps/content_tagging/utils.py b/openedx/core/djangoapps/content_tagging/utils.py index 3d7c340162da..450bbe2637f6 100644 --- a/openedx/core/djangoapps/content_tagging/utils.py +++ b/openedx/core/djangoapps/content_tagging/utils.py @@ -7,11 +7,13 @@ 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 + Get context key from an 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 @@ def __init__(self): """ 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]: """ @@ -100,3 +126,6 @@ def get_library_orgs(self, user, org_names: list[str]) -> list[Organization]: return [ library_orgs[org_name] for org_name in org_names if org_name in library_orgs ] + + +rules_cache = TaggingRulesCache() From 4b895dee692d2575c776d6910292902fb9304d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 23 Feb 2024 17:02:51 -0300 Subject: [PATCH 02/23] fix: pylint --- cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index a60e69f19662..5746a3bab83d 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -190,7 +190,6 @@ def test_copy_and_paste_unit_with_tags(self): 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 @@ -214,7 +213,6 @@ def test_copy_and_paste_unit_with_tags(self): assert str(tags[1]) == ' ' \ 'block-v1:org.1+course_1+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_2' - def test_paste_with_assets(self): """ When pasting into a different course, any required static assets should From 905f711c012fecb35ec02c7057180a1a55ccd405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 23 Feb 2024 19:48:18 -0300 Subject: [PATCH 03/23] chore: temp requirements --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/kernel.in | 3 ++- requirements/edx/testing.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 984440da48d0..7ec8c4006113 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -785,7 +785,7 @@ openedx-filters==1.6.0 # via # -r requirements/edx/kernel.in # lti-consumer-xblock -openedx-learning==0.6.2 +openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index fcd82da8e190..35a5c947a457 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1307,7 +1307,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 @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 20ce9f32cfa8..a9ededf42ce0 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -920,7 +920,7 @@ openedx-filters==1.6.0 # via # -r requirements/edx/base.txt # lti-consumer-xblock -openedx-learning==0.6.2 +openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 3139561ff819..8e1279664bab 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -117,7 +117,8 @@ 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) +# openedx-learning # Open edX Learning core (experimental) +openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function openedx-mongodbproxy openedx-django-wiki openedx-blockstore diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 4319294d232c..46f3274e7a38 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -977,7 +977,7 @@ openedx-filters==1.6.0 # via # -r requirements/edx/base.txt # lti-consumer-xblock -openedx-learning==0.6.2 +openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt From 77f95cbe11b5c2d03f6490aaafb7ac347e6352c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 23 Feb 2024 22:21:35 -0300 Subject: [PATCH 04/23] fix: pylint --- openedx/core/djangoapps/content_tagging/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index 457f1b4d7cd7..ebb5c376d346 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -157,11 +157,13 @@ 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 From 9ac16fa7c72c972fe4b60e7b28f2ebc7a842a689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Sat, 24 Feb 2024 10:48:18 -0300 Subject: [PATCH 05/23] test: fix objecttag str --- .../contentstore/views/tests/test_clipboard_paste.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 5746a3bab83d..6c6759605146 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -179,7 +179,7 @@ def test_copy_and_paste_unit_with_tags(self): taxonomy=taxonomy_all_org_removed, tags=["tag_1", "tag_2"], ) - all_org_tags = tagging_api.get_object_tags(str(unit_key)) + 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") @@ -209,9 +209,9 @@ def test_copy_and_paste_unit_with_tags(self): tags = list(tagging_api.get_object_tags(str(dest_unit_key))) assert len(tags) == 2 assert str(tags[0]) == ' ' \ - 'block-v1:org.1+course_1+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_1' + 'block-v1:org.2025+course_2025+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_1' assert str(tags[1]) == ' ' \ - 'block-v1:org.1+course_1+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_2' + 'block-v1:org.2025+course_2025+Destination_Course+type@vertical+block@vertical1: test_taxonomy=tag_2' def test_paste_with_assets(self): """ From f9c82a8043e8117e83144fe19db3d46da6ccd169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Mon, 26 Feb 2024 15:58:14 -0300 Subject: [PATCH 06/23] refactor: parse data from xml --- cms/lib/xblock/tagging/tagged_block_mixin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cms/lib/xblock/tagging/tagged_block_mixin.py b/cms/lib/xblock/tagging/tagged_block_mixin.py index e062f911397a..5c536f480994 100644 --- a/cms/lib/xblock/tagging/tagged_block_mixin.py +++ b/cms/lib/xblock/tagging/tagged_block_mixin.py @@ -49,22 +49,22 @@ def add_tags_to_node(self, node): if tag_data: node.set('tags-v1', tag_data) - def add_xml_to_node(self, node): + def read_tags_from_node(self, node): """ - Include the serialized tag data in XML when adding to node + Deserialize and read tag data from node """ - super().add_xml_to_node(node) - self.add_tags_to_node(node) + if 'tags-v1' in node.attrib: + self.xml_attributes['tags-v1'] = str(node.attrib['tags-v1']) def add_tags_from_xml(self): """ - Parse and add tag data from xml + Parse and add tag data from xml_attributes """ # 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.xml_attributes.get('tags-v1', None) if self.xml_attributes else None + tag_data = self.xml_attributes.get('tags-v1', None) if hasattr(self, 'xml_attributes') else None if not tag_data: return From 6f76a82fafc5ea459870ca607d1f701bbcdddf90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 27 Feb 2024 09:38:21 -0300 Subject: [PATCH 07/23] fix: changes from review --- .../contentstore/views/tests/test_clipboard_paste.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index 6c6759605146..12f620b41e14 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -157,16 +157,12 @@ def test_copy_and_paste_unit_with_tags(self): # Add tags to the unit taxonomy_all_org = tagging_api.create_taxonomy("test_taxonomy", "Test Taxonomy") - 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") - # Removing a tag is causing tag_object to fail - # tag_removed = Tag.objects.create(taxonomy=taxonomy, value="tag_removed") tagging_api.tag_object( object_id=str(unit_key), taxonomy=taxonomy_all_org, - # tags=["tag_1", "tag_2", "tag_removed"], tags=["tag_1", "tag_2"], ) @@ -194,7 +190,6 @@ def test_copy_and_paste_unit_with_tags(self): copy_response = client.post(CLIPBOARD_ENDPOINT, {"usage_key": str(unit_key)}, format="json") assert copy_response.status_code == 200 - # tag_removed.delete() taxonomy_all_org_removed.delete() # Paste the unit From f1c806f51734b79ee026111f65f239d937fd3eda Mon Sep 17 00:00:00 2001 From: Jillian Date: Thu, 29 Feb 2024 14:23:30 +1030 Subject: [PATCH 08/23] feat: duplicate tags when duplicating a tagged XBlock. (#637) * feat: duplicate tags when duplicating a tagged XBlock. * test: fixes flaky test --- .../contentstore/views/tests/test_block.py | 52 +++++++++++++++++++ .../views/tests/test_clipboard_paste.py | 6 +-- cms/lib/xblock/tagging/tagged_block_mixin.py | 15 +++++- .../core/djangoapps/content_tagging/api.py | 1 + 4 files changed, 69 insertions(+), 5 deletions(-) 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 From 74d907ad7afb3db57ac4e1cd15f496dbdcdda076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 29 Feb 2024 10:19:30 -0300 Subject: [PATCH 09/23] chore: update openedx-learning version --- requirements/constraints.txt | 4 ++-- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/kernel.in | 3 +-- requirements/edx/testing.txt | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 11f9834b517f..ed3573fb9571 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -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 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 7ec8c4006113..96d10db80f56 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -785,7 +785,7 @@ openedx-filters==1.6.0 # via # -r requirements/edx/kernel.in # lti-consumer-xblock -openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function +openedx-learning==0.6.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 35a5c947a457..ac888f6c310b 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1307,7 +1307,7 @@ openedx-filters==1.6.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # lti-consumer-xblock -openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function +openedx-learning==0.6.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 a9ededf42ce0..2cf8207e3c30 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -920,7 +920,7 @@ openedx-filters==1.6.0 # via # -r requirements/edx/base.txt # lti-consumer-xblock -openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function +openedx-learning==0.6.3 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 8e1279664bab..3139561ff819 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -117,8 +117,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) -openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function +openedx-learning # Open edX Learning core (experimental) openedx-mongodbproxy openedx-django-wiki openedx-blockstore diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 46f3274e7a38..cebcd7ad9af6 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -977,7 +977,7 @@ openedx-filters==1.6.0 # via # -r requirements/edx/base.txt # lti-consumer-xblock -openedx-learning @ git+https://github.com/open-craft/openedx-learning@rpenido/fal-3675-add-get-taxonomy-by-external-id-function +openedx-learning==0.6.3 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt From 2eb02303b17725cf2f5908b6b20658e0711d6d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 29 Feb 2024 10:30:32 -0300 Subject: [PATCH 10/23] fix: removing temp comment --- cms/djangoapps/contentstore/helpers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index 5f9ea300a64f..e30e42add3e4 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -346,9 +346,6 @@ def _import_xml_node_to_parent( for child_node in child_nodes: _import_xml_node_to_parent(child_node, new_xblock, store, user_id=user_id) - # ToDo: Check the better place to call it - # I tried to call it in the xml_block.py in the parse_xml() function, - # but the usage_key is not persited yet there from cms.lib.xblock.tagging.tagged_block_mixin import TaggedBlockMixin if isinstance(new_xblock, TaggedBlockMixin): new_xblock.add_tags_from_xml() From dc2b973bb24fe6936692dde039ebf92a6873a591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 29 Feb 2024 12:13:16 -0300 Subject: [PATCH 11/23] test: fix tag_object param order --- cms/djangoapps/contentstore/views/tests/test_block.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index 742db9f5bc20..dd85719fc942 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -1129,11 +1129,11 @@ def test_duplicate_tags(self): 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)) + 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(taxonomyA, ["two"], str(source_block.location)) + tagging_api.tag_object(str(source_block.location), taxonomyA, ["two"],) # Refresh. source_chapter = self.store.get_item(source_chapter.location) From 59e37db27218581da96f4e58f5952f1e10972357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 29 Feb 2024 14:27:36 -0300 Subject: [PATCH 12/23] chore: update ora2 version --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 96d10db80f56..c3164a473911 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -795,7 +795,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/bundled.in -ora2==6.0.34 +ora2==6.2.0 # via -r requirements/edx/bundled.in packaging==23.2 # via diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index ac888f6c310b..595de07a4d3b 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1321,7 +1321,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.2.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 2cf8207e3c30..f0ec2c633416 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -930,7 +930,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -ora2==6.0.34 +ora2==6.2.0 # via -r requirements/edx/base.txt packaging==23.2 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index cebcd7ad9af6..738dfb944052 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -987,7 +987,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -ora2==6.0.34 +ora2==6.2.0 # via -r requirements/edx/base.txt packaging==23.2 # via From 15288dd7714eef8666802690a87201f559ca1d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 29 Feb 2024 14:45:43 -0300 Subject: [PATCH 13/23] chore: fix inconsistent python dependencies --- requirements/common_constraints.txt | 2 +- requirements/edx/base.txt | 1 + requirements/edx/development.txt | 1 + requirements/edx/doc.txt | 1 + requirements/edx/testing.txt | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index 7313473a1675..54348c7a58fa 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -17,7 +17,7 @@ # using LTS django version - +Django<5.0 # elasticsearch>=7.14.0 includes breaking changes in it which caused issues in discovery upgrade process. # elastic search changelog: https://www.elastic.co/guide/en/enterprise-search/master/release-notes-7.14.0.html diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index c247c840575a..09fdd3fab08b 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -174,6 +174,7 @@ defusedxml==0.7.1 # social-auth-core django==4.2.10 # via + # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in # django-appconf diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 9ed3a709abc7..3b75f2604683 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -336,6 +336,7 @@ distlib==0.3.8 # virtualenv django==4.2.10 # via + # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index a075455b9f2b..c6fb31d2dcea 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -220,6 +220,7 @@ defusedxml==0.7.1 # social-auth-core django==4.2.10 # via + # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # django-appconf diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index a2c92050bdbb..1ddb58ad6ed5 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -251,6 +251,7 @@ distlib==0.3.8 # via virtualenv django==4.2.10 # via + # -c requirements/edx/../common_constraints.txt # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt # django-appconf From dacba7d47740900d4536bd11cca856ff862dd7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 29 Feb 2024 15:18:02 -0300 Subject: [PATCH 14/23] chore: temp update xblock-drag-and-drop-v2 version --- requirements/edx/base.txt | 2 +- requirements/edx/bundled.in | 3 ++- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 09fdd3fab08b..985f00154d52 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -1222,7 +1222,7 @@ xblock[django]==1.10.0 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2==3.4.0 +xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data # via -r requirements/edx/bundled.in xblock-google-drive==0.6.1 # via -r requirements/edx/bundled.in diff --git a/requirements/edx/bundled.in b/requirements/edx/bundled.in index b4685a3a2cbe..7931dd688900 100644 --- a/requirements/edx/bundled.in +++ b/requirements/edx/bundled.in @@ -46,5 +46,6 @@ staff-graded-xblock # https://github.com/openedx/staff_graded-xb edx-sga # The more well known "staff graded assignment" XBlock, from MIT. ora2>=4.5.0 # Open Response Assessment XBlock xblock-poll # Xblock for polling users -xblock-drag-and-drop-v2 # Drag and Drop XBlock +# xblock-drag-and-drop-v2 # Drag and Drop XBlock +xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data xblock-google-drive # XBlock for google docs and calendar diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 3b75f2604683..90010223db62 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -2187,7 +2187,7 @@ xblock[django]==1.10.0 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2==3.4.0 +xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index c6fb31d2dcea..727d8d03e724 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1481,7 +1481,7 @@ xblock[django]==1.10.0 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2==3.4.0 +xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data # via -r requirements/edx/base.txt xblock-google-drive==0.6.1 # via -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 1ddb58ad6ed5..4dc8743e1d9e 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1605,7 +1605,7 @@ xblock[django]==1.10.0 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2==3.4.0 +xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data # via -r requirements/edx/base.txt xblock-google-drive==0.6.1 # via -r requirements/edx/base.txt From c1fed5ed7e0bf787c639d7e8192384c8af30914f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 29 Feb 2024 18:07:44 -0300 Subject: [PATCH 15/23] Revert "chore: temp update xblock-drag-and-drop-v2 version" This reverts commit dacba7d47740900d4536bd11cca856ff862dd7e7. --- requirements/edx/base.txt | 2 +- requirements/edx/bundled.in | 3 +-- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 4d379f5f8835..9add4de4fb70 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -1222,7 +1222,7 @@ xblock[django]==1.10.0 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data +xblock-drag-and-drop-v2==3.4.0 # via -r requirements/edx/bundled.in xblock-google-drive==0.6.1 # via -r requirements/edx/bundled.in diff --git a/requirements/edx/bundled.in b/requirements/edx/bundled.in index 7931dd688900..b4685a3a2cbe 100644 --- a/requirements/edx/bundled.in +++ b/requirements/edx/bundled.in @@ -46,6 +46,5 @@ staff-graded-xblock # https://github.com/openedx/staff_graded-xb edx-sga # The more well known "staff graded assignment" XBlock, from MIT. ora2>=4.5.0 # Open Response Assessment XBlock xblock-poll # Xblock for polling users -# xblock-drag-and-drop-v2 # Drag and Drop XBlock -xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data +xblock-drag-and-drop-v2 # Drag and Drop XBlock xblock-google-drive # XBlock for google docs and calendar diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index b204b99d9dcc..68bdd60aa58a 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -2187,7 +2187,7 @@ xblock[django]==1.10.0 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data +xblock-drag-and-drop-v2==3.4.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 96946a9fd574..50097900a953 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1481,7 +1481,7 @@ xblock[django]==1.10.0 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data +xblock-drag-and-drop-v2==3.4.0 # via -r requirements/edx/base.txt xblock-google-drive==0.6.1 # via -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index b3add2047625..e1275665f5b4 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1605,7 +1605,7 @@ xblock[django]==1.10.0 # xblock-google-drive # xblock-poll # xblock-utils -xblock-drag-and-drop-v2 @ git+https://github.com/open-craft/xblock-drag-and-drop-v2@rpenido/fal-3621-paste-tags-when-pasting-xblocks-with-tag-data +xblock-drag-and-drop-v2==3.4.0 # via -r requirements/edx/base.txt xblock-google-drive==0.6.1 # via -r requirements/edx/base.txt From 2ec5afa22e1872ecb2a1956eb3907a1b29985243 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 1 Mar 2024 14:59:45 +1030 Subject: [PATCH 16/23] fix: store tags in TaggedBlockMixin.tags_v1 field Previously, we relied on the presence of the xml_attributes field, which is added by the XMLMixin. However, XMLMixin is not part of the default XBLOCK_MIXINS, so custom XBlocks weren't able to copy/paste tags. --- cms/djangoapps/contentstore/helpers.py | 1 + cms/lib/xblock/tagging/tagged_block_mixin.py | 26 ++++++++++++++++--- .../lib/xblock_serializer/block_serializer.py | 5 ++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index e30e42add3e4..08e63a0fd62d 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -348,6 +348,7 @@ def _import_xml_node_to_parent( from cms.lib.xblock.tagging.tagged_block_mixin import TaggedBlockMixin if isinstance(new_xblock, TaggedBlockMixin): + new_xblock.read_tags_from_node(node) new_xblock.add_tags_from_xml() return new_xblock diff --git a/cms/lib/xblock/tagging/tagged_block_mixin.py b/cms/lib/xblock/tagging/tagged_block_mixin.py index da2ef3c8b57f..ce203b2286d1 100644 --- a/cms/lib/xblock/tagging/tagged_block_mixin.py +++ b/cms/lib/xblock/tagging/tagged_block_mixin.py @@ -3,11 +3,29 @@ """ from urllib.parse import quote, unquote +from xblock.fields import Scope, String + + +def _(text): + """ + A noop underscore function that marks strings for extraction. + """ + return text + class TaggedBlockMixin: """ Mixin containing XML serializing and parsing functionality for tagged blocks """ + + tags_v1 = String( + display_name=_("Tags v1"), + name="tags-v1", + help=_("Serialized content tags"), + default="", + scope=Scope.settings + ) + def studio_post_duplicate(self, store, source_item): """ Duplicates content tags from the source_item. @@ -17,7 +35,7 @@ def studio_post_duplicate(self, store, source_item): if hasattr(source_item, 'serialize_tag_data'): tags = source_item.serialize_tag_data() - self.xml_attributes['tags-v1'] = tags + self.tags_v1 = tags self.add_tags_from_xml() def serialize_tag_data(self): @@ -67,17 +85,17 @@ def read_tags_from_node(self, node): Deserialize and read tag data from node """ if 'tags-v1' in node.attrib: - self.xml_attributes['tags-v1'] = str(node.attrib['tags-v1']) + self.tags_v1 = str(node.attrib['tags-v1']) def add_tags_from_xml(self): """ - Parse and add tag data from xml_attributes + Parse and add tag data from tags_v1 field """ # 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.xml_attributes.get('tags-v1', None) if hasattr(self, 'xml_attributes') else None + tag_data = self.tags_v1 if not tag_data: return diff --git a/openedx/core/lib/xblock_serializer/block_serializer.py b/openedx/core/lib/xblock_serializer/block_serializer.py index deb46b07cecb..52288649aba6 100644 --- a/openedx/core/lib/xblock_serializer/block_serializer.py +++ b/openedx/core/lib/xblock_serializer/block_serializer.py @@ -89,6 +89,11 @@ def _serialize_normal_block(self, block) -> etree.Element: 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 From b98e2f8d4e663ce6f3672b9ab875c5aa27e95437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 1 Mar 2024 10:41:25 -0300 Subject: [PATCH 17/23] chore: update ora2 to reverted version --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 9add4de4fb70..873ddcff10d0 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -796,7 +796,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/bundled.in -ora2==6.2.0 +ora2==6.3.0 # via -r requirements/edx/bundled.in packaging==23.2 # via diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 68bdd60aa58a..2bc0e9a10799 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.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.2.0 +ora2==6.3.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 50097900a953..af811148ef3a 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -931,7 +931,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -ora2==6.2.0 +ora2==6.3.0 # via -r requirements/edx/base.txt packaging==23.2 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index e1275665f5b4..48dc00116124 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -988,7 +988,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -ora2==6.2.0 +ora2==6.3.0 # via -r requirements/edx/base.txt packaging==23.2 # via From a3b181ef62d709f74584f62b0aadcd71995de69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 1 Mar 2024 10:41:54 -0300 Subject: [PATCH 18/23] style: rename add_tags_from_xml() to add_tags_from_field() --- cms/djangoapps/contentstore/helpers.py | 2 +- cms/lib/xblock/tagging/tagged_block_mixin.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index 08e63a0fd62d..a1a2030f1b15 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -349,7 +349,7 @@ def _import_xml_node_to_parent( from cms.lib.xblock.tagging.tagged_block_mixin import TaggedBlockMixin if isinstance(new_xblock, TaggedBlockMixin): new_xblock.read_tags_from_node(node) - new_xblock.add_tags_from_xml() + new_xblock.add_tags_from_field() return new_xblock diff --git a/cms/lib/xblock/tagging/tagged_block_mixin.py b/cms/lib/xblock/tagging/tagged_block_mixin.py index ce203b2286d1..cdfe1e31f55e 100644 --- a/cms/lib/xblock/tagging/tagged_block_mixin.py +++ b/cms/lib/xblock/tagging/tagged_block_mixin.py @@ -36,7 +36,7 @@ def studio_post_duplicate(self, store, source_item): if hasattr(source_item, 'serialize_tag_data'): tags = source_item.serialize_tag_data() self.tags_v1 = tags - self.add_tags_from_xml() + self.add_tags_from_field() def serialize_tag_data(self): """ @@ -87,7 +87,7 @@ def read_tags_from_node(self, node): if 'tags-v1' in node.attrib: self.tags_v1 = str(node.attrib['tags-v1']) - def add_tags_from_xml(self): + def add_tags_from_field(self): """ Parse and add tag data from tags_v1 field """ From e0fb8e9513d8d6d85930a9be552c8e5bd524dccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 1 Mar 2024 11:08:50 -0300 Subject: [PATCH 19/23] chore: temp ora2 update --- requirements/edx/base.txt | 2 +- requirements/edx/bundled.in | 3 ++- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 873ddcff10d0..6f07c9862b35 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -796,7 +796,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/bundled.in -ora2==6.3.0 +ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes # via -r requirements/edx/bundled.in packaging==23.2 # via diff --git a/requirements/edx/bundled.in b/requirements/edx/bundled.in index b4685a3a2cbe..eab7846277db 100644 --- a/requirements/edx/bundled.in +++ b/requirements/edx/bundled.in @@ -44,7 +44,8 @@ done-xblock # a very simple XBlock that allows learners recommender-xblock # https://github.com/edx/RecommenderXBlock staff-graded-xblock # https://github.com/openedx/staff_graded-xblock Allows off-site bulk scoring. edx-sga # The more well known "staff graded assignment" XBlock, from MIT. -ora2>=4.5.0 # Open Response Assessment XBlock +# ora2>=4.5.0 # Open Response Assessment XBlock +ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes xblock-poll # Xblock for polling users xblock-drag-and-drop-v2 # Drag and Drop XBlock xblock-google-drive # XBlock for google docs and calendar diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 2bc0e9a10799..fd114e017f59 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.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.3.0 +ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index af811148ef3a..6b304bf66eb2 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -931,7 +931,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -ora2==6.3.0 +ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes # via -r requirements/edx/base.txt packaging==23.2 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 48dc00116124..c9c7790d00e4 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -988,7 +988,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -ora2==6.3.0 +ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes # via -r requirements/edx/base.txt packaging==23.2 # via From 3166daf9e17077f6f0e72a3e5fd13d518210ce31 Mon Sep 17 00:00:00 2001 From: Jillian Date: Wed, 6 Mar 2024 23:50:26 +1030 Subject: [PATCH 20/23] refactor: TaggedBlockMixin (#642) * refactor: TaggedBlockMixin overrides add_xml_to_node so we can use inheritance to include tags in exported blocks * refactor: store tags_v1 in an internal field instead of an XBlock field, because we don't want this information persisted to the modulestore. * refactor: adds studio_post_paste handler for use after copy/pasting content from the clipboard. * fix: make serialize_tag_data a classmethod so we can serialize tags for blocks that aren't officially TaggedXBlocks --- cms/djangoapps/contentstore/helpers.py | 21 ++--- .../contentstore/views/tests/test_block.py | 8 +- cms/lib/xblock/tagging/tagged_block_mixin.py | 94 ++++++++++++------- xmodule/library_content_block.py | 13 +++ xmodule/studio_editable.py | 15 ++- xmodule/xml_block.py | 6 -- 6 files changed, 97 insertions(+), 60 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index a1a2030f1b15..6ecfe9b6d066 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -17,7 +17,6 @@ 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 @@ -337,20 +336,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) - from cms.lib.xblock.tagging.tagged_block_mixin import TaggedBlockMixin - if isinstance(new_xblock, TaggedBlockMixin): - new_xblock.read_tags_from_node(node) - new_xblock.add_tags_from_field() - return new_xblock diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index dd85719fc942..c00300bd57e9 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -1138,11 +1138,11 @@ def test_duplicate_tags(self): # 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 + assert source_chapter.serialize_tag_data(source_chapter.location) == 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 + assert source_block.serialize_tag_data(source_block.location) == expected_block_tags # Duplicate the chapter (and its children) dupe_location = duplicate_block( @@ -1155,8 +1155,8 @@ def test_duplicate_tags(self): 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 + assert dupe_chapter.serialize_tag_data(dupe_chapter.location) == expected_chapter_tags + assert dupe_block.serialize_tag_data(dupe_block.location) == expected_block_tags @ddt.ddt diff --git a/cms/lib/xblock/tagging/tagged_block_mixin.py b/cms/lib/xblock/tagging/tagged_block_mixin.py index cdfe1e31f55e..f16a065f74a9 100644 --- a/cms/lib/xblock/tagging/tagged_block_mixin.py +++ b/cms/lib/xblock/tagging/tagged_block_mixin.py @@ -3,44 +3,38 @@ """ from urllib.parse import quote, unquote -from xblock.fields import Scope, String - - -def _(text): - """ - A noop underscore function that marks strings for extraction. - """ - return text - class TaggedBlockMixin: """ Mixin containing XML serializing and parsing functionality for tagged blocks """ - tags_v1 = String( - display_name=_("Tags v1"), - name="tags-v1", - help=_("Serialized content tags"), - default="", - scope=Scope.settings - ) + def __init__(self, *args, **kwargs): + """ + 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 = "" - def studio_post_duplicate(self, store, source_item): + @property + def tags_v1(self) -> str: """ - Duplicates content tags from the source_item. + Returns the serialized tags. """ - if hasattr(super(), 'studio_post_duplicate'): - super().studio_post_duplicate() + return self._tags_v1 - if hasattr(source_item, 'serialize_tag_data'): - tags = source_item.serialize_tag_data() - self.tags_v1 = tags - self.add_tags_from_field() + @tags_v1.setter + def tags_v1(self, tags: str) -> None: + """ + Returns the serialized tags. + """ + self._tags_v1 = tags - def serialize_tag_data(self): + @classmethod + def serialize_tag_data(cls, usage_id): """ - Serialize block's tag data to include in the xml, escaping special characters + Serialize a block's tag data to include in the xml, escaping special characters Example tags: LightCast Skills Taxonomy: ["Typing", "Microsoft Office"] @@ -52,7 +46,7 @@ def serialize_tag_data(self): # 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 = {} @@ -76,20 +70,13 @@ def add_tags_to_node(self, node): """ 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 read_tags_from_node(self, node): - """ - Deserialize and read tag data from node - """ - if 'tags-v1' in node.attrib: - self.tags_v1 = str(node.attrib['tags-v1']) - def add_tags_from_field(self): """ - Parse and add tag data from tags_v1 field + 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 @@ -108,3 +95,38 @@ def add_tags_from_field(self): 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 diff --git a/xmodule/library_content_block.py b/xmodule/library_content_block.py index ea037dab8901..00321639a735 100644 --- a/xmodule/library_content_block.py +++ b/xmodule/library_content_block.py @@ -593,10 +593,23 @@ def studio_post_duplicate(self, store, source_block): 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 diff --git a/xmodule/studio_editable.py b/xmodule/studio_editable.py index 332025619eb4..29312014f96a 100644 --- a/xmodule/studio_editable.py +++ b/xmodule/studio_editable.py @@ -49,7 +49,7 @@ def get_preview_view_name(block): """ 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 @@ def get_preview_view_name(block): # 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 @@ def get_preview_view_name(block): # 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): diff --git a/xmodule/xml_block.py b/xmodule/xml_block.py index dc9ec284d252..00e396e4da04 100644 --- a/xmodule/xml_block.py +++ b/xmodule/xml_block.py @@ -428,8 +428,6 @@ def add_xml_to_node(self, node): """ 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 @@ def add_xml_to_node(self, node): 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. From b4f195a6406b11e3ba1369883574a61b1fbabb74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 7 Mar 2024 10:41:38 -0300 Subject: [PATCH 21/23] chore: update edx-ora2 package --- requirements/edx/base.txt | 2 +- requirements/edx/bundled.in | 3 +-- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index ac88fc83e24a..e06bbcf45e51 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -796,7 +796,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/bundled.in -ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes +ora2==6.4.0 # via -r requirements/edx/bundled.in packaging==23.2 # via diff --git a/requirements/edx/bundled.in b/requirements/edx/bundled.in index eab7846277db..b4685a3a2cbe 100644 --- a/requirements/edx/bundled.in +++ b/requirements/edx/bundled.in @@ -44,8 +44,7 @@ done-xblock # a very simple XBlock that allows learners recommender-xblock # https://github.com/edx/RecommenderXBlock staff-graded-xblock # https://github.com/openedx/staff_graded-xblock Allows off-site bulk scoring. edx-sga # The more well known "staff graded assignment" XBlock, from MIT. -# ora2>=4.5.0 # Open Response Assessment XBlock -ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes +ora2>=4.5.0 # Open Response Assessment XBlock xblock-poll # Xblock for polling users xblock-drag-and-drop-v2 # Drag and Drop XBlock xblock-google-drive # XBlock for google docs and calendar diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 187aa5919e59..59dd3bc4c064 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.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 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes +ora2==6.4.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 2b0096697caf..089d91db3aed 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -931,7 +931,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes +ora2==6.4.0 # via -r requirements/edx/base.txt packaging==23.2 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index c37e638f047e..8048a3e8bac5 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -988,7 +988,7 @@ optimizely-sdk==4.1.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt -ora2 @ git+https://github.com/open-craft/edx-ora2@rpenido/fal-3621-revert-tag-serialize-and-deserialize-changes +ora2==6.4.0 # via -r requirements/edx/base.txt packaging==23.2 # via From 0f2ac20831a755612562dfcdad426d6e7e3e44b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 7 Mar 2024 10:29:22 -0300 Subject: [PATCH 22/23] docs: improve docstring Co-authored-by: Jillian --- openedx/core/djangoapps/content_tagging/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/content_tagging/utils.py b/openedx/core/djangoapps/content_tagging/utils.py index 16b5804f28b9..7459c150ab7a 100644 --- a/openedx/core/djangoapps/content_tagging/utils.py +++ b/openedx/core/djangoapps/content_tagging/utils.py @@ -34,7 +34,7 @@ def get_content_key_from_string(key_str: str) -> ContentKey: def get_context_key_from_key(content_key: ContentKey) -> ContextKey: """ - Get context key from an key + Returns the context key from a given content key. """ # If the content key is a CourseKey or a LibraryLocatorV2, return it if isinstance(content_key, (CourseKey, LibraryLocatorV2)): From 672f76cd7d8e54c0e6814be3616882d89857cb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 8 Mar 2024 11:37:47 -0300 Subject: [PATCH 23/23] test: change xblock duplication tests to be implementation independent --- .../contentstore/views/tests/test_block.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index c00300bd57e9..0a12287ac199 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -1135,15 +1135,6 @@ def test_duplicate_tags(self): # Tag the child block tagging_api.tag_object(str(source_block.location), taxonomyA, ["two"],) - # 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(source_chapter.location) == expected_chapter_tags - - source_block = self.store.get_item(source_block.location) - expected_block_tags = 'A:two' - assert source_block.serialize_tag_data(source_block.location) == expected_block_tags - # Duplicate the chapter (and its children) dupe_location = duplicate_block( parent_usage_key=source_course.location, @@ -1155,8 +1146,19 @@ def test_duplicate_tags(self): dupe_block = dupe_chapter.get_children()[0] # Check that the duplicated blocks also duplicated tags - assert dupe_chapter.serialize_tag_data(dupe_chapter.location) == expected_chapter_tags - assert dupe_block.serialize_tag_data(dupe_block.location) == expected_block_tags + expected_chapter_tags = [ + f' {str(dupe_chapter.location)}: A=one', + f' {str(dupe_chapter.location)}: A=two', + f' {str(dupe_chapter.location)}: B=four', + f' {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' {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