feat: paste tags when pasting xblocks with tag data (#34270)
This commit is contained in:
@@ -18,7 +18,6 @@ from xblock.runtime import IdGenerator
|
||||
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
|
||||
|
||||
@@ -367,14 +366,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)
|
||||
|
||||
return new_xblock
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ from common.djangoapps.xblock_django.models import (
|
||||
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,59 @@ class TestDuplicateItem(ItemTest, DuplicateHelper, OpenEdxEventsTestMixin):
|
||||
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(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(str(source_block.location), taxonomyA, ["two"],)
|
||||
|
||||
# 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
|
||||
expected_chapter_tags = [
|
||||
f'<ObjectTag> {str(dupe_chapter.location)}: A=one',
|
||||
f'<ObjectTag> {str(dupe_chapter.location)}: A=two',
|
||||
f'<ObjectTag> {str(dupe_chapter.location)}: B=four',
|
||||
f'<ObjectTag> {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'<ObjectTag> {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
|
||||
class TestMoveItem(ItemTest):
|
||||
|
||||
@@ -6,6 +6,7 @@ APIs.
|
||||
import ddt
|
||||
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
|
||||
@@ -13,6 +14,7 @@ from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory, Toy
|
||||
|
||||
from cms.djangoapps.contentstore.utils import reverse_usage_url
|
||||
from openedx.core.djangoapps.content_libraries import api as library_api
|
||||
from openedx.core.djangoapps.content_tagging import api as tagging_api
|
||||
|
||||
CLIPBOARD_ENDPOINT = "/api/content-staging/v1/clipboard/"
|
||||
XBLOCK_ENDPOINT = "/xblock/"
|
||||
@@ -141,6 +143,69 @@ class ClipboardPasteTestCase(ModuleStoreTestCase):
|
||||
# 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")
|
||||
|
||||
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")
|
||||
tagging_api.tag_object(
|
||||
object_id=str(unit_key),
|
||||
taxonomy=taxonomy_all_org,
|
||||
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"],
|
||||
)
|
||||
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
|
||||
|
||||
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]) == f'<ObjectTag> {dest_unit_key}: test_taxonomy=tag_1'
|
||||
assert str(tags[1]) == f'<ObjectTag> {dest_unit_key}: test_taxonomy=tag_2'
|
||||
|
||||
def test_paste_with_assets(self):
|
||||
"""
|
||||
When pasting into a different course, any required static assets should
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
from urllib.parse import quote
|
||||
"""
|
||||
Content tagging functionality for XBlocks.
|
||||
"""
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
|
||||
class TaggedBlockMixin:
|
||||
@@ -7,9 +9,32 @@ class TaggedBlockMixin:
|
||||
Mixin containing XML serializing and parsing functionality for tagged blocks
|
||||
"""
|
||||
|
||||
def serialize_tag_data(self):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Serialize block's tag data to include in the xml, escaping special characters
|
||||
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 = ""
|
||||
|
||||
@property
|
||||
def tags_v1(self) -> str:
|
||||
"""
|
||||
Returns the serialized tags.
|
||||
"""
|
||||
return self._tags_v1
|
||||
|
||||
@tags_v1.setter
|
||||
def tags_v1(self, tags: str) -> None:
|
||||
"""
|
||||
Returns the serialized tags.
|
||||
"""
|
||||
self._tags_v1 = tags
|
||||
|
||||
@classmethod
|
||||
def serialize_tag_data(cls, usage_id):
|
||||
"""
|
||||
Serialize a block's tag data to include in the xml, escaping special characters
|
||||
|
||||
Example tags:
|
||||
LightCast Skills Taxonomy: ["Typing", "Microsoft Office"]
|
||||
@@ -21,7 +46,7 @@ class TaggedBlockMixin:
|
||||
# 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 = {}
|
||||
@@ -45,13 +70,63 @@ class TaggedBlockMixin:
|
||||
"""
|
||||
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 add_tags_from_field(self):
|
||||
"""
|
||||
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
|
||||
from openedx.core.djangoapps.content_tagging.api import set_object_tags
|
||||
|
||||
tag_data = self.tags_v1
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
@@ -13,7 +13,8 @@ from openedx_tagging.core.tagging.models import ObjectTag, Taxonomy
|
||||
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(
|
||||
@@ -161,16 +162,42 @@ 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
|
||||
|
||||
|
||||
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
|
||||
@@ -181,3 +208,4 @@ delete_object_tags = oel_tagging.delete_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
|
||||
|
||||
@@ -20,10 +20,9 @@ from common.djangoapps.student.roles import (
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -10,7 +10,9 @@ from opaque_keys.edx.locator import LibraryLocatorV2
|
||||
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]]
|
||||
|
||||
@@ -7,11 +7,13 @@ from edx_django_utils.cache import RequestCache
|
||||
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
|
||||
Returns the context key from a given content 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 @@ class TaggingRulesCache:
|
||||
"""
|
||||
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]:
|
||||
"""
|
||||
@@ -102,3 +128,6 @@ class TaggingRulesCache:
|
||||
return [
|
||||
library_orgs[org_name] for org_name in org_names if org_name in library_orgs
|
||||
]
|
||||
|
||||
|
||||
rules_cache = TaggingRulesCache()
|
||||
|
||||
@@ -89,6 +89,11 @@ class XBlockSerializer:
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -786,7 +786,7 @@ openedx-filters==1.6.0
|
||||
# via
|
||||
# -r requirements/edx/kernel.in
|
||||
# lti-consumer-xblock
|
||||
openedx-learning==0.6.2
|
||||
openedx-learning==0.6.3
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/kernel.in
|
||||
@@ -796,7 +796,7 @@ optimizely-sdk==4.1.1
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/bundled.in
|
||||
ora2==6.0.34
|
||||
ora2==6.4.0
|
||||
# via -r requirements/edx/bundled.in
|
||||
packaging==23.2
|
||||
# via
|
||||
|
||||
@@ -1308,7 +1308,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==0.6.3
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/doc.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.0.34
|
||||
ora2==6.4.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
|
||||
@@ -921,7 +921,7 @@ openedx-filters==1.6.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# lti-consumer-xblock
|
||||
openedx-learning==0.6.2
|
||||
openedx-learning==0.6.3
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -931,7 +931,7 @@ optimizely-sdk==4.1.1
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
ora2==6.0.34
|
||||
ora2==6.4.0
|
||||
# via -r requirements/edx/base.txt
|
||||
packaging==23.2
|
||||
# via
|
||||
|
||||
@@ -978,7 +978,7 @@ openedx-filters==1.6.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# lti-consumer-xblock
|
||||
openedx-learning==0.6.2
|
||||
openedx-learning==0.6.3
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -988,7 +988,7 @@ optimizely-sdk==4.1.1
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
ora2==6.0.34
|
||||
ora2==6.4.0
|
||||
# via -r requirements/edx/base.txt
|
||||
packaging==23.2
|
||||
# via
|
||||
|
||||
@@ -593,10 +593,23 @@ class LibraryContentBlock(
|
||||
|
||||
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
|
||||
|
||||
@@ -49,7 +49,7 @@ class StudioEditableBlock(XBlockMixin):
|
||||
"""
|
||||
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 @@ class StudioEditableBlock(XBlockMixin):
|
||||
# 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 @@ class StudioEditableBlock(XBlockMixin):
|
||||
# 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):
|
||||
|
||||
@@ -428,8 +428,6 @@ class XmlMixin:
|
||||
"""
|
||||
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 @@ class XmlMixin:
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user