diff --git a/cms/djangoapps/contentstore/models.py b/cms/djangoapps/contentstore/models.py index c39e04c299dc..d80517c2a842 100644 --- a/cms/djangoapps/contentstore/models.py +++ b/cms/djangoapps/contentstore/models.py @@ -17,7 +17,7 @@ from opaque_keys.edx.locator import LibraryContainerLocator from openedx_content.api import get_published_version from openedx_content.models_api import Component, Container -from openedx_django_lib.fields import immutable_uuid_field, key_field, manual_date_time_field +from openedx_django_lib.fields import immutable_uuid_field, manual_date_time_field, ref_field logger = logging.getLogger(__name__) @@ -87,7 +87,7 @@ class EntityLinkBase(models.Model): """ uuid = immutable_uuid_field() # Search by library/upstream context key - upstream_context_key = key_field( + upstream_context_key = ref_field( help_text=_("Upstream context key i.e., learning_package/library key"), db_index=True, ) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index bebcdfd53dc5..7ac9e8e479ee 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -274,7 +274,7 @@ def setUp(self): collection_key = "test-collection" content_api.create_collection( learning_package_id=learning_package.id, - key=collection_key, + collection_code=collection_key, title="Test Collection", created_by=self.user.id, ) diff --git a/cms/djangoapps/modulestore_migrator/api/read_api.py b/cms/djangoapps/modulestore_migrator/api/read_api.py index 064223dc9633..cd9962c4aa19 100644 --- a/cms/djangoapps/modulestore_migrator/api/read_api.py +++ b/cms/djangoapps/modulestore_migrator/api/read_api.py @@ -135,9 +135,9 @@ def get_migrations( if source_key: migrations = migrations.filter(source__key=source_key) if target_key: - migrations = migrations.filter(target__key=str(target_key)) + migrations = migrations.filter(target__package_ref=str(target_key)) if target_collection_slug: - migrations = migrations.filter(target_collection__key=target_collection_slug) + migrations = migrations.filter(target_collection__collection_code=target_collection_slug) if task_uuid: migrations = migrations.filter(task_status__uuid=task_uuid) if is_failed is not None: @@ -176,9 +176,9 @@ def _migration(m: models.ModulestoreMigration) -> ModulestoreMigration: return ModulestoreMigration( pk=m.id, source_key=m.source.key, - target_key=LibraryLocatorV2.from_string(m.target.key), + target_key=LibraryLocatorV2.from_string(m.target.package_ref), target_title=m.target.title, - target_collection_slug=(m.target_collection.key if m.target_collection else None), + target_collection_slug=(m.target_collection.collection_code if m.target_collection else None), target_collection_title=(m.target_collection.title if m.target_collection else None), is_failed=m.is_failed, task_uuid=m.task_status.uuid, @@ -209,7 +209,7 @@ def _block_migration_success( """ Build an instance of the migration success dataclass """ - target_library_key = LibraryLocatorV2.from_string(target.learning_package.key) + target_library_key = LibraryLocatorV2.from_string(target.learning_package.package_ref) target_key: LibraryUsageLocatorV2 | LibraryContainerLocator if hasattr(target, "component"): target_key = library_component_usage_key(target_library_key, target.component) diff --git a/cms/djangoapps/modulestore_migrator/rest_api/v1/serializers.py b/cms/djangoapps/modulestore_migrator/rest_api/v1/serializers.py index 797c42d9a5b3..9dc0c5dda5d3 100644 --- a/cms/djangoapps/modulestore_migrator/rest_api/v1/serializers.py +++ b/cms/djangoapps/modulestore_migrator/rest_api/v1/serializers.py @@ -18,6 +18,10 @@ class LibraryMigrationCollectionSerializer(serializers.ModelSerializer): """ Serializer for the target collection of a library migration. """ + # Expose Collection.collection_code as "key" to preserve the REST API field name. + # This is temporary: https://github.com/openedx/openedx-platform/issues/38406 + key = serializers.CharField(source='collection_code') + class Meta: model = Collection fields = ["key", "title"] diff --git a/cms/djangoapps/modulestore_migrator/rest_api/v1/views.py b/cms/djangoapps/modulestore_migrator/rest_api/v1/views.py index 0b76c01cdaaa..594c9518a2ae 100644 --- a/cms/djangoapps/modulestore_migrator/rest_api/v1/views.py +++ b/cms/djangoapps/modulestore_migrator/rest_api/v1/views.py @@ -540,7 +540,7 @@ def get_queryset(self): self.request.user, lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY ) - queryset = queryset.filter(target__key=library_key, source__key__startswith='course-v1') + queryset = queryset.filter(target__package_ref=str(library_key), source__key__startswith='course-v1') return queryset diff --git a/cms/djangoapps/modulestore_migrator/tasks.py b/cms/djangoapps/modulestore_migrator/tasks.py index b54b9191e6ba..15b25524b871 100644 --- a/cms/djangoapps/modulestore_migrator/tasks.py +++ b/cms/djangoapps/modulestore_migrator/tasks.py @@ -346,13 +346,13 @@ def _import_structure( LibraryUsageLocatorV2(target_library.key, block_type, block_id) # type: ignore[abstract] for block_type, block_id in content_api.get_components(migration.target.id).values_list( - "component_type__name", "local_key" + "component_type__name", "component_code" ) ), used_container_slugs=set( content_api.get_containers( migration.target.id - ).values_list("publishable_entity__key", flat=True) + ).values_list("publishable_entity__entity_ref", flat=True) ), previous_block_migrations=( get_migration_blocks(source_data.previous_migration.pk) @@ -409,7 +409,7 @@ def _populate_collection(user_id: int, migration: models.ModulestoreMigration) - if block_target_pks: content_api.add_to_collection( learning_package_id=migration.target.pk, - key=migration.target_collection.key, + collection_code=migration.target_collection.collection_code, entities_qset=PublishableEntity.objects.filter(id__in=block_target_pks), created_by=user_id, ) @@ -867,7 +867,7 @@ def _migrate_container( container_exists = False if PublishableEntity.objects.filter( learning_package_id=context.target_package_id, - key=target_key.container_id, + entity_ref=target_key.container_id, ).exists(): libraries_api.restore_container(container_key=target_key) container = libraries_api.get_container(target_key) @@ -932,7 +932,7 @@ def _migrate_component( try: component = content_api.get_components(context.target_package_id).get( component_type=component_type, - local_key=target_key.block_id, + component_code=target_key.block_id, ) component_existed = True # Do we have a specific method for this? @@ -953,7 +953,7 @@ def _migrate_component( component = content_api.create_component( context.target_package_id, component_type=component_type, - local_key=target_key.block_id, + component_code=target_key.block_id, created=context.created_at, created_by=context.created_by, ) @@ -971,7 +971,7 @@ def _migrate_component( continue new_path = f"static/{filename}" content_api.create_component_version_media( - component_version.pk, media_pk, key=new_path + component_version.pk, media_pk, path=new_path ) # Publish the component diff --git a/cms/djangoapps/modulestore_migrator/tests/test_api.py b/cms/djangoapps/modulestore_migrator/tests/test_api.py index 311d2b5b69ea..7e88e031a1ad 100644 --- a/cms/djangoapps/modulestore_migrator/tests/test_api.py +++ b/cms/djangoapps/modulestore_migrator/tests/test_api.py @@ -232,7 +232,7 @@ def test_start_migration_to_library_with_collection(self): collection_key = "test-collection" content_api.create_collection( learning_package_id=self.learning_package.id, - key=collection_key, + collection_code=collection_key, title="Test Collection", created_by=user.id, ) @@ -249,7 +249,7 @@ def test_start_migration_to_library_with_collection(self): ) modulestoremigration = ModulestoreMigration.objects.get() - assert modulestoremigration.target_collection.key == collection_key + assert modulestoremigration.target_collection.collection_code == collection_key def test_start_migration_to_library_with_strategy_skip(self): """ @@ -487,19 +487,19 @@ def test_migration_api_for_various_scenarios(self): # Lib 2 has Collection C content_api.create_collection( learning_package_id=self.learning_package.id, - key="test-collection-1a", + collection_code="test-collection-1a", title="Test Collection A in Lib 1", created_by=user.id, ) content_api.create_collection( learning_package_id=self.learning_package.id, - key="test-collection-1b", + collection_code="test-collection-1b", title="Test Collection B in Lib 1", created_by=user.id, ) content_api.create_collection( learning_package_id=self.learning_package_2.id, - key="test-collection-2c", + collection_code="test-collection-2c", title="Test Collection C in Lib 2", created_by=user.id, ) diff --git a/cms/djangoapps/modulestore_migrator/tests/test_tasks.py b/cms/djangoapps/modulestore_migrator/tests/test_tasks.py index 2285dd7d77e8..ae4ad1548937 100644 --- a/cms/djangoapps/modulestore_migrator/tests/test_tasks.py +++ b/cms/djangoapps/modulestore_migrator/tests/test_tasks.py @@ -89,12 +89,12 @@ def setUp(self): ) self.collection = Collection.objects.create( learning_package=self.learning_package, - key="test_collection", + collection_code="test_collection", title="Test Collection", ) self.collection2 = Collection.objects.create( learning_package=self.learning_package, - key="test_collection2", + collection_code="test_collection2", title="Test Collection 2", ) @@ -426,7 +426,7 @@ def test_migrate_component_with_static_content(self): self.assertIsNone(reason) # noqa: PT009 component_media = result.componentversion.componentversionmedia_set.filter( - key="static/test_image.png" + path="static/test_image.png" ).first() self.assertIsNotNone(component_media) # noqa: PT009 self.assertEqual(component_media.media.id, test_media.id) # noqa: PT009 @@ -673,12 +673,12 @@ def test_migrate_component_content_filename_not_in_olx(self): referenced_content_exists = ( result.componentversion.componentversionmedia_set.filter( - key="static/referenced.png" + path="static/referenced.png" ).exists() ) unreferenced_content_exists = ( result.componentversion.componentversionmedia_set.filter( - key="static/unreferenced.png" + path="static/unreferenced.png" ).exists() ) @@ -718,7 +718,7 @@ def test_migrate_container_creates_new_container(self): component_type=content_api.get_or_create_component_type( "xblock.v1", "problem" ), - local_key="child_problem_1", + component_code="child_problem_1", created=timezone.now(), created_by=self.user.id, ) @@ -734,7 +734,7 @@ def test_migrate_container_creates_new_container(self): component_type=content_api.get_or_create_component_type( "xblock.v1", "html" ), - local_key="child_html_1", + component_code="child_html_1", created=timezone.now(), created_by=self.user.id, ) @@ -906,7 +906,7 @@ def test_migrate_container_preserves_child_order(self): component_type=content_api.get_or_create_component_type( "xblock.v1", "problem" ), - local_key=f"child_problem_{i}", + component_code=f"child_problem_{i}", created=timezone.now(), created_by=self.user.id, ) @@ -946,7 +946,7 @@ def test_migrate_container_with_mixed_child_types(self): component_type=content_api.get_or_create_component_type( "xblock.v1", "problem" ), - local_key="mixed_problem", + component_code="mixed_problem", created=timezone.now(), created_by=self.user.id, ) @@ -962,7 +962,7 @@ def test_migrate_container_with_mixed_child_types(self): component_type=content_api.get_or_create_component_type( "xblock.v1", "html" ), - local_key="mixed_html", + component_code="mixed_html", created=timezone.now(), created_by=self.user.id, ) @@ -978,7 +978,7 @@ def test_migrate_container_with_mixed_child_types(self): component_type=content_api.get_or_create_component_type( "xblock.v1", "video" ), - local_key="mixed_video", + component_code="mixed_video", created=timezone.now(), created_by=self.user.id, ) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index f6bfdf13be77..2d044cd4210d 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -641,7 +641,7 @@ def index_collection_batch(batch, num_done, library_key) -> int: docs = [] for collection in batch: try: - collection_key = lib_api.library_collection_locator(library_key, collection.key) + collection_key = lib_api.library_collection_locator(library_key, collection.collection_code) doc = searchable_doc_for_collection(collection_key, collection=collection) doc.update(searchable_doc_tags(collection_key)) docs.append(doc) @@ -677,7 +677,7 @@ def index_container_batch(batch, num_done, library_key) -> int: doc.update(searchable_doc_containers(container_key, "sections")) docs.append(doc) except Exception as err: # pylint: disable=broad-except - status_cb(f"Error indexing container {container.key}: {err}") + status_cb(f"Error indexing container {container.entity_ref}: {err}") num_done += 1 if docs: @@ -1032,7 +1032,7 @@ def upsert_content_library_index_docs(library_key: LibraryLocatorV2, full_index: docs.append(doc) for collection in lib_api.get_library_collections(library_key): - collection_key = lib_api.library_collection_locator(library_key, collection.key) + collection_key = lib_api.library_collection_locator(library_key, collection.collection_code) doc = searchable_doc_for_collection(collection_key, collection=collection) docs.append(doc) diff --git a/openedx/core/djangoapps/content/search/documents.py b/openedx/core/djangoapps/content/search/documents.py index b986966ec42c..678281191eeb 100644 --- a/openedx/core/djangoapps/content/search/documents.py +++ b/openedx/core/djangoapps/content/search/documents.py @@ -7,6 +7,7 @@ from hashlib import blake2b from django.core.exceptions import ObjectDoesNotExist +from django.db.models import F from django.utils.text import slugify from opaque_keys.edx.keys import ContainerKey, LearningContextKey, OpaqueKey, UsageKey from opaque_keys.edx.locator import LibraryCollectionLocator, LibraryContainerLocator @@ -33,7 +34,7 @@ class Fields: usage_key = "usage_key" type = "type" # DocType.course_block or DocType.library_block (see below) # The block_id part of the usage key for course or library blocks. - # If it's a collection, the collection.key is stored here. + # If it's a collection, the collection.collection_code is stored here. # Sometimes human-readable, sometimes a random hex ID # Is only unique within the given context_key. block_id = "block_id" @@ -64,7 +65,8 @@ class Fields: tags_level2 = "level2" tags_level3 = "level3" # Collections (dictionary) that this object belongs to. - # Similarly to tags above, we collect the collection.titles and collection.keys into hierarchical facets. + # Similarly to tags above, we collect the collection.titles and collection.collection_codes + # into hierarchical facets. collections = "collections" collections_display_name = "display_name" collections_key = "key" @@ -448,10 +450,13 @@ def searchable_doc_collections(object_id: OpaqueKey) -> dict: try: if isinstance(object_id, UsageKey): component = lib_api.get_component_from_usage_key(object_id) + # Temporarily alias collection_code to "key" so downstream consumers + # (search indexer, REST API) keep the same field name. We will update + # downstream consumers later: https://github.com/openedx/openedx-platform/issues/38406 collections = content_api.get_entity_collections( component.learning_package_id, - component.key, - ).values('key', 'title') + component.entity_ref, + ).values("title", key=F('collection_code')) elif isinstance(object_id, LibraryContainerLocator): container = lib_api.get_container(object_id, include_collections=True) collections = container.collections @@ -543,7 +548,7 @@ def searchable_doc_for_collection( pass if collection: - assert collection.key == collection_key.collection_id + assert collection.collection_code == collection_key.collection_id draft_num_children = content_api.filter_publishable_entities( collection.entities, @@ -558,7 +563,7 @@ def searchable_doc_for_collection( Fields.context_key: str(collection_key.context_key), Fields.org: str(collection_key.org), Fields.usage_key: str(collection_key), - Fields.block_id: collection.key, + Fields.block_id: collection.collection_code, Fields.type: DocType.collection, Fields.display_name: collection.title, Fields.description: collection.description, diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py index 8a29bb450326..afa847748e89 100644 --- a/openedx/core/djangoapps/content/search/tests/test_api.py +++ b/openedx/core/djangoapps/content/search/tests/test_api.py @@ -188,11 +188,11 @@ def setUp(self) -> None: tagging_api.add_tag_to_taxonomy(self.taxonomyB, "four") # Create a collection: - self.learning_package = content_api.get_learning_package_by_key(self.library.key) + self.learning_package = content_api.get_learning_package_by_ref(str(self.library.key)) with freeze_time(self.created_date): self.collection = content_api.create_collection( learning_package_id=self.learning_package.id, - key="MYCOL", + collection_code="MYCOL", title="my_collection", created_by=None, description="my collection description" @@ -202,7 +202,7 @@ def setUp(self) -> None: ) self.collection_dict = { "id": "lib-collectionorg1libmycol-5b647617", - "block_id": self.collection.key, + "block_id": self.collection.collection_code, "usage_key": str(self.collection_key), "type": "collection", "display_name": "my_collection", @@ -536,7 +536,7 @@ def test_reindex_meilisearch_library_block_error(self, mock_meilisearch) -> None def mocked_from_component(lib_key, component): # Simulate an error when processing problem 1 - if component.key == 'xblock.v1:problem:p1': + if component.entity_ref == 'xblock.v1:problem:p1': raise Exception('Error') return orig_from_component(lib_key, component) @@ -722,7 +722,7 @@ def test_index_library_block_and_collections(self, mock_meilisearch) -> None: for collection in (collection2, collection1): library_api.update_library_collection_items( self.library.key, - collection_key=collection.key, + collection_key=collection.collection_code, opaque_keys=[ self.problem1.usage_key, ], @@ -732,8 +732,8 @@ def test_index_library_block_and_collections(self, mock_meilisearch) -> None: lib_access, _ = SearchAccess.objects.get_or_create(context_key=self.library.key) doc_collection1_created = { "id": "lib-collectionorg1libcol1-283a79c9", - "block_id": collection1.key, - "usage_key": f"lib-collection:org1:lib:{collection1.key}", + "block_id": collection1.collection_code, + "usage_key": f"lib-collection:org1:lib:{collection1.collection_code}", "type": "collection", "display_name": "Collection 1", "description": "First Collection", @@ -750,8 +750,8 @@ def test_index_library_block_and_collections(self, mock_meilisearch) -> None: } doc_collection2_created = { "id": "lib-collectionorg1libcol2-46823d4d", - "block_id": collection2.key, - "usage_key": f"lib-collection:org1:lib:{collection2.key}", + "block_id": collection2.collection_code, + "usage_key": f"lib-collection:org1:lib:{collection2.collection_code}", "type": "collection", "display_name": "Collection 2", "description": "Second Collection", @@ -768,8 +768,8 @@ def test_index_library_block_and_collections(self, mock_meilisearch) -> None: } doc_collection2_updated = { "id": "lib-collectionorg1libcol2-46823d4d", - "block_id": collection2.key, - "usage_key": f"lib-collection:org1:lib:{collection2.key}", + "block_id": collection2.collection_code, + "usage_key": f"lib-collection:org1:lib:{collection2.collection_code}", "type": "collection", "display_name": "Collection 2", "description": "Second Collection", @@ -786,8 +786,8 @@ def test_index_library_block_and_collections(self, mock_meilisearch) -> None: } doc_collection1_updated = { "id": "lib-collectionorg1libcol1-283a79c9", - "block_id": collection1.key, - "usage_key": f"lib-collection:org1:lib:{collection1.key}", + "block_id": collection1.collection_code, + "usage_key": f"lib-collection:org1:lib:{collection1.collection_code}", "type": "collection", "display_name": "Collection 1", "description": "First Collection", @@ -904,7 +904,7 @@ def test_delete_collection(self, mock_meilisearch) -> None: with freeze_time(updated_date): library_api.update_library_collection_items( self.library.key, - collection_key=self.collection.key, + collection_key=self.collection.collection_code, opaque_keys=[ self.problem1.usage_key, self.unit.container_key @@ -918,14 +918,14 @@ def test_delete_collection(self, mock_meilisearch) -> None: "id": self.doc_problem1["id"], "collections": { "display_name": [self.collection.title], - "key": [self.collection.key], + "key": [self.collection.collection_code], }, } doc_unit_with_collection = { "id": self.unit_dict["id"], "collections": { "display_name": [self.collection.title], - "key": [self.collection.key], + "key": [self.collection.collection_code], }, } @@ -944,7 +944,7 @@ def test_delete_collection(self, mock_meilisearch) -> None: # Soft-delete the collection content_api.delete_collection( self.collection.learning_package_id, - self.collection.key, + self.collection.collection_code, ) doc_problem_without_collection = { @@ -979,7 +979,7 @@ def test_delete_collection(self, mock_meilisearch) -> None: with freeze_time(restored_date): content_api.restore_collection( self.collection.learning_package_id, - self.collection.key, + self.collection.collection_code, ) doc_collection = copy.deepcopy(self.collection_dict) @@ -1001,7 +1001,7 @@ def test_delete_collection(self, mock_meilisearch) -> None: # Hard-delete the collection content_api.delete_collection( self.collection.learning_package_id, - self.collection.key, + self.collection.collection_code, hard_delete=True, ) diff --git a/openedx/core/djangoapps/content/search/tests/test_documents.py b/openedx/core/djangoapps/content/search/tests/test_documents.py index a9aea3ab3cfb..ee4a1d613f61 100644 --- a/openedx/core/djangoapps/content/search/tests/test_documents.py +++ b/openedx/core/djangoapps/content/search/tests/test_documents.py @@ -492,7 +492,7 @@ def test_collection_with_library(self): assert doc == { "id": "lib-collectionedx2012_falltoy_collection-d1d907a4", - "block_id": self.collection.key, + "block_id": self.collection.collection_code, "usage_key": str(self.collection_key), "type": "collection", "org": "edX", @@ -521,7 +521,7 @@ def test_collection_with_published_library(self): assert doc == { "id": "lib-collectionedx2012_falltoy_collection-d1d907a4", - "block_id": self.collection.key, + "block_id": self.collection.collection_code, "usage_key": str(self.collection_key), "type": "collection", "org": "edX", diff --git a/openedx/core/djangoapps/content_libraries/api/blocks.py b/openedx/core/djangoapps/content_libraries/api/blocks.py index 2ddc06fde254..f8d45b78d5fe 100644 --- a/openedx/core/djangoapps/content_libraries/api/blocks.py +++ b/openedx/core/djangoapps/content_libraries/api/blocks.py @@ -15,7 +15,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import validate_unicode_slug from django.db import transaction -from django.db.models import QuerySet +from django.db.models import F, QuerySet from django.urls import reverse from django.utils.text import slugify from django.utils.translation import gettext as _ @@ -176,10 +176,13 @@ def get_library_block(usage_key: LibraryUsageLocatorV2, include_collections=Fals raise ContentLibraryBlockNotFound(usage_key) if include_collections: + # Temporarily alias collection_code to "key" so downstream consumers + # (search indexer, REST API) keep the same field name. We will update + # downstream consumers later: https://github.com/openedx/openedx-platform/issues/38406 associated_collections = content_api.get_entity_collections( component.learning_package_id, - component.key, - ).values('key', 'title') + component.entity_ref, + ).values("title", key=F('collection_code')) else: associated_collections = None xblock_metadata = LibraryXBlockMetadata.from_component( @@ -425,7 +428,7 @@ def _import_staged_block( component = content_api.create_component( # noqa: F841 learning_package.id, component_type=component_type, - local_key=usage_key.block_id, + component_code=usage_key.block_id, created=now, created_by=user.id, ) @@ -490,7 +493,7 @@ def _import_staged_block( content_api.create_component_version_media( component_version.pk, content.id, - key=filename, + path=filename, ) # Emit library block created event @@ -725,7 +728,7 @@ def send_block_deleted_signal(): send_block_deleted_signal() raise - affected_collections = content_api.get_entity_collections(component.learning_package_id, component.key) + affected_collections = content_api.get_entity_collections(component.learning_package_id, component.entity_ref) affected_containers = get_containers_contains_item(usage_key) content_api.soft_delete_draft(component.id, deleted_by=user_id) @@ -743,7 +746,7 @@ def send_block_deleted_signal(): library_collection=LibraryCollectionData( collection_key=library_collection_locator( library_key=library_key, - collection_key=collection.key, + collection_key=collection.collection_code, ), background=True, ) @@ -770,7 +773,7 @@ def restore_library_block(usage_key: LibraryUsageLocatorV2, user_id: int | None """ component = get_component_from_usage_key(usage_key) library_key = usage_key.context_key - affected_collections = content_api.get_entity_collections(component.learning_package_id, component.key) + affected_collections = content_api.get_entity_collections(component.learning_package_id, component.entity_ref) # Set draft version back to the latest available component version id. content_api.set_draft_version( @@ -809,7 +812,7 @@ def restore_library_block(usage_key: LibraryUsageLocatorV2, user_id: int | None library_collection=LibraryCollectionData( collection_key=library_collection_locator( library_key=library_key, - collection_key=collection.key, + collection_key=collection.collection_code, ), background=True, ) @@ -852,7 +855,7 @@ def get_library_block_static_asset_files(usage_key: LibraryUsageLocatorV2) -> li component_version .componentversionmedia_set .filter(media__has_file=True) - .order_by('key') + .order_by('path') .select_related('media') ) @@ -860,13 +863,13 @@ def get_library_block_static_asset_files(usage_key: LibraryUsageLocatorV2) -> li return [ LibraryXBlockStaticFile( - path=cvm.key, + path=cvm.path, size=cvm.media.size, url=site_root_url + reverse( 'content_libraries:library-assets', kwargs={ 'component_version_uuid': component_version.uuid, - 'asset_path': cvm.key, + 'asset_path': cvm.path, } ), ) @@ -985,7 +988,7 @@ def publish_component_changes(usage_key: LibraryUsageLocatorV2, user_id: int): learning_package = content_library.learning_package assert learning_package # The core publishing API is based on draft objects, so find the draft that corresponds to this component: - drafts_to_publish = content_api.get_all_drafts(learning_package.id).filter(entity__key=component.key) + drafts_to_publish = content_api.get_all_drafts(learning_package.id).filter(entity__entity_ref=component.entity_ref) # Publish the component and update anything that needs to be updated (e.g. search index): publish_log = content_api.publish_from_drafts( learning_package.id, draft_qset=drafts_to_publish, published_by=user_id, @@ -1046,7 +1049,7 @@ def _create_component_for_block( component, component_version = content_api.create_component_and_version( learning_package.id, component_type=component_type, - local_key=usage_key.block_id, + component_code=usage_key.block_id, title=display_name, created=now, created_by=user_id, @@ -1061,7 +1064,7 @@ def _create_component_for_block( content_api.create_component_version_media( component_version.pk, content.id, - key="block.xml", + path="block.xml", ) return component_version diff --git a/openedx/core/djangoapps/content_libraries/api/collections.py b/openedx/core/djangoapps/content_libraries/api/collections.py index 9d011bdae363..1c87acd21232 100644 --- a/openedx/core/djangoapps/content_libraries/api/collections.py +++ b/openedx/core/djangoapps/content_libraries/api/collections.py @@ -2,7 +2,6 @@ Python API for library collections ================================== """ -from django.db import IntegrityError from opaque_keys import OpaqueKey from opaque_keys.edx.keys import BlockTypeKey, UsageKeyV2 from opaque_keys.edx.locator import LibraryCollectionLocator, LibraryContainerLocator, LibraryLocatorV2 @@ -51,18 +50,18 @@ def create_library_collection( assert content_library.learning_package_id assert content_library.library_key == library_key - try: - collection = content_api.create_collection( - learning_package_id=content_library.learning_package_id, - key=collection_key, - title=title, - description=description, - created_by=created_by, - ) - except IntegrityError as err: - raise LibraryCollectionAlreadyExists from err - - return collection + if Collection.objects.filter( + learning_package_id=content_library.learning_package_id, + collection_code=collection_key, + ).exists(): + raise LibraryCollectionAlreadyExists(f"Collection {collection_key} already exists in {library_key}") + return content_api.create_collection( + learning_package_id=content_library.learning_package_id, + collection_code=collection_key, + title=title, + description=description, + created_by=created_by, + ) def update_library_collection( @@ -86,7 +85,7 @@ def update_library_collection( try: collection = content_api.update_collection( learning_package_id=content_library.learning_package_id, - key=collection_key, + collection_code=collection_key, title=title, description=description, ) @@ -127,39 +126,39 @@ def update_library_collection_items( assert content_library.learning_package_id assert content_library.library_key == library_key - # Fetch the Component.key values for the provided UsageKeys. - item_keys = [] + # Fetch the Component.entity_ref values for the provided UsageKeys. + item_refs = [] for opaque_key in opaque_keys: if isinstance(opaque_key, LibraryContainerLocator): try: - container = content_api.get_container_by_key( + container = content_api.get_container_by_code( content_library.learning_package_id, - key=opaque_key.container_id, + container_code=opaque_key.container_id, ) except Collection.DoesNotExist as exc: raise ContentLibraryContainerNotFound(opaque_key) from exc - item_keys.append(container.key) + item_refs.append(container.entity_ref) elif isinstance(opaque_key, UsageKeyV2): # Parse the block_family from the key to use as namespace. block_type = BlockTypeKey.from_string(str(opaque_key)) try: - component = content_api.get_component_by_key( + component = content_api.get_component_by_code( content_library.learning_package_id, namespace=block_type.block_family, type_name=opaque_key.block_type, - local_key=opaque_key.block_id, + component_code=opaque_key.block_id, ) except Component.DoesNotExist as exc: raise ContentLibraryBlockNotFound(opaque_key) from exc - item_keys.append(component.key) + item_refs.append(component.entity_ref) else: # This should never happen, but just in case. raise ValueError(f"Invalid opaque_key: {opaque_key}") entities_qset = PublishableEntity.objects.filter( - key__in=item_keys, + entity_ref__in=item_refs, ) if remove: @@ -181,7 +180,7 @@ def update_library_collection_items( def set_library_item_collections( library_key: LibraryLocatorV2, - entity_key: str, + entity_ref: str, *, collection_keys: list[str], created_by: int | None = None, @@ -207,14 +206,14 @@ def set_library_item_collections( assert content_library.learning_package_id assert content_library.library_key == library_key - publishable_entity = content_api.get_publishable_entity_by_key( + publishable_entity = content_api.get_publishable_entity_by_ref( content_library.learning_package_id, - key=entity_key, + entity_ref=entity_ref, ) - # Note: Component.key matches its PublishableEntity.key + # Note: Component.entity_ref matches its PublishableEntity.entity_ref collection_qs = content_api.get_collections(content_library.learning_package_id).filter( - key__in=collection_keys + collection_code__in=collection_keys ) affected_collections = content_api.set_collections( @@ -232,7 +231,7 @@ def set_library_item_collections( library_collection=LibraryCollectionData( collection_key=library_collection_locator( library_key=library_key, - collection_key=collection.key, + collection_key=collection.collection_code, ), background=True, ) diff --git a/openedx/core/djangoapps/content_libraries/api/container_metadata.py b/openedx/core/djangoapps/content_libraries/api/container_metadata.py index 5bb8bcd50ae4..98a5024ac674 100644 --- a/openedx/core/djangoapps/content_libraries/api/container_metadata.py +++ b/openedx/core/djangoapps/content_libraries/api/container_metadata.py @@ -365,8 +365,9 @@ def library_container_locator( container_type_code = content_api.get_container_type_code_of(container) if container_type_code not in LIBRARY_ALLOWED_CONTAINER_TYPES: raise ValueError(f"Unsupported container type for content libraries: {container!r}") - - return LibraryContainerLocator(library_key, container_type=container_type_code, container_id=container.key) + return LibraryContainerLocator( + library_key, container_type=container_type_code, container_id=container.container_code, + ) def get_container_from_key(container_key: LibraryContainerLocator, include_deleted=False) -> Container: @@ -379,7 +380,7 @@ def get_container_from_key(container_key: LibraryContainerLocator, include_delet content_library = ContentLibrary.objects.get_by_key(container_key.lib_key) learning_package = content_library.learning_package assert learning_package is not None - container = content_api.get_container_by_key(learning_package.id, key=container_key.container_id) + container = content_api.get_container_by_code(learning_package.id, container_code=container_key.container_id) assert content_api.get_container_type_code_of(container) in LIBRARY_ALLOWED_CONTAINER_TYPES # We only return the container if it exists and either: # 1. the container has a draft version (which means it is not soft-deleted) OR diff --git a/openedx/core/djangoapps/content_libraries/api/containers.py b/openedx/core/djangoapps/content_libraries/api/containers.py index 5db34dc753da..0b126e5665df 100644 --- a/openedx/core/djangoapps/content_libraries/api/containers.py +++ b/openedx/core/djangoapps/content_libraries/api/containers.py @@ -10,6 +10,7 @@ from uuid import uuid4 from django.db import transaction +from django.db.models import F from django.utils.text import slugify from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2, LibraryUsageLocatorV2 from openedx_content import api as content_api @@ -73,10 +74,13 @@ def get_container( """ container = get_container_from_key(container_key) if include_collections: + # Temporarily alias collection_code to "key" so downstream consumers + # (search indexer, REST API) keep the same field name. We will update + # downstream consumers later: https://github.com/openedx/openedx-platform/issues/38406 associated_collections = content_api.get_entity_collections( container.publishable_entity.learning_package_id, container_key.container_id, - ).values("key", "title") + ).values("title", key=F("collection_code")) else: associated_collections = None container_meta = ContainerMetadata.from_container( @@ -121,7 +125,7 @@ def create_container( # Then try creating the actual container: container, _initial_version = content_api.create_container_and_version( content_library.learning_package_id, - key=slug, + container_code=slug, title=title, container_cls=container_cls, entities=[], @@ -226,7 +230,7 @@ def send_container_deleted_signal(): # Fetch related collections and containers before soft-delete affected_collections = content_api.get_entity_collections( container.publishable_entity.learning_package_id, - container.key, + container.entity_ref, ) affected_containers = get_containers_contains_item(container_key) # Get children containers or components to update their index data @@ -249,7 +253,7 @@ def send_container_deleted_signal(): library_collection=LibraryCollectionData( collection_key=library_collection_locator( library_key=library_key, - collection_key=collection.key, + collection_key=collection.collection_code, ), background=True, ) @@ -291,7 +295,7 @@ def restore_container(container_key: LibraryContainerLocator) -> None: affected_collections = content_api.get_entity_collections( container.publishable_entity.learning_package_id, - container.key, + container.entity_ref, ) content_api.set_draft_version(container.id, container.versioning.latest.pk) @@ -333,7 +337,7 @@ def restore_container(container_key: LibraryContainerLocator) -> None: library_collection=LibraryCollectionData( collection_key=library_collection_locator( library_key=library_key, - collection_key=collection.key, + collection_key=collection.collection_code, ), ) ) diff --git a/openedx/core/djangoapps/content_libraries/api/libraries.py b/openedx/core/djangoapps/content_libraries/api/libraries.py index bf91039b686b..68d4258bb065 100644 --- a/openedx/core/djangoapps/content_libraries/api/libraries.py +++ b/openedx/core/djangoapps/content_libraries/api/libraries.py @@ -461,14 +461,14 @@ def create_library( # and also update its title/description in case they differ. content_api.update_learning_package( learning_package.id, - key=str(ref.library_key), + package_ref=str(ref.library_key), title=title, description=description, ) else: # We have to generate a new LearningPackage for this library. learning_package = content_api.create_learning_package( - key=str(ref.library_key), + package_ref=str(ref.library_key), title=title, description=description, ) @@ -718,7 +718,7 @@ def library_component_usage_key( return LibraryUsageLocatorV2( # type: ignore[abstract] library_key, block_type=component.component_type.name, - usage_id=component.local_key, + usage_id=component.component_code, ) diff --git a/openedx/core/djangoapps/content_libraries/library_context.py b/openedx/core/djangoapps/content_libraries/library_context.py index bd91a3f89250..5d2c25c4286e 100644 --- a/openedx/core/djangoapps/content_libraries/library_context.py +++ b/openedx/core/djangoapps/content_libraries/library_context.py @@ -95,11 +95,11 @@ def block_exists(self, usage_key: LibraryUsageLocatorV2): if learning_package is None: return False - return content_api.component_exists_by_key( + return content_api.component_exists_by_code( learning_package.id, namespace='xblock.v1', type_name=usage_key.block_type, - local_key=usage_key.block_id, + component_code=usage_key.block_id, ) def send_block_updated_event(self, usage_key: UsageKeyV2): diff --git a/openedx/core/djangoapps/content_libraries/rest_api/blocks.py b/openedx/core/djangoapps/content_libraries/rest_api/blocks.py index 518ac5b31fe3..8dd70a069edb 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/blocks.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/blocks.py @@ -272,7 +272,7 @@ def patch(self, request: RestRequest, usage_key_str) -> Response: collection_keys = serializer.validated_data['collection_keys'] api.set_library_item_collections( library_key=key.lib_key, - entity_key=component.publishable_entity.key, + entity_ref=component.publishable_entity.entity_ref, collection_keys=collection_keys, created_by=request.user.id, content_library=content_library, @@ -379,7 +379,7 @@ def get_component_version_asset(request, component_version_uuid, asset_path): # Permissions check... learning_package = component_version.component.learning_package - library_key = LibraryLocatorV2.from_string(learning_package.key) + library_key = LibraryLocatorV2.from_string(learning_package.package_ref) api.require_permission_for_library_key( library_key, request.user, permissions.CAN_VIEW_THIS_CONTENT_LIBRARY, ) @@ -402,7 +402,7 @@ def get_component_version_asset(request, component_version_uuid, asset_path): return redirect_response # If we got here, we know that the asset exists and it's okay to download. - cv_media = component_version.componentversionmedia_set.get(key=asset_path) + cv_media = component_version.componentversionmedia_set.get(path=asset_path) media = cv_media.media # Delete the re-direct part of the response headers. We'll copy the rest. diff --git a/openedx/core/djangoapps/content_libraries/rest_api/collections.py b/openedx/core/djangoapps/content_libraries/rest_api/collections.py index 3f67f5e777a8..9875f31d79d5 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/collections.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/collections.py @@ -33,7 +33,10 @@ class LibraryCollectionsView(ModelViewSet): """ serializer_class = ContentLibraryCollectionSerializer - lookup_field = 'key' + # URL kwarg is `key` for backwards compatibility. + # https://github.com/openedx/openedx-platform/issues/38406 + lookup_field = 'collection_code' + lookup_url_kwarg = 'key' def __init__(self, *args, **kwargs) -> None: """ @@ -184,7 +187,7 @@ def destroy(self, request: RestRequest, *args, **kwargs) -> Response: assert collection.learning_package_id content_api.delete_collection( collection.learning_package_id, - collection.key, + collection.collection_code, hard_delete=False, ) return Response(None, status=HTTP_204_NO_CONTENT) diff --git a/openedx/core/djangoapps/content_libraries/rest_api/containers.py b/openedx/core/djangoapps/content_libraries/rest_api/containers.py index 04dde384361e..12a4132920c3 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/containers.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/containers.py @@ -346,7 +346,7 @@ def patch(self, request: RestRequest, container_key: LibraryContainerLocator) -> collection_keys = serializer.validated_data['collection_keys'] api.set_library_item_collections( library_key=container_key.lib_key, - entity_key=container_key.container_id, + entity_ref=container_key.container_id, collection_keys=collection_keys, created_by=request.user.id, content_library=content_library, diff --git a/openedx/core/djangoapps/content_libraries/rest_api/serializers.py b/openedx/core/djangoapps/content_libraries/rest_api/serializers.py index 72c59f695833..f8dd8b18a839 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/serializers.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/serializers.py @@ -284,10 +284,13 @@ class ContentLibraryCollectionSerializer(serializers.ModelSerializer): """ Serializer for a Content Library Collection """ + # Expose Collection.collection_code as "key" to preserve the REST API field name. + # https://github.com/openedx/openedx-platform/issues/38406 + key = serializers.CharField(source='collection_code') class Meta: model = Collection - fields = '__all__' + exclude = ['collection_code'] class ContentLibraryCollectionUpdateSerializer(serializers.Serializer): @@ -447,14 +450,16 @@ class RestoreSuccessDataSerializer(serializers.Serializer): """ learning_package_id = serializers.IntegerField(source="lp_restored_data.id") title = serializers.CharField(source="lp_restored_data.title") - org = serializers.CharField(source="lp_restored_data.archive_org_key") - slug = serializers.CharField(source="lp_restored_data.archive_slug") + org = serializers.SerializerMethodField() + slug = serializers.SerializerMethodField() - # The `key` is a unique temporary key assigned to the learning package during the restore process, - # whereas the `archive_key` is the original key of the learning package from the backup. - # The temporary learning package key is replaced with a standard key once it is added to a content library. - key = serializers.CharField(source="lp_restored_data.key") - archive_key = serializers.CharField(source="lp_restored_data.archive_lp_key") + # The `package_ref` is a unique temporary key assigned to the learning + # package during the restore process, whereas the `archive_package_ref` is + # the original key of the learning package from the backup. The temporary + # learning package_ref is replaced with a standard key once it is added to a + # content library. + key = serializers.CharField(source="lp_restored_data.package_ref") + archive_key = serializers.CharField(source="lp_restored_data.archive_package_ref") containers = serializers.IntegerField(source="lp_restored_data.num_containers") components = serializers.IntegerField(source="lp_restored_data.num_components") @@ -467,6 +472,18 @@ class RestoreSuccessDataSerializer(serializers.Serializer): created_at = serializers.DateTimeField(source="backup_metadata.created_at", format=DATETIME_FORMAT) created_by = serializers.SerializerMethodField() + def get_org(self, obj) -> str: + """ + The org code/slug, as parsed from archive_package_ref, or "unknown" if unparseable. + """ + return obj["lp_restored_data"]["archive_org_code"] or "unknown" + + def get_slug(self, obj) -> str: + """ + The library code/slug, as parsed from archive_package_ref, or "unknown" if unparseable. + """ + return obj["lp_restored_data"]["archive_package_code"] or "unknown" + def get_created_by(self, obj): """ Get the user information of the archive creator, if available. diff --git a/openedx/core/djangoapps/content_libraries/signal_handlers.py b/openedx/core/djangoapps/content_libraries/signal_handlers.py index 041a49b473e0..fada1cb2f874 100644 --- a/openedx/core/djangoapps/content_libraries/signal_handlers.py +++ b/openedx/core/djangoapps/content_libraries/signal_handlers.py @@ -43,7 +43,7 @@ def library_collection_saved(sender, instance, created, **kwargs): library_collection=LibraryCollectionData( collection_key=library_collection_locator( library_key=library.library_key, - collection_key=instance.key, + collection_key=instance.collection_code, ), ) ) @@ -54,7 +54,7 @@ def library_collection_saved(sender, instance, created, **kwargs): library_collection=LibraryCollectionData( collection_key=library_collection_locator( library_key=library.library_key, - collection_key=instance.key, + collection_key=instance.collection_code, ), ) ) @@ -77,7 +77,7 @@ def library_collection_deleted(sender, instance, **kwargs): library_collection=LibraryCollectionData( collection_key=library_collection_locator( library_key=library.library_key, - collection_key=instance.key, + collection_key=instance.collection_code, ), ) ) diff --git a/openedx/core/djangoapps/content_libraries/tasks.py b/openedx/core/djangoapps/content_libraries/tasks.py index bb5db0a397c1..ee972b248649 100644 --- a/openedx/core/djangoapps/content_libraries/tasks.py +++ b/openedx/core/djangoapps/content_libraries/tasks.py @@ -142,8 +142,8 @@ def send_events_after_publish(publish_log_pk: int, library_key_str: str) -> None pass else: log.warning( - f"PublishableEntity {record.entity.pk} / {record.entity.key} was modified during publish operation " - "but is of unknown type." + f"PublishableEntity {record.entity.pk} / {record.entity.entity_ref} " + "was modified during publish operation but is of unknown type." ) for container_key in affected_containers: @@ -246,17 +246,17 @@ def send_events_after_revert(draft_change_log_id: int, library_key_str: str) -> updated_container_keys.add(container_key) else: log.warning( - f"PublishableEntity {record.entity.pk} / {record.entity.key} was modified during publish operation " - "but is of unknown type." + f"PublishableEntity {record.entity.pk} / {record.entity.entity_ref} " + "was modified during publish operation but is of unknown type." ) # If any collections contain this entity, their item count may need to be updated, e.g. if this was a # newly created component in the collection and is now deleted, or this was deleted and is now re-added. for parent_collection in content_api.get_entity_collections( - record.entity.learning_package_id, record.entity.key, + record.entity.learning_package_id, record.entity.entity_ref, ): collection_key = api.library_collection_locator( library_key=library_key, - collection_key=parent_collection.key, + collection_key=parent_collection.collection_code, ) affected_collection_keys.add(collection_key) @@ -541,7 +541,7 @@ def backup_library(self, user_id: int, library_key_str: str) -> None: file_path = os.path.join(root_dir, filename) user = User.objects.get(id=user_id) origin_server = getattr(settings, 'CMS_BASE', None) - create_lib_zip_file(lp_key=str(library_key), path=file_path, user=user, origin_server=origin_server) + create_lib_zip_file(package_ref=str(library_key), path=file_path, user=user, origin_server=origin_server) set_custom_attribute("exporting_completed", str(library_key)) with open(file_path, 'rb') as zipfile: @@ -651,7 +651,7 @@ def restore_library(self, user_id, storage_path): TASK_LOGGER.info( 'Restored learning package (id: %s) with key %s', learning_package_data.get('id'), - learning_package_data.get('key') + learning_package_data.get('package_ref') ) # Save the restore details as an artifact in JSON format diff --git a/openedx/core/djangoapps/content_libraries/tests/test_api.py b/openedx/core/djangoapps/content_libraries/tests/test_api.py index 408d16618569..cc0564964fb8 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_api.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_api.py @@ -115,7 +115,7 @@ def test_create_library_collection(self) -> None: description="Description for Collection 4", created_by=self.user.id, ) - assert collection.key == "COL4" + assert collection.collection_code == "COL4" assert collection.title == "Collection 4" assert collection.description == "Description for Collection 4" assert collection.created_by == self.user @@ -150,10 +150,10 @@ def test_update_library_collection(self) -> None: self.col1 = api.update_library_collection( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, title="New title for Collection 1", ) - assert self.col1.key == "COL1" + assert self.col1.collection_code == "COL1" assert self.col1.title == "New title for Collection 1" assert self.col1.description == "Description for Collection 1" assert self.col1.created_by == self.user @@ -177,7 +177,7 @@ def test_update_library_collection_wrong_library(self) -> None: with self.assertRaises(api.ContentLibraryCollectionNotFound) as exc: # noqa: F841, PT027 api.update_library_collection( self.lib1.library_key, - self.col2.key, + self.col2.collection_code, ) def test_delete_library_collection(self) -> None: @@ -187,7 +187,7 @@ def test_delete_library_collection(self) -> None: assert self.lib1.learning_package_id is not None content_api.delete_collection( self.lib1.learning_package_id, - self.col1.key, + self.col1.collection_code, hard_delete=True, ) @@ -211,7 +211,7 @@ def test_update_library_collection_items(self) -> None: self.col1 = api.update_library_collection_items( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_problem_block["id"]), LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]), @@ -222,7 +222,7 @@ def test_update_library_collection_items(self) -> None: self.col1 = api.update_library_collection_items( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]), ], @@ -240,7 +240,7 @@ def test_update_library_collection_components_event(self) -> None: api.update_library_collection_items( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_problem_block["id"]), LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]), @@ -300,7 +300,7 @@ def test_update_collection_components_from_wrong_library(self) -> None: with self.assertRaises(api.ContentLibraryBlockNotFound) as exc: # noqa: PT027 api.update_library_collection_items( self.lib2.library_key, - self.col2.key, + self.col2.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_problem_block["id"]), LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]), @@ -318,13 +318,15 @@ def test_set_library_component_collections(self) -> None: component = api.get_component_from_usage_key(UsageKeyV2.from_string(self.lib2_problem_block["id"])) api.set_library_item_collections( library_key=self.lib2.library_key, - entity_key=component.publishable_entity.key, - collection_keys=[self.col2.key, self.col3.key], + entity_ref=component.publishable_entity.entity_ref, + collection_keys=[self.col2.collection_code, self.col3.collection_code], ) assert self.lib2.learning_package_id is not None - assert len(content_api.get_collection(self.lib2.learning_package_id, self.col2.key).entities.all()) == 1 - assert len(content_api.get_collection(self.lib2.learning_package_id, self.col3.key).entities.all()) == 1 + col2 = content_api.get_collection(self.lib2.learning_package_id, self.col2.collection_code) + col3 = content_api.get_collection(self.lib2.learning_package_id, self.col3.collection_code) + assert len(col2.entities.all()) == 1 + assert len(col3.entities.all()) == 1 self.assertDictContainsEntries( event_receiver.call_args_list[0].kwargs, @@ -343,11 +345,15 @@ def test_set_library_component_collections(self) -> None: assert all(event["signal"] == LIBRARY_COLLECTION_UPDATED for event in collection_update_events) assert {event["library_collection"] for event in collection_update_events} == { LibraryCollectionData( - collection_key=api.library_collection_locator(self.lib2.library_key, collection_key=self.col2.key), + collection_key=api.library_collection_locator( + self.lib2.library_key, collection_key=self.col2.collection_code, + ), background=True, ), LibraryCollectionData( - collection_key=api.library_collection_locator(self.lib2.library_key, collection_key=self.col3.key), + collection_key=api.library_collection_locator( + self.lib2.library_key, collection_key=self.col3.collection_code, + ), background=True, ) } @@ -355,7 +361,7 @@ def test_set_library_component_collections(self) -> None: def test_delete_library_block(self) -> None: api.update_library_collection_items( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_problem_block["id"]), LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]), @@ -376,7 +382,7 @@ def test_delete_library_block(self) -> None: "library_collection": LibraryCollectionData( collection_key=api.library_collection_locator( self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), background=True, ), @@ -386,7 +392,7 @@ def test_delete_library_block(self) -> None: def test_delete_library_container(self) -> None: api.update_library_collection_items( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_problem_block["id"]), LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]), @@ -415,7 +421,7 @@ def test_delete_library_container(self) -> None: "library_collection": LibraryCollectionData( collection_key=api.library_collection_locator( self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), background=True, ), @@ -499,7 +505,7 @@ def test_delete_library_block_when_component_does_not_exist(self) -> None: def test_restore_library_block(self) -> None: api.update_library_collection_items( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_problem_block["id"]), LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]), @@ -520,7 +526,7 @@ def test_restore_library_block(self) -> None: "library_collection": LibraryCollectionData( collection_key=api.library_collection_locator( self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), background=True, ), @@ -539,7 +545,7 @@ def test_add_component_and_revert(self) -> None: # Add component. Note: collections are not part of the draft/publish cycle so this is not a draft change. api.update_library_collection_items( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]), LibraryUsageLocatorV2.from_string(new_problem_block["id"]), @@ -560,7 +566,7 @@ def test_add_component_and_revert(self) -> None: "library_collection": LibraryCollectionData( collection_key=api.library_collection_locator( self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), ), }, @@ -574,7 +580,7 @@ def test_delete_component_and_revert(self) -> None: # Add components and publish api.update_library_collection_items( self.lib1.library_key, - self.col1.key, + self.col1.collection_code, opaque_keys=[ LibraryUsageLocatorV2.from_string(self.lib1_problem_block["id"]), LibraryUsageLocatorV2.from_string(self.lib1_html_block["id"]) @@ -599,7 +605,7 @@ def test_delete_component_and_revert(self) -> None: "library_collection": LibraryCollectionData( collection_key=api.library_collection_locator( self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), ), }, diff --git a/openedx/core/djangoapps/content_libraries/tests/test_containers.py b/openedx/core/djangoapps/content_libraries/tests/test_containers.py index 37ad621d26e6..a95b238b78a4 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_containers.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_containers.py @@ -591,7 +591,7 @@ def test_unit_collections(self) -> None: result = self._patch_container_collections( self.unit["id"], - collection_keys=[col1.key], + collection_keys=[col1.collection_code], ) assert result['count'] == 1 @@ -600,7 +600,7 @@ def test_unit_collections(self) -> None: unit_as_read = self._get_container(self.unit["id"]) # Verify the collections - assert unit_as_read['collections'] == [{"title": col1.title, "key": col1.key}] + assert unit_as_read['collections'] == [{"title": col1.title, "key": col1.collection_code}] def test_section_hierarchy(self): with self.assertNumQueries(126): diff --git a/openedx/core/djangoapps/content_libraries/tests/test_views_collections.py b/openedx/core/djangoapps/content_libraries/tests/test_views_collections.py index 270580cb2a61..9f8e5dc1cde3 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_views_collections.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_views_collections.py @@ -88,7 +88,7 @@ def test_get_library_collection(self): Test retrieving a Content Library Collection """ resp = self.client.get( - URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.key) + URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.collection_code) ) # Check that correct Content Library Collection data retrieved @@ -103,7 +103,7 @@ def test_get_library_collection(self): random_user = UserFactory.create(username="Random", email="random@example.com") with self.as_user(random_user): resp = self.client.get( - URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.key) + URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.collection_code) ) assert resp.status_code == 403 @@ -113,7 +113,7 @@ def test_get_invalid_library_collection(self): """ # Fetch collection that belongs to a different library, it should fail resp = self.client.get( - URL_LIB_COLLECTION.format(lib_key=self.lib1.library_key, collection_key=self.col3.key) + URL_LIB_COLLECTION.format(lib_key=self.lib1.library_key, collection_key=self.col3.collection_code) ) assert resp.status_code == 404 @@ -247,9 +247,9 @@ def test_create_invalid_library_collection(self): assert resp.status_code == 400 - # Create collection with an existing collection.key; it should fail + # Create collection with an existing collection.collection_code; it should fail post_data_existing_key = { - "key": self.col1.key, + "key": self.col1.collection_code, "title": "Collection 4", } resp = self.client.post( @@ -275,7 +275,7 @@ def test_update_library_collection(self): "title": "Collection 3 Updated", } resp = self.client.patch( - URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.key), + URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.collection_code), patch_data, format="json" ) @@ -297,7 +297,7 @@ def test_update_library_collection(self): "title": "Collection 3 should not update", } resp = self.client.patch( - URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.key), + URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.collection_code), patch_data, format="json" ) @@ -313,7 +313,7 @@ def test_update_invalid_library_collection(self): } # Update collection that belongs to a different library, it should fail resp = self.client.patch( - URL_LIB_COLLECTION.format(lib_key=self.lib1.library_key, collection_key=self.col3.key), + URL_LIB_COLLECTION.format(lib_key=self.lib1.library_key, collection_key=self.col3.collection_code), patch_data, format="json" ) @@ -331,7 +331,7 @@ def test_update_invalid_library_collection(self): # Update collection with invalid library_key provided, it should fail resp = self.client.patch( - URL_LIB_COLLECTION.format(lib_key=123, collection_key=self.col3.key), + URL_LIB_COLLECTION.format(lib_key=123, collection_key=self.col3.collection_code), patch_data, format="json" ) @@ -342,22 +342,22 @@ def test_delete_library_collection(self): Test soft-deleting and restoring a Content Library Collection """ resp = self.client.delete( - URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.key) + URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.collection_code) ) assert resp.status_code == 204 resp = self.client.get( - URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.key) + URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.collection_code) ) assert resp.status_code == 404 resp = self.client.post( - URL_LIB_COLLECTION_RESTORE.format(lib_key=self.lib2.library_key, collection_key=self.col3.key) + URL_LIB_COLLECTION_RESTORE.format(lib_key=self.lib2.library_key, collection_key=self.col3.collection_code) ) assert resp.status_code == 204 resp = self.client.get( - URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.key) + URL_LIB_COLLECTION.format(lib_key=self.lib2.library_key, collection_key=self.col3.collection_code) ) # Check that correct Content Library Collection data retrieved expected_collection = { @@ -375,7 +375,7 @@ def test_get_components(self): resp = self.client.get( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), ) assert resp.status_code == 405 @@ -388,7 +388,7 @@ def test_update_components(self): resp = self.client.patch( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), data={ "usage_keys": [ @@ -404,7 +404,7 @@ def test_update_components(self): resp = self.client.delete( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), data={ "usage_keys": [ @@ -423,7 +423,7 @@ def test_update_containers(self): resp = self.client.patch( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), data={ "usage_keys": [ @@ -440,7 +440,7 @@ def test_update_containers(self): resp = self.client.delete( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), data={ "usage_keys": [ @@ -460,7 +460,7 @@ def test_update_components_wrong_collection(self, method): resp = getattr(self.client, method)( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib2.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), data={ "usage_keys": [ @@ -478,7 +478,7 @@ def test_update_components_missing_data(self, method): resp = getattr(self.client, method)( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib2.library_key, - collection_key=self.col3.key, + collection_key=self.col3.collection_code, ), ) assert resp.status_code == 400 @@ -494,7 +494,7 @@ def test_update_components_from_another_library(self, method): resp = getattr(self.client, method)( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib2.library_key, - collection_key=self.col3.key, + collection_key=self.col3.collection_code, ), data={ "usage_keys": [ @@ -515,7 +515,7 @@ def test_update_components_permissions(self, method): resp = getattr(self.client, method)( URL_LIB_COLLECTION_COMPONENTS.format( lib_key=self.lib1.library_key, - collection_key=self.col1.key, + collection_key=self.col1.collection_code, ), ) assert resp.status_code == 403 diff --git a/openedx/core/djangoapps/video_config/transcripts_utils.py b/openedx/core/djangoapps/video_config/transcripts_utils.py index c76a2b8b0377..6e6f0fde7b8c 100644 --- a/openedx/core/djangoapps/video_config/transcripts_utils.py +++ b/openedx/core/djangoapps/video_config/transcripts_utils.py @@ -1016,7 +1016,7 @@ def get_transcript_from_openedx_content(video_block, language, output_format, tr .componentversionmedia_set .filter(media__has_file=True) .select_related('media') - .get(key=file_path) + .get(path=file_path) .media ) data = media.read_file().read() diff --git a/openedx/core/djangoapps/xblock/api.py b/openedx/core/djangoapps/xblock/api.py index 0ab620db9ab3..a4661f67b164 100644 --- a/openedx/core/djangoapps/xblock/api.py +++ b/openedx/core/djangoapps/xblock/api.py @@ -199,14 +199,14 @@ def get_component_from_usage_key(usage_key: UsageKeyV2) -> Component: This is a lower-level function that will return a Component even if there is no current draft version of that Component (because it's been soft-deleted). """ - learning_package = content_api.get_learning_package_by_key( + learning_package = content_api.get_learning_package_by_ref( str(usage_key.context_key) ) - return content_api.get_component_by_key( + return content_api.get_component_by_code( learning_package.id, namespace='xblock.v1', type_name=usage_key.block_type, - local_key=usage_key.block_id, + component_code=usage_key.block_id, ) @@ -232,9 +232,9 @@ def get_block_olx( raise NoSuchUsage(usage_key) # TODO: we should probably make a method on ComponentVersion that returns - # a content based on the name. Accessing by componentversionmedia__key is + # a content based on the name. Accessing by componentversionmedia__path is # awkward. - content = component_version.media.get(componentversionmedia__key="block.xml") + content = component_version.media.get(componentversionmedia__path="block.xml") return content.text diff --git a/openedx/core/djangoapps/xblock/runtime/openedx_content_runtime.py b/openedx/core/djangoapps/xblock/runtime/openedx_content_runtime.py index 3a90fb27a5f7..1da6f048b1cc 100644 --- a/openedx/core/djangoapps/xblock/runtime/openedx_content_runtime.py +++ b/openedx/core/djangoapps/xblock/runtime/openedx_content_runtime.py @@ -191,7 +191,7 @@ def get_block(self, usage_key, for_parent=None, *, version: int | LatestVersion raise NoSuchUsage(usage_key) content = component_version.media.get( - componentversionmedia__key="block.xml" + componentversionmedia__path="block.xml" ) xml_node = etree.fromstring(content.text) block_type = usage_key.block_type @@ -251,13 +251,13 @@ def get_block_assets(self, block, fetch_asset_data): .componentversionmedia_set .filter(media__has_file=True) .select_related('media') - .order_by('key') + .order_by('path') ) return [ StaticFile( - name=cvm.key, - url=self._absolute_url_for_asset(component_version, cvm.key), + name=cvm.path, + url=self._absolute_url_for_asset(component_version, cvm.path), data=cvm.media.read_file().read() if fetch_asset_data else None, ) for cvm in cvm_list @@ -326,13 +326,13 @@ def _get_component_from_usage_key(self, usage_key): TODO: This is the third place where we're implementing this. Figure out where the definitive place should be and have everything else call that. """ - learning_package = content_api.get_learning_package_by_key(str(usage_key.lib_key)) + learning_package = content_api.get_learning_package_by_ref(str(usage_key.lib_key)) try: - component = content_api.get_component_by_key( + component = content_api.get_component_by_code( learning_package.id, namespace='xblock.v1', type_name=usage_key.block_type, - local_key=usage_key.block_id, + component_code=usage_key.block_id, ) except ObjectDoesNotExist as exc: raise NoSuchUsage(usage_key) from exc @@ -447,7 +447,7 @@ def _lookup_asset_url(self, block: XBlock, asset_path: str) -> str | None: component_version .componentversionmedia_set .filter(media__has_file=True) - .get(key=f"static/{asset_path}") + .get(path=f"static/{asset_path}") ) except ObjectDoesNotExist: try: @@ -458,7 +458,7 @@ def _lookup_asset_url(self, block: XBlock, asset_path: str) -> str | None: component_version .componentversionmedia_set .filter(media__has_file=True) - .get(key=f"static/{asset_path}") + .get(path=f"static/{asset_path}") ) except ObjectDoesNotExist: # This means we see a path that _looks_ like it should be a static diff --git a/requirements/constraints.txt b/requirements/constraints.txt index f051cc8f6114..73e9f6effa10 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -65,7 +65,7 @@ numpy<2.0.0 # breaking changes which openedx-core devs want to roll out manually. New patch versions # are OK to accept automatically. # Issue for unpinning: https://github.com/openedx/edx-platform/issues/35269 -openedx-core<0.40 +openedx-core<0.45 # Date: 2023-11-29 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 298f05e12aab..71790d659ff7 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -840,7 +840,7 @@ openedx-calc==5.0.0 # via # -r requirements/edx/kernel.in # xblocks-contrib -openedx-core==0.39.2 +openedx-core==0.44.0 # via # -c requirements/constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index ea0fd8e68eae..3ad4c94197fd 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1394,7 +1394,7 @@ openedx-calc==5.0.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # xblocks-contrib -openedx-core==0.39.2 +openedx-core==0.44.0 # via # -c requirements/constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 194b5438ce1f..b7f29a68f1df 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1018,7 +1018,7 @@ openedx-calc==5.0.0 # via # -r requirements/edx/base.txt # xblocks-contrib -openedx-core==0.39.2 +openedx-core==0.44.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index eb9732129f73..1e14484defc2 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1065,7 +1065,7 @@ openedx-calc==5.0.0 # via # -r requirements/edx/base.txt # xblocks-contrib -openedx-core==0.39.2 +openedx-core==0.44.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt