feat!: Remove outdated Libraries Relaunch cruft (#35644)
The V2 libraries project had a few past iterations which were never launched. This commit cleans up pieces from those which we don't need for the real Libraries Relaunch MVP in Sumac: * Remove ENABLE_LIBRARY_AUTHORING_MICROFRONTEND, LIBRARY_AUTHORING_FRONTEND_URL, and REDIRECT_TO_LIBRARY_AUTHORING_MICROFRONTEND, all of which are obsolete now that library authoring has been merged into https://github.com/openedx/frontend-app-authoring. More details on the new Content Libraries configuration settings are here: https://github.com/openedx/frontend-app-authoring/issues/1334 * Remove dangling support for syncing V2 (learning core-backed) library content using the LibraryContentBlock. This code was all based on an older understanding of V2 Content Libraries, where the libraries were smaller and versioned as a whole rather then versioned by-item. Reference to V2 libraries will be done on a per-block basis using the upstream/downstream system, described here: https://github.com/openedx/edx-platform/blob/master/docs/decisions/0020-upstream-downstream.rst It's important that we remove this support now so that OLX course authors don't stuble upon it and use it, which would be buggy and complicate future migrations. * Remove the "mode" parameter from LibraryContentBlock. The only supported mode was and is "random". We will not be adding any further modes. Going forward for V2, we will have an ItemBank block for randomizing items (regardless of source), which can be synthesized with upstream referenced as described above. Existing LibraryContentBlocks will be migrated. * Finally, some renamings: * LibraryContentBlock -> LegacyLibraryContentBlock * LibraryToolsService -> LegacyLibraryToolsService * LibrarySummary -> LegacyLibrarySummary Module names and the old OLX tag (library_content) are unchanged. Closes: https://github.com/openedx/frontend-app-authoring/issues/1115
This commit is contained in:
@@ -76,7 +76,6 @@ from opaque_keys.edx.locator import (
|
||||
LibraryLocator as LibraryLocatorV1,
|
||||
LibraryCollectionLocator,
|
||||
)
|
||||
from opaque_keys import InvalidKeyError
|
||||
from openedx_events.content_authoring.data import (
|
||||
ContentLibraryData,
|
||||
LibraryBlockData,
|
||||
@@ -99,10 +98,7 @@ from xblock.exceptions import XBlockNotFoundError
|
||||
|
||||
from openedx.core.djangoapps.xblock.api import get_component_from_usage_key, xblock_type_display_name
|
||||
from openedx.core.lib.xblock_serializer.api import serialize_modulestore_block_for_learning_core
|
||||
from xmodule.library_root_xblock import LibraryRoot as LibraryRootV1
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from . import permissions, tasks
|
||||
from .constants import ALL_RIGHTS_RESERVED, COMPLEX
|
||||
@@ -421,8 +417,8 @@ def get_library(library_key):
|
||||
# updated version of content that a course could pull in. But more recently,
|
||||
# we've decided to do those version references at the level of the
|
||||
# individual blocks being used, since a Learning Core backed library is
|
||||
# intended to be used for many LibraryContentBlocks and not 1:1 like v1
|
||||
# libraries. The top level version stays for now because LibraryContentBlock
|
||||
# intended to be referenced in multiple course locations and not 1:1 like v1
|
||||
# libraries. The top level version stays for now because LegacyLibraryContentBlock
|
||||
# uses it, but that should hopefully change before the Redwood release.
|
||||
version = 0 if last_publish_log is None else last_publish_log.pk
|
||||
published_by = None
|
||||
@@ -1340,77 +1336,6 @@ def get_library_collection_from_usage_key(
|
||||
raise ContentLibraryCollectionNotFound from exc
|
||||
|
||||
|
||||
# V1/V2 Compatibility Helpers
|
||||
# (Should be removed as part of
|
||||
# https://github.com/openedx/edx-platform/issues/32457)
|
||||
# ======================================================
|
||||
|
||||
def get_v1_or_v2_library(
|
||||
library_id: str | LibraryLocatorV1 | LibraryLocatorV2,
|
||||
version: str | int | None,
|
||||
) -> LibraryRootV1 | ContentLibraryMetadata | None:
|
||||
"""
|
||||
Fetch either a V1 or V2 content library from a V1/V2 key (or key string) and version.
|
||||
|
||||
V1 library versions are Mongo ObjectID strings.
|
||||
V2 library versions can be positive ints, or strings of positive ints.
|
||||
Passing version=None will return the latest version the library.
|
||||
|
||||
Returns None if not found.
|
||||
If key is invalid, raises InvalidKeyError.
|
||||
For V1, if key has a version, it is ignored in favor of `version`.
|
||||
For V2, if version is provided but it isn't an int or parseable to one, we raise a ValueError.
|
||||
|
||||
Examples:
|
||||
* get_v1_or_v2_library("library-v1:ProblemX+PR0B", None) -> <LibraryRootV1>
|
||||
* get_v1_or_v2_library("library-v1:ProblemX+PR0B", "65ff...") -> <LibraryRootV1>
|
||||
* get_v1_or_v2_library("lib:RG:rg-1", None) -> <ContentLibraryMetadata>
|
||||
* get_v1_or_v2_library("lib:RG:rg-1", "36") -> <ContentLibraryMetadata>
|
||||
* get_v1_or_v2_library("lib:RG:rg-1", "xyz") -> <ValueError>
|
||||
* get_v1_or_v2_library("notakey", "xyz") -> <InvalidKeyError>
|
||||
|
||||
If you just want to get a V2 library, use `get_library` instead.
|
||||
"""
|
||||
library_key: LibraryLocatorV1 | LibraryLocatorV2
|
||||
if isinstance(library_id, str):
|
||||
try:
|
||||
library_key = LibraryLocatorV1.from_string(library_id)
|
||||
except InvalidKeyError:
|
||||
library_key = LibraryLocatorV2.from_string(library_id)
|
||||
else:
|
||||
library_key = library_id
|
||||
if isinstance(library_key, LibraryLocatorV2):
|
||||
v2_version: int | None
|
||||
if version:
|
||||
v2_version = int(version)
|
||||
else:
|
||||
v2_version = None
|
||||
try:
|
||||
library = get_library(library_key)
|
||||
if v2_version is not None and library.version != v2_version:
|
||||
raise NotImplementedError(
|
||||
f"Tried to load version {v2_version} of learning_core-based library {library_key}. "
|
||||
f"Currently, only the latest version ({library.version}) may be loaded. "
|
||||
"This is a known issue. "
|
||||
"It will be fixed before the production release of learning_core-based (V2) content libraries. "
|
||||
)
|
||||
return library
|
||||
except ContentLibrary.DoesNotExist:
|
||||
return None
|
||||
elif isinstance(library_key, LibraryLocatorV1):
|
||||
v1_version: str | None
|
||||
if version:
|
||||
v1_version = str(version)
|
||||
else:
|
||||
v1_version = None
|
||||
store = modulestore()
|
||||
library_key = library_key.for_branch(ModuleStoreEnum.BranchName.library).for_version(v1_version)
|
||||
try:
|
||||
return store.get_library(library_key, remove_version=False, remove_branch=False, head_validation=False)
|
||||
except ItemNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
# Import from Courseware
|
||||
# ======================
|
||||
|
||||
|
||||
@@ -21,33 +21,20 @@ import logging
|
||||
from celery import shared_task
|
||||
from celery_utils.logged_task import LoggedTask
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from edx_django_utils.monitoring import set_code_owner_attribute, set_code_owner_attribute_from_module
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from opaque_keys.edx.locator import (
|
||||
BlockUsageLocator,
|
||||
LibraryLocatorV2,
|
||||
LibraryUsageLocatorV2,
|
||||
LibraryLocator as LibraryLocatorV1
|
||||
)
|
||||
|
||||
from user_tasks.tasks import UserTask, UserTaskStatus
|
||||
from xblock.fields import Scope
|
||||
|
||||
from common.djangoapps.student.auth import has_studio_write_access
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.content_libraries import api as library_api
|
||||
from openedx.core.djangoapps.xblock.api import load_block
|
||||
from opaque_keys.edx.locator import BlockUsageLocator
|
||||
from openedx.core.lib import ensure_cms
|
||||
from xmodule.capa_block import ProblemBlock
|
||||
from xmodule.library_content_block import ANY_CAPA_TYPE_VALUE, LibraryContentBlock
|
||||
from xmodule.library_root_xblock import LibraryRoot as LibraryRootV1
|
||||
from xmodule.library_content_block import ANY_CAPA_TYPE_VALUE, LegacyLibraryContentBlock
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.mixed import MixedModuleStore
|
||||
from xmodule.modulestore.split_mongo import BlockKey
|
||||
from xmodule.util.keys import derive_key
|
||||
|
||||
from . import api
|
||||
from .models import ContentLibraryBlockImportTask
|
||||
@@ -84,77 +71,6 @@ def import_blocks_from_course(import_task_id, course_key_str, use_course_key_as_
|
||||
)
|
||||
|
||||
|
||||
def _import_block(store, user_id, source_block, dest_parent_key):
|
||||
"""
|
||||
Recursively import a learning core block and its children.`
|
||||
"""
|
||||
def generate_block_key(source_key, dest_parent_key):
|
||||
"""
|
||||
Deterministically generate an ID for the new block and return the key.
|
||||
Keys are generated such that they appear identical to a v1 library with
|
||||
the same input block_id, library name, library organization, and parent block using derive_key
|
||||
"""
|
||||
if not isinstance(source_key.lib_key, LibraryLocatorV2):
|
||||
raise TypeError(f"Expected source library key of type LibraryLocatorV2, got {source_key.lib_key} instead.")
|
||||
source_key_as_v1_course_key = LibraryLocatorV1(
|
||||
org=source_key.lib_key.org,
|
||||
library=source_key.lib_key.slug,
|
||||
branch='library'
|
||||
)
|
||||
derived_block_key = derive_key(
|
||||
source=source_key_as_v1_course_key.make_usage_key(source_key.block_type, source_key.block_id),
|
||||
dest_parent=BlockKey(dest_parent_key.block_type, dest_parent_key.block_id),
|
||||
)
|
||||
return dest_parent_key.context_key.make_usage_key(*derived_block_key)
|
||||
|
||||
source_key = source_block.scope_ids.usage_id
|
||||
new_block_key = generate_block_key(source_key, dest_parent_key)
|
||||
try:
|
||||
new_block = store.get_item(new_block_key)
|
||||
if new_block.parent.block_id != dest_parent_key.block_id:
|
||||
raise ValueError(
|
||||
"Expected existing block {} to be a child of {} but instead it's a child of {}".format(
|
||||
new_block_key, dest_parent_key, new_block.parent,
|
||||
)
|
||||
)
|
||||
except ItemNotFoundError:
|
||||
new_block = store.create_child(
|
||||
user_id,
|
||||
dest_parent_key,
|
||||
source_key.block_type,
|
||||
block_id=new_block_key.block_id,
|
||||
)
|
||||
|
||||
# Prepare a list of this block's static assets; any assets that are referenced as /static/{path} (the
|
||||
# recommended way for referencing them) will stop working, and so we rewrite the url when importing.
|
||||
# Copying assets not advised because modulestore doesn't namespace assets to each block like learning core, which
|
||||
# might cause conflicts when the same filename is used across imported blocks.
|
||||
if isinstance(source_key, LibraryUsageLocatorV2):
|
||||
all_assets = library_api.get_library_block_static_asset_files(source_key)
|
||||
else:
|
||||
all_assets = []
|
||||
|
||||
for field_name, field in source_block.fields.items():
|
||||
if field.scope not in (Scope.settings, Scope.content):
|
||||
continue # Only copy authored field data
|
||||
if field.is_set_on(source_block) or field.is_set_on(new_block):
|
||||
field_value = getattr(source_block, field_name)
|
||||
setattr(new_block, field_name, field_value)
|
||||
new_block.save()
|
||||
store.update_item(new_block, user_id)
|
||||
|
||||
if new_block.has_children:
|
||||
# Delete existing children in the new block, which can be reimported again if they still exist in the
|
||||
# source library
|
||||
for existing_child_key in new_block.children:
|
||||
store.delete_item(existing_child_key, user_id)
|
||||
# Now import the children
|
||||
for child in source_block.get_children():
|
||||
_import_block(store, user_id, child, new_block_key)
|
||||
|
||||
return new_block_key
|
||||
|
||||
|
||||
def _filter_child(store, usage_key, capa_type):
|
||||
"""
|
||||
Return whether this block is both a problem and has a `capa_type` which is included in the filter.
|
||||
@@ -172,49 +88,6 @@ def _problem_type_filter(store, library, capa_type):
|
||||
return [key for key in library.children if _filter_child(store, key, capa_type)]
|
||||
|
||||
|
||||
def _import_from_learning_core(user_id, store, dest_block, source_block_ids):
|
||||
"""
|
||||
Imports a block from a learning-core-based learning context (usually a
|
||||
content library) into modulestore, as a new child of dest_block.
|
||||
Any existing children of dest_block are replaced.
|
||||
"""
|
||||
dest_key = dest_block.scope_ids.usage_id
|
||||
if not isinstance(dest_key, BlockUsageLocator):
|
||||
raise TypeError(f"Destination {dest_key} should be a modulestore course.")
|
||||
if user_id is None:
|
||||
raise ValueError("Cannot check user permissions - LibraryTools user_id is None")
|
||||
|
||||
if len(set(source_block_ids)) != len(source_block_ids):
|
||||
# We don't support importing the exact same block twice because it would break the way we generate new IDs
|
||||
# for each block and then overwrite existing copies of blocks when re-importing the same blocks.
|
||||
raise ValueError("One or more library component IDs is a duplicate.")
|
||||
|
||||
dest_course_key = dest_key.context_key
|
||||
user = User.objects.get(id=user_id)
|
||||
if not has_studio_write_access(user, dest_course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
# Read the source block; this will also confirm that user has permission to read it.
|
||||
# (This could be slow and use lots of memory, except for the fact that LibraryContentBlock which calls this
|
||||
# should be limiting the number of blocks to a reasonable limit. We load them all now instead of one at a
|
||||
# time in order to raise any errors before we start actually copying blocks over.)
|
||||
orig_blocks = [load_block(UsageKey.from_string(key), user) for key in source_block_ids]
|
||||
|
||||
with store.bulk_operations(dest_course_key):
|
||||
child_ids_updated = set()
|
||||
|
||||
for block in orig_blocks:
|
||||
new_block_id = _import_block(store, user_id, block, dest_key)
|
||||
child_ids_updated.add(new_block_id)
|
||||
|
||||
# Remove any existing children that are no longer used
|
||||
for old_child_id in set(dest_block.children) - child_ids_updated:
|
||||
store.delete_item(old_child_id, user_id)
|
||||
# If this was called from a handler, it will save dest_block at the end, so we must update
|
||||
# dest_block.children to avoid it saving the old value of children and deleting the new ones.
|
||||
dest_block.children = store.get_item(dest_key).children
|
||||
|
||||
|
||||
class LibrarySyncChildrenTask(UserTask): # pylint: disable=abstract-method
|
||||
"""
|
||||
Base class for tasks which operate upon library_content children.
|
||||
@@ -244,7 +117,7 @@ def sync_from_library(
|
||||
self: LibrarySyncChildrenTask,
|
||||
user_id: int,
|
||||
dest_block_id: str,
|
||||
library_version: str | int | None,
|
||||
library_version: str | None,
|
||||
) -> None:
|
||||
"""
|
||||
Celery task to update the children of the library_content block at `dest_block_id`.
|
||||
@@ -300,8 +173,8 @@ def _sync_children(
|
||||
task: LibrarySyncChildrenTask,
|
||||
store: MixedModuleStore,
|
||||
user_id: int,
|
||||
dest_block: LibraryContentBlock,
|
||||
library_version: int | str | None,
|
||||
dest_block: LegacyLibraryContentBlock,
|
||||
library_version: str | None,
|
||||
) -> None:
|
||||
"""
|
||||
Implementation helper for `sync_from_library` and `duplicate_children` Celery tasks.
|
||||
@@ -309,41 +182,29 @@ def _sync_children(
|
||||
Can update children with a specific library `library_version`, or latest (`library_version=None`).
|
||||
"""
|
||||
source_blocks = []
|
||||
library_key = dest_block.source_library_key
|
||||
filter_children = (dest_block.capa_type != ANY_CAPA_TYPE_VALUE)
|
||||
library = library_api.get_v1_or_v2_library(library_key, version=library_version)
|
||||
if not library:
|
||||
library_key = dest_block.source_library_key.for_branch(
|
||||
ModuleStoreEnum.BranchName.library
|
||||
).for_version(library_version)
|
||||
try:
|
||||
library = store.get_library(library_key, remove_version=False, remove_branch=False, head_validation=False)
|
||||
except ItemNotFoundError:
|
||||
task.status.fail(f"Requested library {library_key} not found.")
|
||||
elif isinstance(library, LibraryRootV1):
|
||||
if filter_children:
|
||||
# Apply simple filtering based on CAPA problem types:
|
||||
source_blocks.extend(_problem_type_filter(store, library, dest_block.capa_type))
|
||||
else:
|
||||
source_blocks.extend(library.children)
|
||||
with store.bulk_operations(dest_block.scope_ids.usage_id.context_key):
|
||||
try:
|
||||
dest_block.source_library_version = str(library.location.library_key.version_guid)
|
||||
store.update_item(dest_block, user_id)
|
||||
dest_block.children = store.copy_from_template(
|
||||
source_blocks, dest_block.location, user_id, head_validation=True
|
||||
)
|
||||
# ^-- copy_from_template updates the children in the DB
|
||||
# but we must also set .children here to avoid overwriting the DB again
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
TASK_LOGGER.exception('Error importing children for %s', dest_block.scope_ids.usage_id, exc_info=True)
|
||||
if task.status.state != UserTaskStatus.FAILED:
|
||||
task.status.fail({'raw_error_msg': str(exception)})
|
||||
raise
|
||||
elif isinstance(library, library_api.ContentLibraryMetadata):
|
||||
# TODO: add filtering by capa_type when V2 library will support different problem types
|
||||
return
|
||||
filter_children = (dest_block.capa_type != ANY_CAPA_TYPE_VALUE)
|
||||
if filter_children:
|
||||
# Apply simple filtering based on CAPA problem types:
|
||||
source_blocks.extend(_problem_type_filter(store, library, dest_block.capa_type))
|
||||
else:
|
||||
source_blocks.extend(library.children)
|
||||
with store.bulk_operations(dest_block.scope_ids.usage_id.context_key):
|
||||
try:
|
||||
source_block_ids = [
|
||||
str(library_api.LibraryXBlockMetadata.from_component(library_key, component).usage_key)
|
||||
for component in library_api.get_library_components(library_key)
|
||||
]
|
||||
_import_from_learning_core(user_id, store, dest_block, source_block_ids)
|
||||
dest_block.source_library_version = str(library.version)
|
||||
dest_block.source_library_version = str(library.location.library_key.version_guid)
|
||||
store.update_item(dest_block, user_id)
|
||||
dest_block.children = store.copy_from_template(
|
||||
source_blocks, dest_block.location, user_id, head_validation=True
|
||||
)
|
||||
# ^-- copy_from_template updates the children in the DB
|
||||
# but we must also set .children here to avoid overwriting the DB again
|
||||
except Exception as exception: # pylint: disable=broad-except
|
||||
TASK_LOGGER.exception('Error importing children for %s', dest_block.scope_ids.usage_id, exc_info=True)
|
||||
if task.status.state != UserTaskStatus.FAILED:
|
||||
@@ -354,8 +215,8 @@ def _sync_children(
|
||||
def _copy_overrides(
|
||||
store: MixedModuleStore,
|
||||
user_id: int,
|
||||
source_block: LibraryContentBlock,
|
||||
dest_block: LibraryContentBlock
|
||||
source_block: LegacyLibraryContentBlock,
|
||||
dest_block: LegacyLibraryContentBlock
|
||||
) -> None:
|
||||
"""
|
||||
Copy any overrides the user has made on children of `source` over to the children of `dest_block`, recursively.
|
||||
|
||||
@@ -452,7 +452,7 @@ def xblock_resource_pkg(block):
|
||||
ProblemBlock, and most other built-in blocks currently. Handling for these
|
||||
assets does not interact with this function.
|
||||
2. The (preferred) standard XBlock runtime resource loading system, used by
|
||||
LibraryContentBlock. Handling for these assets *does* interact with this
|
||||
LegacyLibraryContentBlock. Handling for these assets *does* interact with this
|
||||
function.
|
||||
|
||||
We hope to migrate to (2) eventually, tracked by:
|
||||
|
||||
@@ -10,7 +10,7 @@ from completion.test_utils import CompletionWaffleTestMixin
|
||||
from django.conf import settings
|
||||
from django.test import override_settings
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.library_tools import LibraryToolsService
|
||||
from xmodule.library_tools import LegacyLibraryToolsService
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory, LibraryFactory
|
||||
from xmodule.tests import prepare_block_runtime
|
||||
@@ -122,7 +122,7 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, SharedModuleStoreTest
|
||||
Bind a block (part of self.course) so we can access student-specific data.
|
||||
"""
|
||||
prepare_block_runtime(block.runtime, course_id=block.location.course_key)
|
||||
block.runtime._services.update({'library_tools': LibraryToolsService(self.store, self.user.id)}) # lint-amnesty, pylint: disable=protected-access
|
||||
block.runtime._services.update({'library_tools': LegacyLibraryToolsService(self.store, self.user.id)}) # lint-amnesty, pylint: disable=protected-access
|
||||
|
||||
def get_block(descriptor):
|
||||
"""Mocks module_system get_block_for_descriptor function"""
|
||||
|
||||
Reference in New Issue
Block a user