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:
@@ -1,54 +0,0 @@
|
||||
/* JavaScript for special editing operations that can be done on LibraryContentXBlock */
|
||||
// This is a temporary UI improvements that will be removed when V2 content libraries became
|
||||
// fully functional
|
||||
|
||||
/**
|
||||
* Toggle the "Problem Type" settings section depending on selected library type.
|
||||
* As for now, the V2 libraries don't support different problem types, so they can't be
|
||||
* filtered by it. We're hiding the Problem Type field for them.
|
||||
*/
|
||||
function checkProblemTypeShouldBeVisible(editor) {
|
||||
var libraries = editor.find('.wrapper-comp-settings.metadata_edit.is-active')
|
||||
.data().metadata.source_library_id.options;
|
||||
var selectedIndex = $("select[name='Library']", editor)[0].selectedIndex;
|
||||
var libraryKey = libraries[selectedIndex].value;
|
||||
var url = URI('/xblock')
|
||||
.segment(editor.find('.xblock.xblock-studio_view.xblock-studio_view-library_content.xblock-initialized')
|
||||
.data('usage-id'))
|
||||
.segment('handler')
|
||||
.segment('is_v2_library');
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: JSON.stringify({'library_key': libraryKey}),
|
||||
success: function(data) {
|
||||
var problemTypeSelect = editor.find("select[name='Problem Type']")
|
||||
.parents("li.field.comp-setting-entry.metadata_entry");
|
||||
data.is_v2 ? problemTypeSelect.hide() : problemTypeSelect.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits untill editor html loaded, than calls checks for Program Type field toggling.
|
||||
*/
|
||||
function waitForEditorLoading() {
|
||||
var checkContent = setInterval(function() {
|
||||
var $modal = $('.xblock-editor');
|
||||
var content = $modal.html();
|
||||
if (content) {
|
||||
clearInterval(checkContent);
|
||||
checkProblemTypeShouldBeVisible($modal);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
// Initial call
|
||||
waitForEditorLoading();
|
||||
|
||||
var $librarySelect = $("select[name='Library']");
|
||||
$(document).on('change', $librarySelect, waitForEditorLoading)
|
||||
|
||||
var $libraryContentEditors = $('.xblock-header.xblock-header-library_content');
|
||||
var $editBtns = $libraryContentEditors.find('.action-item.action-edit');
|
||||
$(document).on('click', $editBtns, waitForEditorLoading)
|
||||
@@ -1,5 +1,12 @@
|
||||
"""
|
||||
LibraryContent: The XBlock used to include blocks from a library in a course.
|
||||
LegacyLibraryContent: The XBlock used to randomly select a subset of blocks from a "v1" (modulestore-backed) library.
|
||||
|
||||
In Studio, it's called the "Randomized Content Module".
|
||||
|
||||
In the long-term, this block is deprecated in favor of "v2" (learning core-backed) library references:
|
||||
https://github.com/openedx/edx-platform/issues/32457
|
||||
|
||||
We need to retain backwards-compatibility, but please do not build any new features into this.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -15,8 +22,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.utils.functional import classproperty
|
||||
from lxml import etree
|
||||
from lxml.etree import XMLSyntaxError
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
from rest_framework import status
|
||||
from web_fragments.fragment import Fragment
|
||||
from webob import Response
|
||||
@@ -78,7 +84,7 @@ class LibraryToolsUnavailable(ValueError):
|
||||
@XBlock.wants('studio_user_permissions') # Only available in CMS.
|
||||
@XBlock.wants('user')
|
||||
@XBlock.needs('mako')
|
||||
class LibraryContentBlock(
|
||||
class LegacyLibraryContentBlock(
|
||||
MakoTemplateBlockBase,
|
||||
XmlMixin,
|
||||
XModuleToXBlockMixin,
|
||||
@@ -87,7 +93,7 @@ class LibraryContentBlock(
|
||||
StudioEditableBlock,
|
||||
):
|
||||
"""
|
||||
An XBlock whose children are chosen dynamically from a content library.
|
||||
An XBlock whose children are chosen dynamically from a legacy (v1) content library.
|
||||
Can be used to create randomized assessments among other things.
|
||||
|
||||
Note: technically, all matching blocks from the content library are added
|
||||
@@ -135,17 +141,6 @@ class LibraryContentBlock(
|
||||
display_name=_("Library Version"),
|
||||
scope=Scope.settings,
|
||||
)
|
||||
mode = String(
|
||||
display_name=_("Mode"),
|
||||
help=_("Determines how content is drawn from the library"),
|
||||
default="random",
|
||||
values=[
|
||||
{"display_name": _("Choose n at random"), "value": "random"}
|
||||
# Future addition: Choose a new random set of n every time the student refreshes the block, for self tests
|
||||
# Future addition: manually selected blocks
|
||||
],
|
||||
scope=Scope.settings,
|
||||
)
|
||||
max_count = Integer(
|
||||
display_name=_("Count"),
|
||||
help=_("Enter the number of components to display to each student. Set it to -1 to display all components."),
|
||||
@@ -179,15 +174,12 @@ class LibraryContentBlock(
|
||||
"""
|
||||
Convenience method to get the library ID as a LibraryLocator and not just a string.
|
||||
|
||||
Supports either library v1 or library v2 locators.
|
||||
Supports only v1 libraries.
|
||||
"""
|
||||
try:
|
||||
return LibraryLocator.from_string(self.source_library_id)
|
||||
except InvalidKeyError:
|
||||
return LibraryLocatorV2.from_string(self.source_library_id)
|
||||
return LibraryLocator.from_string(self.source_library_id)
|
||||
|
||||
@classmethod
|
||||
def make_selection(cls, selected, children, max_count, mode):
|
||||
def make_selection(cls, selected, children, max_count):
|
||||
"""
|
||||
Dynamically selects block_ids indicating which of the possible children are displayed to the current user.
|
||||
|
||||
@@ -195,7 +187,6 @@ class LibraryContentBlock(
|
||||
selected - list of (block_type, block_id) tuples assigned to this student
|
||||
children - children of this block
|
||||
max_count - number of components to display to each student
|
||||
mode - how content is drawn from the library
|
||||
|
||||
Returns:
|
||||
A dict containing the following keys:
|
||||
@@ -231,12 +222,9 @@ class LibraryContentBlock(
|
||||
if num_to_add > 0:
|
||||
# We need to select [more] blocks to display to this user:
|
||||
pool = valid_block_keys - selected_keys
|
||||
if mode == "random":
|
||||
num_to_add = min(len(pool), num_to_add)
|
||||
added_block_keys = set(rand.sample(list(pool), num_to_add))
|
||||
# We now have the correct n random children to show for this user.
|
||||
else:
|
||||
raise NotImplementedError("Unsupported mode.")
|
||||
num_to_add = min(len(pool), num_to_add)
|
||||
added_block_keys = set(rand.sample(list(pool), num_to_add))
|
||||
# We now have the correct n random children to show for this user.
|
||||
selected_keys |= added_block_keys
|
||||
|
||||
if any((invalid_block_keys, overlimit_block_keys, added_block_keys)):
|
||||
@@ -334,7 +322,7 @@ class LibraryContentBlock(
|
||||
if max_count < 0:
|
||||
max_count = len(self.children)
|
||||
|
||||
block_keys = self.make_selection(self.selected, self.children, max_count, "random") # pylint: disable=no-member
|
||||
block_keys = self.make_selection(self.selected, self.children, max_count) # pylint: disable=no-member
|
||||
|
||||
# Publish events for analytics purposes:
|
||||
lib_tools = self.get_tools()
|
||||
@@ -467,7 +455,6 @@ class LibraryContentBlock(
|
||||
fragment = Fragment(
|
||||
self.runtime.service(self, 'mako').render_cms_template(self.mako_template, self.get_context())
|
||||
)
|
||||
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/library_content_edit_helpers.js'))
|
||||
add_webpack_js_to_fragment(fragment, 'LibraryContentBlockEditor')
|
||||
shim_xmodule_js(fragment, self.studio_js_module_name)
|
||||
return fragment
|
||||
@@ -481,16 +468,12 @@ class LibraryContentBlock(
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super().non_editable_metadata_fields
|
||||
# The only supported mode is currently 'random'.
|
||||
# Add the mode field to non_editable_metadata_fields so that it doesn't
|
||||
# render in the edit form.
|
||||
non_editable_fields.extend([
|
||||
LibraryContentBlock.mode,
|
||||
LibraryContentBlock.source_library_version,
|
||||
LegacyLibraryContentBlock.source_library_version,
|
||||
])
|
||||
return non_editable_fields
|
||||
|
||||
def get_tools(self, to_read_library_content: bool = False) -> 'LibraryToolsService':
|
||||
def get_tools(self, to_read_library_content: bool = False) -> 'LegacyLibraryToolsService':
|
||||
"""
|
||||
Grab the library tools service and confirm that it'll work for us. Else, raise LibraryToolsUnavailable.
|
||||
"""
|
||||
@@ -564,22 +547,6 @@ class LibraryContentBlock(
|
||||
library_version=(None if upgrade_to_latest else self.source_library_version),
|
||||
)
|
||||
|
||||
@XBlock.json_handler
|
||||
def is_v2_library(self, data, suffix=''): # pylint: disable=unused-argument
|
||||
"""
|
||||
Check the library version by library_id.
|
||||
|
||||
This is a temporary handler needed for hiding the Problem Type xblock editor field for V2 libraries.
|
||||
"""
|
||||
lib_key = data.get('library_key')
|
||||
try:
|
||||
LibraryLocatorV2.from_string(lib_key)
|
||||
except InvalidKeyError:
|
||||
is_v2 = False
|
||||
else:
|
||||
is_v2 = True
|
||||
return {'is_v2': is_v2}
|
||||
|
||||
@XBlock.handler
|
||||
def children_are_syncing(self, request, suffix=''): # pylint: disable=unused-argument
|
||||
"""
|
||||
@@ -809,14 +776,14 @@ class LibraryContentBlock(
|
||||
return xml_object
|
||||
|
||||
|
||||
class LibrarySummary:
|
||||
class LegacyLibrarySummary:
|
||||
"""
|
||||
A library summary object which contains the fields required for library listing on studio.
|
||||
"""
|
||||
|
||||
def __init__(self, library_locator, display_name):
|
||||
"""
|
||||
Initialize LibrarySummary
|
||||
Initialize LegacyLibrarySummary
|
||||
|
||||
Arguments:
|
||||
library_locator (LibraryLocator): LibraryLocator object of the library.
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
"""
|
||||
XBlock runtime services for LibraryContentBlock
|
||||
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.conf import settings
|
||||
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 api as library_api
|
||||
from openedx.core.djangoapps.content_libraries import tasks as library_tasks
|
||||
from xmodule.library_content_block import LibraryContentBlock
|
||||
from xmodule.library_root_xblock import LibraryRoot as LibraryRootV1
|
||||
from xmodule.library_content_block import LegacyLibraryContentBlock
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
@@ -21,9 +20,9 @@ def normalize_key_for_search(library_key):
|
||||
return library_key.replace(version_guid=None, branch=None)
|
||||
|
||||
|
||||
class LibraryToolsService:
|
||||
class LegacyLibraryToolsService:
|
||||
"""
|
||||
Service for LibraryContentBlock.
|
||||
Service for LegacyLibraryContentBlock.
|
||||
|
||||
Allows to interact with libraries in the modulestore and learning core.
|
||||
|
||||
@@ -33,24 +32,31 @@ class LibraryToolsService:
|
||||
self.store = modulestore
|
||||
self.user_id = user_id
|
||||
|
||||
def get_latest_library_version(self, lib_key) -> str | None:
|
||||
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;
|
||||
str(<int>) - for V2 library.
|
||||
None - if the library does not exist.
|
||||
"""
|
||||
library = library_api.get_v1_or_v2_library(lib_key, version=None)
|
||||
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
|
||||
elif isinstance(library, LibraryRootV1):
|
||||
# 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)
|
||||
elif isinstance(library, library_api.ContentLibraryMetadata):
|
||||
return str(library.version)
|
||||
# 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 create_block_analytics_summary(self, course_key, block_keys):
|
||||
"""
|
||||
@@ -96,7 +102,7 @@ class LibraryToolsService:
|
||||
"""
|
||||
return self.store.check_supports(block.location.course_key, 'copy_from_template')
|
||||
|
||||
def trigger_library_sync(self, dest_block: LibraryContentBlock, library_version: str | int | None) -> None:
|
||||
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).
|
||||
|
||||
@@ -118,16 +124,20 @@ class LibraryToolsService:
|
||||
`dest_block.children`.
|
||||
"""
|
||||
ensure_cms("library_content block children may only be synced in a CMS context")
|
||||
if not isinstance(dest_block, LibraryContentBlock):
|
||||
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
|
||||
if not library_api.get_v1_or_v2_library(library_key, version=library_version):
|
||||
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.")
|
||||
raise ObjectDoesNotExist(f"Library {library_key} not found.")
|
||||
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.
|
||||
@@ -140,12 +150,14 @@ class LibraryToolsService:
|
||||
),
|
||||
)
|
||||
|
||||
def trigger_duplication(self, source_block: LibraryContentBlock, dest_block: LibraryContentBlock) -> None:
|
||||
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, LibraryContentBlock):
|
||||
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(
|
||||
@@ -163,7 +175,7 @@ class LibraryToolsService:
|
||||
dest_block_id=str(dest_block.scope_ids.usage_id),
|
||||
)
|
||||
|
||||
def are_children_syncing(self, library_content_block: LibraryContentBlock) -> bool:
|
||||
def are_children_syncing(self, library_content_block: LegacyLibraryContentBlock) -> bool:
|
||||
"""
|
||||
Is a task currently running to sync the children of `library_content_block`?
|
||||
|
||||
@@ -179,21 +191,12 @@ class LibraryToolsService:
|
||||
|
||||
def list_available_libraries(self):
|
||||
"""
|
||||
List all known libraries.
|
||||
List all known legacy libraries.
|
||||
|
||||
Collects Only V2 Libaries if the FEATURES[ENABLE_LIBRARY_AUTHORING_MICROFRONTEND] setting is True.
|
||||
Otherwise, return all v1 and v2 libraries.
|
||||
Returns tuples of (library key, display_name).
|
||||
"""
|
||||
user = User.objects.get(id=self.user_id)
|
||||
v1_libs = [
|
||||
return [
|
||||
(lib.location.library_key.replace(version_guid=None, branch=None), lib.display_name)
|
||||
for lib in self.store.get_library_summaries()
|
||||
]
|
||||
v2_query = library_api.get_libraries_for_user(user)
|
||||
v2_libs_with_meta = library_api.get_metadata(v2_query)
|
||||
v2_libs = [(lib.key, lib.title) for lib in v2_libs_with_meta]
|
||||
|
||||
if settings.FEATURES.get('ENABLE_LIBRARY_AUTHORING_MICROFRONTEND'):
|
||||
return v2_libs
|
||||
return v1_libs + v2_libs
|
||||
|
||||
@@ -343,7 +343,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
|
||||
@strip_key
|
||||
def get_library_summaries(self, **kwargs):
|
||||
"""
|
||||
Returns a list of LibrarySummary objects.
|
||||
Returns a list of LegacyLibrarySummary objects.
|
||||
Information contains `location`, `display_name`, `locator` of the libraries in this modulestore.
|
||||
"""
|
||||
library_summaries = {}
|
||||
|
||||
@@ -13,7 +13,7 @@ from xblock.runtime import KeyValueStore, KvsFieldData
|
||||
|
||||
from xmodule.error_block import ErrorBlock
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.library_tools import LibraryToolsService
|
||||
from xmodule.library_tools import LegacyLibraryToolsService
|
||||
from xmodule.mako_block import MakoDescriptorSystem
|
||||
from xmodule.modulestore import BlockData
|
||||
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
|
||||
@@ -78,7 +78,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): # li
|
||||
|
||||
user = get_current_user()
|
||||
user_id = user.id if user else None
|
||||
self._services['library_tools'] = LibraryToolsService(modulestore, user_id=user_id)
|
||||
self._services['library_tools'] = LegacyLibraryToolsService(modulestore, user_id=user_id)
|
||||
|
||||
# Cache of block field datas, keyed by the XBlock instance (since the ScopeId changes!)
|
||||
self.block_field_datas = weakref.WeakKeyDictionary()
|
||||
|
||||
@@ -81,7 +81,7 @@ from xmodule.assetstore import AssetMetadata
|
||||
from xmodule.course_block import CourseSummary
|
||||
from xmodule.error_block import ErrorBlock
|
||||
from xmodule.errortracker import null_error_tracker
|
||||
from xmodule.library_content_block import LibrarySummary
|
||||
from xmodule.library_content_block import LegacyLibrarySummary
|
||||
from xmodule.modulestore import (
|
||||
BlockData,
|
||||
BulkOperationsMixin,
|
||||
@@ -1029,7 +1029,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
|
||||
@autoretry_read()
|
||||
def get_library_summaries(self, **kwargs):
|
||||
"""
|
||||
Returns a list of `LibrarySummary` objects.
|
||||
Returns a list of `LegacyLibrarySummary` objects.
|
||||
kwargs can be valid db fields to match against active_versions
|
||||
collection e.g org='example_org'.
|
||||
"""
|
||||
@@ -1057,7 +1057,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
|
||||
display_name = library_block_fields['display_name']
|
||||
|
||||
libraries_summaries.append(
|
||||
LibrarySummary(library_locator, display_name)
|
||||
LegacyLibrarySummary(library_locator, display_name)
|
||||
)
|
||||
|
||||
return libraries_summaries
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""
|
||||
Basic unit tests for LibraryContentBlock
|
||||
|
||||
Higher-level tests are in `cms/djangoapps/contentstore/tests/test_libraries.py`.
|
||||
Basic unit tests for LegacyLibraryContentBlock
|
||||
"""
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
@@ -9,15 +7,15 @@ import ddt
|
||||
from bson.objectid import ObjectId
|
||||
from fs.memoryfs import MemoryFS
|
||||
from lxml import etree
|
||||
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
from rest_framework import status
|
||||
from search.search_engine_base import SearchEngine
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.runtime import Runtime as VanillaRuntime
|
||||
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_cms
|
||||
from xmodule.library_content_block import ANY_CAPA_TYPE_VALUE, LibraryContentBlock
|
||||
from xmodule.library_tools import LibraryToolsService
|
||||
from xmodule.library_content_block import ANY_CAPA_TYPE_VALUE, LegacyLibraryContentBlock
|
||||
from xmodule.library_tools import LegacyLibraryToolsService
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory
|
||||
from xmodule.modulestore.tests.utils import MixedSplitTestCase
|
||||
@@ -33,15 +31,15 @@ dummy_render = lambda block, _: Fragment(block.data) # pylint: disable=invalid-
|
||||
|
||||
|
||||
@skip_unless_cms
|
||||
class LibraryContentTest(MixedSplitTestCase):
|
||||
class LegacyLibraryContentTest(MixedSplitTestCase):
|
||||
"""
|
||||
Base class for tests of LibraryContentBlock (library_content_block.py)
|
||||
Base class for tests of LegacyLibraryContentBlock (library_content_block.py)
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user_id = UserFactory().id
|
||||
self.tools = LibraryToolsService(self.store, self.user_id)
|
||||
self.tools = LegacyLibraryToolsService(self.store, self.user_id)
|
||||
self.library = LibraryFactory.create(modulestore=self.store)
|
||||
self.lib_blocks = [
|
||||
self.make_block("html", self.library, data=f"Hello world from block {i}")
|
||||
@@ -88,29 +86,22 @@ class LibraryContentTest(MixedSplitTestCase):
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class LibraryContentGeneralTest(LibraryContentTest):
|
||||
class LegacyLibraryContentGeneralTest(LegacyLibraryContentTest):
|
||||
"""
|
||||
Test the base functionality of the LibraryContentBlock.
|
||||
Test the base functionality of the LegacyLibraryContentBlock.
|
||||
"""
|
||||
|
||||
@ddt.data(
|
||||
('library-v1:ProblemX+PR0B', LibraryLocator),
|
||||
('lib:ORG:test-1', LibraryLocatorV2)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_source_library_key(self, library_key, expected_locator_type):
|
||||
def test_source_library_key(self):
|
||||
"""
|
||||
Test the source_library_key property of the xblock.
|
||||
|
||||
The method should correctly work either with V1 or V2 libraries.
|
||||
"""
|
||||
library = self.make_block(
|
||||
"library_content",
|
||||
self.vertical,
|
||||
max_count=1,
|
||||
source_library_id=library_key
|
||||
source_library_id='library-v1:ProblemX+PR0B',
|
||||
)
|
||||
assert isinstance(library.source_library_key, expected_locator_type)
|
||||
assert isinstance(library.source_library_key, LibraryLocator)
|
||||
|
||||
def test_initial_sync_from_library(self):
|
||||
"""
|
||||
@@ -133,9 +124,9 @@ class LibraryContentGeneralTest(LibraryContentTest):
|
||||
assert len(self.lc_block.children) == len(self.lib_blocks)
|
||||
|
||||
|
||||
class TestLibraryContentExportImport(LibraryContentTest):
|
||||
class TestLibraryContentExportImport(LegacyLibraryContentTest):
|
||||
"""
|
||||
Export and import tests for LibraryContentBlock
|
||||
Export and import tests for LegacyLibraryContentBlock
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -173,7 +164,6 @@ class TestLibraryContentExportImport(LibraryContentTest):
|
||||
assert imported_lc_block.display_name == self.lc_block.display_name
|
||||
assert imported_lc_block.source_library_id == self.lc_block.source_library_id
|
||||
assert imported_lc_block.source_library_version == self.lc_block.source_library_version
|
||||
assert imported_lc_block.mode == self.lc_block.mode
|
||||
assert imported_lc_block.max_count == self.lc_block.max_count
|
||||
assert imported_lc_block.capa_type == self.lc_block.capa_type
|
||||
assert len(imported_lc_block.children) == len(self.lc_block.children)
|
||||
@@ -195,13 +185,13 @@ class TestLibraryContentExportImport(LibraryContentTest):
|
||||
|
||||
# Now import it.
|
||||
olx_element = etree.fromstring(exported_olx)
|
||||
imported_lc_block = LibraryContentBlock.parse_xml(olx_element, self.runtime, None)
|
||||
imported_lc_block = LegacyLibraryContentBlock.parse_xml(olx_element, self.runtime, None)
|
||||
|
||||
self._verify_xblock_properties(imported_lc_block)
|
||||
|
||||
def test_xml_import_with_comments(self):
|
||||
"""
|
||||
Test that XML comments within LibraryContentBlock are ignored during the import.
|
||||
Test that XML comments within LegacyLibraryContentBlock are ignored during the import.
|
||||
"""
|
||||
olx_with_comments = (
|
||||
'<!-- Comment -->\n'
|
||||
@@ -219,15 +209,15 @@ class TestLibraryContentExportImport(LibraryContentTest):
|
||||
|
||||
# Import the olx.
|
||||
olx_element = etree.fromstring(olx_with_comments)
|
||||
imported_lc_block = LibraryContentBlock.parse_xml(olx_element, self.runtime, None)
|
||||
imported_lc_block = LegacyLibraryContentBlock.parse_xml(olx_element, self.runtime, None)
|
||||
|
||||
self._verify_xblock_properties(imported_lc_block)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class LibraryContentBlockTestMixin:
|
||||
class LegacyLibraryContentBlockTestMixin:
|
||||
"""
|
||||
Basic unit tests for LibraryContentBlock
|
||||
Basic unit tests for LegacyLibraryContentBlock
|
||||
"""
|
||||
problem_types = [
|
||||
["multiplechoiceresponse"], ["optionresponse"], ["optionresponse", "coderesponse"],
|
||||
@@ -424,8 +414,7 @@ class LibraryContentBlockTestMixin:
|
||||
Test the settings that are marked as "non-editable".
|
||||
"""
|
||||
non_editable_metadata_fields = self.lc_block.non_editable_metadata_fields
|
||||
assert LibraryContentBlock.mode in non_editable_metadata_fields
|
||||
assert LibraryContentBlock.display_name not in non_editable_metadata_fields
|
||||
assert LegacyLibraryContentBlock.display_name not in non_editable_metadata_fields
|
||||
|
||||
def test_overlimit_blocks_chosen_randomly(self):
|
||||
"""
|
||||
@@ -503,7 +492,7 @@ search_index_mock = Mock(spec=SearchEngine) # pylint: disable=invalid-name
|
||||
|
||||
|
||||
@patch.object(SearchEngine, 'get_search_engine', Mock(return_value=None, autospec=True))
|
||||
class TestLibraryContentBlockWithSearchIndex(LibraryContentBlockTestMixin, LibraryContentTest):
|
||||
class TestLegacyLibraryContentBlockWithSearchIndex(LegacyLibraryContentBlockTestMixin, LegacyLibraryContentTest):
|
||||
"""
|
||||
Tests for library container with mocked search engine response.
|
||||
"""
|
||||
@@ -532,9 +521,9 @@ class TestLibraryContentBlockWithSearchIndex(LibraryContentBlockTestMixin, Libra
|
||||
)
|
||||
@patch('xmodule.html_block.HtmlBlock.author_view', dummy_render, create=True)
|
||||
@patch('xmodule.x_module.DescriptorSystem.applicable_aside_types', lambda self, block: [])
|
||||
class TestLibraryContentRender(LibraryContentTest):
|
||||
class TestLibraryContentRender(LegacyLibraryContentTest):
|
||||
"""
|
||||
Rendering unit tests for LibraryContentBlock
|
||||
Rendering unit tests for LegacyLibraryContentBlock
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
@@ -559,9 +548,9 @@ class TestLibraryContentRender(LibraryContentTest):
|
||||
# but some js initialization should happen
|
||||
|
||||
|
||||
class TestLibraryContentAnalytics(LibraryContentTest):
|
||||
class TestLibraryContentAnalytics(LegacyLibraryContentTest):
|
||||
"""
|
||||
Test analytics features of LibraryContentBlock
|
||||
Test analytics features of LegacyLibraryContentBlock
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
@@ -573,7 +562,7 @@ class TestLibraryContentAnalytics(LibraryContentTest):
|
||||
|
||||
def _assert_event_was_published(self, event_type):
|
||||
"""
|
||||
Check that a LibraryContentBlock analytics event was published by self.lc_block.
|
||||
Check that a LegacyLibraryContentBlock analytics event was published by self.lc_block.
|
||||
"""
|
||||
assert self.publisher.called
|
||||
assert len(self.publisher.call_args[0]) == 3 # pylint:disable=unsubscriptable-object
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
"""
|
||||
Tests for library tools service (only used by CMS)
|
||||
Tests for legacy library tools service (only used by CMS)
|
||||
|
||||
Currently, the only known user of the LibraryToolsService is the
|
||||
LibraryContentBlock, so these tests are all written with only that
|
||||
The only known user of the LegacyLibraryToolsService is the
|
||||
LegacyLibraryContentBlock, so these tests are all written with only that
|
||||
block type in mind.
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.test import override_settings
|
||||
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
|
||||
from opaque_keys.edx.locator import LibraryLocator
|
||||
|
||||
from common.djangoapps.student.roles import CourseInstructorRole
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_cms
|
||||
from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest
|
||||
from xmodule.library_tools import LibraryToolsService
|
||||
from xmodule.library_tools import LegacyLibraryToolsService
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, LibraryFactory
|
||||
from xmodule.modulestore.tests.utils import MixedSplitTestCase
|
||||
|
||||
@@ -26,34 +23,23 @@ from xmodule.modulestore.tests.utils import MixedSplitTestCase
|
||||
@ddt.ddt
|
||||
class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest):
|
||||
"""
|
||||
Tests for LibraryToolsService.
|
||||
|
||||
Tests interaction with learning-core-based (V2) and mongo-based (V1) content libraries.
|
||||
Tests for LegacyLibraryToolsService.
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
UserFactory(is_staff=True, id=self.user_id)
|
||||
self.tools = LibraryToolsService(self.store, self.user_id)
|
||||
self.tools = LegacyLibraryToolsService(self.store, self.user_id)
|
||||
|
||||
def test_list_available_libraries(self):
|
||||
"""
|
||||
Test listing of libraries.
|
||||
|
||||
Collects Only V2 Libaries if the FEATURES[ENABLE_LIBRARY_AUTHORING_MICROFRONTEND] setting is True.
|
||||
Otherwise, return all v1 and v2 libraries.
|
||||
Test listing of v1 libraries.
|
||||
"""
|
||||
# create V1 library
|
||||
_ = LibraryFactory.create(modulestore=self.store)
|
||||
# create V2 library
|
||||
# create V2 library (should not be included in this list)
|
||||
self._create_library(slug="testlib1_preview", title="Test Library 1", description="Testing XBlocks")
|
||||
all_libraries = self.tools.list_available_libraries()
|
||||
assert all_libraries
|
||||
assert len(all_libraries) == 2
|
||||
|
||||
with override_settings(FEATURES={**settings.FEATURES, "ENABLE_LIBRARY_AUTHORING_MICROFRONTEND": True}):
|
||||
all_libraries = self.tools.list_available_libraries()
|
||||
assert all_libraries
|
||||
assert len(all_libraries) == 1
|
||||
assert len(all_libraries) == 1
|
||||
|
||||
@mock.patch('xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.get_library_summaries')
|
||||
def test_list_available_libraries_fetch(self, mock_get_library_summaries):
|
||||
@@ -63,7 +49,7 @@ class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest):
|
||||
_ = self.tools.list_available_libraries()
|
||||
assert mock_get_library_summaries.called
|
||||
|
||||
def test_get_latest_v1_library_version(self):
|
||||
def test_get_latest_library_version(self):
|
||||
"""
|
||||
Test get_v1_library_version for V1 libraries.
|
||||
|
||||
@@ -84,49 +70,16 @@ class ContentLibraryToolsTest(MixedSplitTestCase, ContentLibrariesRestApiTest):
|
||||
assert result == str(lib.location.library_key.version_guid)
|
||||
|
||||
@ddt.data(
|
||||
'library-v1:Fake+Key', # V1 library key
|
||||
'lib:Fake:V-2', # V2 library key
|
||||
'library-v1:Fake+Key',
|
||||
LibraryLocator.from_string('library-v1:Fake+Key'),
|
||||
LibraryLocatorV2.from_string('lib:Fake:V-2'),
|
||||
)
|
||||
def test_get_latest_library_version_no_library(self, lib_key):
|
||||
"""
|
||||
Test get_latest_library_version result when the library does not exist.
|
||||
|
||||
Provided lib_key's are valid V1 or V2 keys.
|
||||
"""
|
||||
assert self.tools.get_latest_library_version(lib_key) is None
|
||||
|
||||
def test_update_children_for_v2_lib(self):
|
||||
"""
|
||||
Test update_children with V2 library as a source.
|
||||
"""
|
||||
library = self._create_library(
|
||||
slug="cool-v2-lib", title="The best Library", description="Spectacular description"
|
||||
)
|
||||
self._add_block_to_library(library["id"], "unit", "unit1_id")
|
||||
|
||||
course = CourseFactory.create(modulestore=self.store, user_id=self.user.id)
|
||||
CourseInstructorRole(course.id).add_users(self.user)
|
||||
|
||||
content_block = self.make_block(
|
||||
"library_content",
|
||||
course,
|
||||
max_count=1,
|
||||
source_library_id=library['id']
|
||||
)
|
||||
assert len(content_block.children) == 0
|
||||
|
||||
# Populate children from library
|
||||
self.tools.trigger_library_sync(content_block, library_version=None)
|
||||
|
||||
# The updates happen in a Celery task, so this particular content_block instance is no updated.
|
||||
# We must re-instantiate it from modulstore in order to see the updated children list.
|
||||
content_block = self.store.get_item(content_block.location)
|
||||
|
||||
assert len(content_block.children) == 1
|
||||
|
||||
def test_update_children_for_v1_lib(self):
|
||||
def test_update_children(self):
|
||||
"""
|
||||
Test update_children with V1 library as a source.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from .test_course_block import DummySystem as TestImportSystem
|
||||
|
||||
class RandomizeBlockTest(MixedSplitTestCase):
|
||||
"""
|
||||
Base class for tests of LibraryContentBlock (library_content_block.py)
|
||||
Base class for tests of RandomizeBlock (randomize_block.py)
|
||||
"""
|
||||
maxDiff = None
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ class VerticalBlock(
|
||||
if has_access_error:
|
||||
return True
|
||||
|
||||
# Check child nodes if they exist (e.g. randomized library question aka LibraryContentBlock)
|
||||
# Check child nodes if they exist (e.g. randomized library question aka LegacyLibraryContentBlock)
|
||||
for child in block.get_children():
|
||||
has_access_error = getattr(child, 'has_access_error', False)
|
||||
if has_access_error:
|
||||
|
||||
Reference in New Issue
Block a user