Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ed395e3
feat: show item bank ui for migrated legacy library content
navinkarkera Sep 24, 2025
ddeca53
feat: migrate legacy content block to item bank block on view in studio
navinkarkera Sep 29, 2025
a0a0644
fix: duplicate and copy issues
navinkarkera Sep 30, 2025
1d7d208
refactor: migration location and add tests
navinkarkera Oct 1, 2025
6436099
fix: lint issues
navinkarkera Oct 1, 2025
19ada39
fix: item bank and library content children view add button functiona…
navinkarkera Oct 2, 2025
1ba790d
fix: lint issues
navinkarkera Oct 2, 2025
8e2856d
fix: lint issues
navinkarkera Oct 2, 2025
3a3485e
feat: only migrate if same version of library is migrated
navinkarkera Oct 2, 2025
01a5135
refactor: migrate block on request
navinkarkera Oct 2, 2025
747cb11
fix: component reload on migration
navinkarkera Oct 3, 2025
b0257a4
fix: tests
navinkarkera Oct 3, 2025
5ebca79
refactor: comments and message wordings
navinkarkera Oct 7, 2025
8d82e8a
refactor: update alert text
navinkarkera Oct 8, 2025
ea83ae6
docs: add context
navinkarkera Oct 9, 2025
7964d6b
fix: component links not being created on migrating legacy blocks
navinkarkera Oct 13, 2025
7210985
fix: api docs and types
navinkarkera Oct 13, 2025
29f1b30
refactor: use inheritance and specific parent method call
navinkarkera Oct 13, 2025
6537e59
fix: imports
navinkarkera Oct 13, 2025
6cdb1f0
fix: api typing
navinkarkera Oct 13, 2025
62ab6c9
fix: upstream_version check
navinkarkera Oct 13, 2025
6353ffd
refactor: rename variables
navinkarkera Oct 16, 2025
9047bf7
refactor: parsing entity keys to usage_keys
navinkarkera Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1677,7 +1677,7 @@ def handle_update_xblock_upstream_link(usage_key):
except (ItemNotFoundError, InvalidKeyError):
LOGGER.exception(f'Could not find item for given usage_key: {usage_key}')
return
if not xblock.upstream or not xblock.upstream_version:
if not xblock.upstream or xblock.upstream_version is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unnecessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, since we set upstream_version = 0 while migrating legacy blocks under LegacyLibraryContent, not xblock.upstream_version evaluates to True and the Entity links are not created.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha

return
create_or_update_xblock_upstream_link(xblock)

Expand Down
46 changes: 38 additions & 8 deletions cms/djangoapps/modulestore_migrator/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@
API for migration from modulestore to learning core
"""
from celery.result import AsyncResult
from opaque_keys.edx.keys import CourseKey, LearningContextKey
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, LearningContextKey, UsageKey
from opaque_keys.edx.locator import LibraryLocator, LibraryLocatorV2, LibraryUsageLocatorV2
from openedx_learning.api.authoring import get_collection
from openedx_learning.api.authoring_models import Component
from user_tasks.models import UserTaskStatus

from openedx.core.djangoapps.content_libraries.api import get_library
from openedx.core.djangoapps.content_libraries.api import get_library, library_component_usage_key
from openedx.core.types.user import AuthUser

from . import tasks
from .models import ModulestoreSource
from .models import ModulestoreBlockMigration, ModulestoreSource

__all__ = (
"start_migration_to_library",
"start_bulk_migration_to_library",
"is_successfully_migrated",
"get_migration_info",
"get_target_block_usage_keys",
)


Expand Down Expand Up @@ -102,13 +105,17 @@ def start_bulk_migration_to_library(
)


def is_successfully_migrated(source_key: CourseKey | LibraryLocator) -> bool:
def is_successfully_migrated(
source_key: CourseKey | LibraryLocator,
source_version: str | None = None,
) -> bool:
"""
Check if the source course/library has been migrated successfully.
"""
return ModulestoreSource.objects.get_or_create(key=str(source_key))[0].migrations.filter(
task_status__state=UserTaskStatus.SUCCEEDED
).exists()
filters = {"task_status__state": UserTaskStatus.SUCCEEDED}
if source_version is not None:
filters["source_version"] = source_version
return ModulestoreSource.objects.get_or_create(key=str(source_key))[0].migrations.filter(**filters).exists()


def get_migration_info(source_keys: list[CourseKey | LibraryLocator]) -> dict:
Expand All @@ -129,3 +136,26 @@ def get_migration_info(source_keys: list[CourseKey | LibraryLocator]) -> dict:
named=True,
)
}


def get_target_block_usage_keys(source_key: CourseKey | LibraryLocator) -> dict[UsageKey, LibraryUsageLocatorV2 | None]:
"""
For given source_key, get a map of legacy block key and its new location in migrated v2 library.
"""
query_set = ModulestoreBlockMigration.objects.filter(overall_migration__source__key=source_key).select_related(
'source', 'target__component__component_type', 'target__learning_package'
)

def construct_usage_key(lib_key_str: str, component: Component) -> LibraryUsageLocatorV2 | None:
try:
lib_key = LibraryLocatorV2.from_string(lib_key_str)
except InvalidKeyError:
return None
return library_component_usage_key(lib_key, component)

# Use LibraryUsageLocatorV2 and construct usage key
return {
obj.source.key: construct_usage_key(obj.target.learning_package.key, obj.target.component)
for obj in query_set
if obj.source.key is not None
}
24 changes: 24 additions & 0 deletions cms/djangoapps/modulestore_migrator/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def setUp(self):
)
self.library_v2 = lib_api.ContentLibrary.objects.get(slug=self.lib_key_v2.slug)
self.learning_package = self.library_v2.learning_package
self.blocks = []
for _ in range(3):
self.blocks.append(self._add_simple_content_block().usage_key)

def test_start_migration_to_library(self):
"""
Expand Down Expand Up @@ -384,3 +387,24 @@ def test_get_migration_info(self):
assert row.migrations__target__title == "Test Library"
assert row.migrations__target_collection__key == collection_key
assert row.migrations__target_collection__title == "Test Collection"

def test_get_target_block_usage_keys(self):
"""
Test that the API can get the list of target block usage keys for a given library.
"""
user = UserFactory()

api.start_migration_to_library(
user=user,
source_key=self.lib_key,
target_library_key=self.library_v2.library_key,
target_collection_slug=None,
composition_level=CompositionLevel.Component.value,
repeat_handling_strategy=RepeatHandlingStrategy.Skip.value,
preserve_url_slugs=True,
forward_source_to_target=True,
)
with self.assertNumQueries(1):
result = api.get_target_block_usage_keys(self.lib_key)
for key in self.blocks:
assert result.get(key) is not None
10 changes: 5 additions & 5 deletions cms/lib/xblock/upstream_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@

import logging
import typing as t
from dataclasses import dataclass, asdict
from dataclasses import asdict, dataclass

from django.conf import settings
from django.utils.translation import gettext_lazy as _
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryUsageLocatorV2
from opaque_keys.edx.keys import UsageKey
from xblock.core import XBlock, XBlockMixin
from xblock.exceptions import XBlockNotFoundError
from xblock.fields import Scope, String, Integer, List
from xblock.core import XBlockMixin, XBlock
from xblock.fields import Integer, List, Scope, String

from xmodule.util.keys import BlockKey

if t.TYPE_CHECKING:
Expand Down
9 changes: 5 additions & 4 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,13 +632,13 @@ function($, _, Backbone, gettext, BasePage,
doneAddingBlock = (addResult) => {
const $placeholderEl = $(this.createPlaceholderElement());
const placeholderElement = $placeholderEl.insertBefore($insertSpot);
placeholderElement.data('locator', addResult.locator);
return this.refreshXBlock(placeholderElement, true);
return this.onNewXBlock(placeholderElement, 0, false, addResult);
};
doneAddingAllBlocks = () => {};
}
// Note: adding all the XBlocks in parallel will cause a race condition 😢 so we have to add
// them one at a time:

let lastAdded = $.when();
for (const { usageKey, blockType } of selectedBlocks) {
const addData = {
Expand Down Expand Up @@ -1220,12 +1220,13 @@ function($, _, Backbone, gettext, BasePage,
refreshXBlock: function(element, block_added, is_duplicate) {
var xblockElement = this.findXBlockElement(element),
parentElement = xblockElement.parent(),
rootLocator = this.xblockView.model.id;
rootLocator = this.xblockView.model.id,
parentBlockType = parentElement.data('block-type');
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
if (block_added) {
this.render({refresh: true, block_added: block_added});
}
} else if (parentElement.hasClass('reorderable-container')) {
} else if (parentElement.hasClass('reorderable-container') || ["itembank", "library_content"].includes(parentBlockType) ) {
this.refreshChildXBlock(xblockElement, block_added, is_duplicate);
} else {
this.refreshXBlock(this.findXBlockElement(parentElement));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
/* JavaScript for special editing operations that can be done on LibraryContentXBlock */
window.LibraryContentAuthorView = function(runtime, element) {
window.LibraryContentAuthorView = function(runtime, element, initArgs) {
'use strict';
var $element = $(element);
var usage_id = $element.data('usage-id');
// The "Update Now" button is not a child of 'element', as it is in the validation message area
// But it is still inside this xblock's wrapper element, which we can easily find:
var $wrapper = $element.parents('*[data-locator="' + usage_id + '"]');
var { is_root: isRoot = false } = initArgs;

function postMessageToParent(body, callbackFn = null) {
try {
window.parent.postMessage(body, document.referrer);
if (callbackFn) {
callbackFn();
}
} catch (e) {
console.error('Failed to post message:', e);
}
};

function reloadPreviewPage() {
if (window.self !== window.top) {
// We are inside iframe
// Normal location.reload() reloads the iframe but subsequent calls to
// postMessage fails. So we are using postMessage to tell the parent page
// to reload the iframe.
postMessageToParent({
type: 'refreshIframe',
message: 'Refresh Iframe',
payload: {},
})
} else {
location.reload();
}
}

$wrapper.on('click', '.library-update-btn', function(e) {
e.preventDefault();
Expand All @@ -20,17 +48,33 @@ window.LibraryContentAuthorView = function(runtime, element) {
state: 'end',
element: element
});
if ($element.closest('.wrapper-xblock').is(':not(.level-page)')) {
// We are on a course unit page. The notify('save') should refresh this block,
// but that is only working on the container page view of this block.
// Why? On the unit page, this XBlock's runtime has no reference to the
// XBlockContainerPage - only the top-level XBlock (a vertical) runtime does.
// But unfortunately there is no way to get a reference to our parent block's
// JS 'runtime' object. So instead we must refresh the whole page:
location.reload();
if (isRoot) {
// We are inside preview page where all children blocks are listed.
reloadPreviewPage();
}
});
});

$wrapper.on('click', '.library-block-migrate-btn', function(e) {
e.preventDefault();
// migrate library content block to item bank block
runtime.notify('save', {
state: 'start',
element: element,
message: gettext('Migrating to Problem Bank')
});
$.post(runtime.handlerUrl(element, 'upgrade_to_v2_library')).done(function() {
runtime.notify('save', {
state: 'end',
element: element
});
if (isRoot) {
// We are inside preview page where all children blocks are listed.
reloadPreviewPage();
}
});
});

// Hide loader and show element when update task finished.
var $loader = $wrapper.find('.ui-loading');
var $xblockHeader = $wrapper.find('.xblock-header');
Expand Down
Loading
Loading