build!: Switch to openedx-core (renamed from openedx-learning) Instead of installing openedx-learning==0.32.0, we install openedx-core==0.34.1. We update various class names, function names, docstrings, and comments to represent the rename: * We say "openedx-core" when referring to the whole repo or PyPI project * or occasionally "Open edX Core" if we want it to look nice in the docs. * We say "openedx_content" to refer to the Content API within openedx-core, which is actually the thing we have been calling "Learning Core" all along. * In snake-case code, it's `*_openedx_content_*`. * In camel-case code, it's `*OpenedXContent*` For consistency's sake we avoid anything else like oex_core, OeXCore, OpenEdXCore, OexContent, openedx-content, OpenEdxContent, etc. There should be no more references to learning_core, learning-core, Learning Core, Learning-Core, LC, openedx-learning, openedx_learning, etc. BREAKING CHANGE: for openedx-learning/openedx-core developers: You may need to uninstall openedx-learning and re-install openedx-core from your venv. If running tutor, you may need to un-mount openedx-learning, rename the directory to openedx-core, re-mount it, and re-build. The code APIs themselves are fully backwards-compatible. Part of: https://github.com/openedx/openedx-core/issues/470
165 lines
7.8 KiB
Python
165 lines
7.8 KiB
Python
"""
|
|
XBlock runtime services for LegacyLibraryContentBlock
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from opaque_keys.edx.locator import LibraryLocator
|
|
from user_tasks.models import UserTaskStatus
|
|
|
|
from openedx.core.lib import ensure_cms
|
|
from openedx.core.djangoapps.content_libraries import tasks as library_tasks
|
|
from xmodule.library_content_block import LegacyLibraryContentBlock
|
|
from xmodule.modulestore import ModuleStoreEnum
|
|
from xmodule.modulestore.exceptions import ItemNotFoundError
|
|
|
|
|
|
def normalize_key_for_search(library_key):
|
|
""" Normalizes library key for use with search indexing """
|
|
return library_key.replace(version_guid=None, branch=None)
|
|
|
|
|
|
class LegacyLibraryToolsService:
|
|
"""
|
|
Service for LegacyLibraryContentBlock.
|
|
|
|
Allows to interact with libraries in the modulestore and openedx_content.
|
|
|
|
Should only be used in the CMS.
|
|
"""
|
|
def __init__(self, modulestore, user_id):
|
|
self.store = modulestore
|
|
self.user_id = user_id
|
|
|
|
def get_latest_library_version(self, library_id: str | LibraryLocator) -> str | None:
|
|
"""
|
|
Get the version of the given library as string.
|
|
|
|
The return value (library version) could be:
|
|
str(<ObjectID>) - for V1 library;
|
|
None - if the library does not exist.
|
|
"""
|
|
library_key: LibraryLocator
|
|
if isinstance(library_id, str):
|
|
library_key = LibraryLocator.from_string(library_id)
|
|
else:
|
|
library_key = library_id
|
|
library_key = library_key.for_branch(ModuleStoreEnum.BranchName.library).for_version(None)
|
|
try:
|
|
library = self.store.get_library(
|
|
library_key, remove_version=False, remove_branch=False, head_validation=False
|
|
)
|
|
except ItemNotFoundError:
|
|
return None
|
|
if not library:
|
|
return None
|
|
# We need to know the library's version so ensure it's set in library.location.library_key.version_guid
|
|
assert library.location.library_key.version_guid is not None
|
|
return str(library.location.library_key.version_guid)
|
|
|
|
def can_use_library_content(self, block):
|
|
"""
|
|
Determines whether a modulestore holding a course_id supports libraries.
|
|
"""
|
|
return self.store.check_supports(block.location.course_key, 'copy_from_template')
|
|
|
|
def trigger_library_sync(self, dest_block: LegacyLibraryContentBlock, library_version: str | None) -> None:
|
|
"""
|
|
Queue task to synchronize the children of `dest_block` with it source library (at `library_version` or latest).
|
|
|
|
Raises ObjectDoesNotExist if library/version cannot be found.
|
|
|
|
The task will:
|
|
* Load that library at `dest_block.source_library_id` and `library_version`.
|
|
* If `library_version` is None, load the latest.
|
|
* Update `dest_block.source_library_version` based on what is loaded.
|
|
* Ensure that `dest_block` has children corresponding to all matching source library blocks.
|
|
* Considered fields of `dest_block` include: `source_library_id`, `source_library_version`, `capa_type`.
|
|
library version, and upate `dest_block.source_library_version` to match.
|
|
* Derive each child block id as a function of `dest_block`'s id and the library block's definition id.
|
|
* Follow these important create/update/delete semantics for children:
|
|
* When a matching library child DOES NOT EXIT in `dest_block.children`: import it in as a new block.
|
|
* When a matching library child ALREADY EXISTS in `dest_block.children`: re-import its definition, clobbering
|
|
any content updates in this existing child, but preserving any settings overrides in the existing child.
|
|
* When a block in `dest_block.children` DOES NOT MATCH any library children: delete it from
|
|
`dest_block.children`.
|
|
"""
|
|
ensure_cms("library_content block children may only be synced in a CMS context")
|
|
if not isinstance(dest_block, LegacyLibraryContentBlock):
|
|
raise ValueError(f"Can only sync children for library_content blocks, not {dest_block.tag} blocks.")
|
|
if not dest_block.source_library_id:
|
|
dest_block.source_library_version = ""
|
|
return
|
|
library_key = dest_block.source_library_key.for_branch(
|
|
ModuleStoreEnum.BranchName.library
|
|
).for_version(library_version)
|
|
try:
|
|
self.store.get_library(library_key, remove_version=False, remove_branch=False, head_validation=False)
|
|
except ItemNotFoundError as exc:
|
|
if library_version:
|
|
raise ObjectDoesNotExist(f"Version {library_version} of library {library_key} not found.") from exc
|
|
raise ObjectDoesNotExist(f"Library {library_key} not found.") from exc
|
|
|
|
# TODO: This task is synchronous until we can figure out race conditions with import.
|
|
# These race conditions lead to failed imports of library content from course import.
|
|
# See: TNL-11339, https://github.com/openedx/edx-platform/issues/34029 for more info.
|
|
library_tasks.sync_from_library.apply(
|
|
kwargs=dict(
|
|
user_id=self.user_id,
|
|
dest_block_id=str(dest_block.scope_ids.usage_id),
|
|
library_version=library_version,
|
|
),
|
|
)
|
|
|
|
def trigger_duplication(
|
|
self, source_block: LegacyLibraryContentBlock, dest_block: LegacyLibraryContentBlock
|
|
) -> None:
|
|
"""
|
|
Queue a task to duplicate the children of `source_block` to `dest_block`.
|
|
"""
|
|
ensure_cms("library_content block children may only be duplicated in a CMS context")
|
|
if not isinstance(dest_block, LegacyLibraryContentBlock):
|
|
raise ValueError(f"Can only duplicate children for library_content blocks, not {dest_block.tag} blocks.")
|
|
if source_block.scope_ids.usage_id.context_key != source_block.scope_ids.usage_id.context_key:
|
|
raise ValueError(
|
|
"Cannot duplicate_children across different learning contexts "
|
|
f"(source={source_block.scope_ids.usage_id}, dest={dest_block.scope_ids.usage_id})"
|
|
)
|
|
if source_block.source_library_key != dest_block.source_library_key:
|
|
raise ValueError(
|
|
"Cannot duplicate_children across different source libraries or versions thereof "
|
|
f"({source_block.source_library_key=}, {dest_block.source_library_key=})."
|
|
)
|
|
library_tasks.duplicate_children.delay(
|
|
user_id=self.user_id,
|
|
source_block_id=str(source_block.scope_ids.usage_id),
|
|
dest_block_id=str(dest_block.scope_ids.usage_id),
|
|
)
|
|
|
|
def are_children_syncing(self, library_content_block: LegacyLibraryContentBlock) -> bool:
|
|
"""
|
|
Is a task currently running to sync the children of `library_content_block`?
|
|
|
|
Only checks the latest task (so that this block's state can't get permanently messed up by
|
|
some older task that's stuck in PENDING).
|
|
"""
|
|
args = {'dest_block_id': library_content_block.scope_ids.usage_id}
|
|
name = library_tasks.LibrarySyncChildrenTask.generate_name(args)
|
|
status = UserTaskStatus.objects.filter(name=name).order_by('-created').first()
|
|
return status and status.state in [
|
|
UserTaskStatus.IN_PROGRESS, UserTaskStatus.PENDING, UserTaskStatus.RETRYING
|
|
]
|
|
|
|
def list_available_libraries(self):
|
|
"""
|
|
List all known legacy libraries.
|
|
|
|
Returns tuples of (library key, display_name).
|
|
"""
|
|
user = User.objects.get(id=self.user_id)
|
|
return [
|
|
(lib.location.library_key.replace(version_guid=None, branch=None), lib.display_name)
|
|
for lib in self.store.get_library_summaries()
|
|
]
|