Skip to content

Commit 6b8158e

Browse files
committed
fix: fix fork on multiple migrations
1 parent 245b861 commit 6b8158e

File tree

3 files changed

+80
-23
lines changed

3 files changed

+80
-23
lines changed

cms/djangoapps/modulestore_migrator/tasks.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from dataclasses import dataclass
1010
from datetime import datetime, timezone
1111
from enum import Enum
12+
from itertools import groupby
1213

1314
from celery import shared_task
1415
from celery.utils.log import get_task_logger
@@ -20,8 +21,11 @@
2021
from opaque_keys import InvalidKeyError
2122
from opaque_keys.edx.keys import CourseKey, UsageKey
2223
from opaque_keys.edx.locator import (
23-
CourseLocator, LibraryLocator,
24-
LibraryLocatorV2, LibraryUsageLocatorV2, LibraryContainerLocator
24+
CourseLocator,
25+
LibraryContainerLocator,
26+
LibraryLocator,
27+
LibraryLocatorV2,
28+
LibraryUsageLocatorV2
2529
)
2630
from openedx_learning.api import authoring as authoring_api
2731
from openedx_learning.api.authoring_models import (
@@ -30,21 +34,20 @@
3034
ComponentType,
3135
LearningPackage,
3236
PublishableEntity,
33-
PublishableEntityVersion,
37+
PublishableEntityVersion
3438
)
3539
from user_tasks.tasks import UserTask, UserTaskStatus
3640

37-
from openedx.core.djangoapps.content_libraries.api import ContainerType, get_library
41+
from common.djangoapps.split_modulestore_django.models import SplitModulestoreCourseIndex
3842
from openedx.core.djangoapps.content_libraries import api as libraries_api
43+
from openedx.core.djangoapps.content_libraries.api import ContainerType, get_library
3944
from openedx.core.djangoapps.content_staging import api as staging_api
4045
from xmodule.modulestore import exceptions as modulestore_exceptions
4146
from xmodule.modulestore.django import modulestore
42-
from common.djangoapps.split_modulestore_django.models import SplitModulestoreCourseIndex
4347

4448
from .constants import CONTENT_STAGING_PURPOSE_TEMPLATE
4549
from .data import CompositionLevel, RepeatHandlingStrategy
46-
from .models import ModulestoreSource, ModulestoreMigration, ModulestoreBlockSource, ModulestoreBlockMigration
47-
50+
from .models import ModulestoreBlockMigration, ModulestoreBlockSource, ModulestoreMigration, ModulestoreSource
4851

4952
log = get_task_logger(__name__)
5053

@@ -89,7 +92,7 @@ class _MigrationContext:
8992
Context for the migration process.
9093
"""
9194
existing_source_to_target_keys: dict[ # Note: It's intended to be mutable to reflect changes during migration.
92-
UsageKey, PublishableEntity
95+
UsageKey, list[PublishableEntity]
9396
]
9497
target_package_id: int
9598
target_library_key: LibraryLocatorV2
@@ -105,16 +108,30 @@ def is_already_migrated(self, source_key: UsageKey) -> bool:
105108
return source_key in self.existing_source_to_target_keys
106109

107110
def get_existing_target(self, source_key: UsageKey) -> PublishableEntity:
108-
return self.existing_source_to_target_keys[source_key]
111+
"""
112+
Get the target entity for a given source key.
113+
114+
If the source key is already migrated, return the FIRST target entity.
115+
If the source key is not found, raise a KeyError.
116+
"""
117+
if source_key not in self.existing_source_to_target_keys:
118+
raise KeyError(f"Source key {source_key} not found in existing source to target keys")
119+
120+
# NOTE: This is a list of PublishableEntities, but we always return the first one.
121+
return self.existing_source_to_target_keys[source_key][0]
109122

110123
def add_migration(self, source_key: UsageKey, target: PublishableEntity) -> None:
111124
"""Update the context with a new migration (keeps it current)"""
112-
self.existing_source_to_target_keys[source_key] = target
125+
if source_key not in self.existing_source_to_target_keys:
126+
self.existing_source_to_target_keys[source_key] = [target]
127+
else:
128+
self.existing_source_to_target_keys[source_key].append(target)
113129

114130
def get_existing_target_entity_keys(self, base_key: str) -> set[str]:
115131
return set(
116-
publishable_entity.key for _, publishable_entity in
117-
self.existing_source_to_target_keys.items()
132+
publishable_entity.key
133+
for publishable_entity_list in self.existing_source_to_target_keys.values()
134+
for publishable_entity in publishable_entity_list
118135
if publishable_entity.key.startswith(base_key)
119136
)
120137

@@ -285,10 +302,13 @@ def migrate_from_modulestore(
285302
# a given LearningPackage.
286303
# We use this mapping to ensure that we don't create duplicate
287304
# PublishableEntities during the migration process for a given LearningPackage.
305+
existing_source_to_target_keys: dict[UsageKey, list[PublishableEntity]] = {}
306+
modulestore_blocks = (
307+
ModulestoreBlockMigration.objects.filter(overall_migration__target=migration.target.id).order_by("source__key")
308+
)
288309
existing_source_to_target_keys = {
289-
block.source.key: block.target for block in ModulestoreBlockMigration.objects.filter(
290-
overall_migration__target=migration.target.id
291-
)
310+
source_key: list(block.target for block in group) for source_key, group in groupby(
311+
modulestore_blocks, key=lambda x: x.source.key)
292312
}
293313

294314
migration_context = _MigrationContext(
@@ -657,7 +677,7 @@ def _get_distinct_target_usage_key(
657677
# Check if we already processed this block and we are not forking. If we are forking, we will
658678
# want a new target key.
659679
if context.is_already_migrated(source_key) and not context.should_fork_strategy:
660-
log.debug(f"Block {source_key} already exists, reusing existing target")
680+
log.debug(f"Block {source_key} already exists, reusing first existing target")
661681
existing_target = context.get_existing_target(source_key)
662682
block_id = existing_target.component.local_key
663683

cms/djangoapps/modulestore_migrator/tests/test_api.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,43 @@ def test_start_migration_to_library_with_strategy_forking(self):
276276
)
277277
assert second_component.display_name == "Updated Block"
278278

279+
# Update the block again, changing its name
280+
library_block.display_name = "Updated Block Again"
281+
self.store.update_item(library_block, user.id)
282+
283+
# Migrate again using the Fork strategy
284+
api.start_migration_to_library(
285+
user=user,
286+
source_key=source.key,
287+
target_library_key=self.library_v2.library_key,
288+
composition_level=CompositionLevel.Component.value,
289+
repeat_handling_strategy=RepeatHandlingStrategy.Fork.value,
290+
preserve_url_slugs=True,
291+
forward_source_to_target=False,
292+
)
293+
294+
modulestoremigration = ModulestoreMigration.objects.last()
295+
assert modulestoremigration is not None
296+
assert modulestoremigration.repeat_handling_strategy == RepeatHandlingStrategy.Fork.value
297+
298+
migrated_components_fork = lib_api.get_library_components(self.library_v2.library_key)
299+
assert len(migrated_components_fork) == 3
300+
301+
first_component = lib_api.LibraryXBlockMetadata.from_component(
302+
self.library_v2.library_key, migrated_components_fork[0]
303+
)
304+
assert first_component.display_name == "Original Block"
305+
306+
second_component = lib_api.LibraryXBlockMetadata.from_component(
307+
self.library_v2.library_key, migrated_components_fork[1]
308+
)
309+
assert second_component.display_name == "Updated Block"
310+
311+
third_component = lib_api.LibraryXBlockMetadata.from_component(
312+
self.library_v2.library_key, migrated_components_fork[2]
313+
)
314+
assert third_component.display_name == "Updated Block Again"
315+
279316
def test_get_migration_info(self):
280317
"""
281318
Test that the API can retrieve migration info.

cms/djangoapps/modulestore_migrator/tests/test_tasks.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ def test_migrate_component_replace_existing_false(self):
447447
title="test_problem"
448448
)
449449

450-
context.existing_source_to_target_keys[source_key] = first_result.entity
450+
context.existing_source_to_target_keys[source_key] = [first_result.entity]
451451

452452
second_result = _migrate_component(
453453
context=context,
@@ -489,7 +489,7 @@ def test_migrate_component_same_title(self):
489489
title="test_problem"
490490
)
491491

492-
context.existing_source_to_target_keys[source_key_1] = first_result.entity
492+
context.existing_source_to_target_keys[source_key_1] = [first_result.entity]
493493

494494
second_result = _migrate_component(
495495
context=context,
@@ -527,7 +527,7 @@ def test_migrate_component_replace_existing_true(self):
527527
title="original"
528528
)
529529

530-
context.existing_source_to_target_keys[source_key] = first_result.entity
530+
context.existing_source_to_target_keys[source_key] = [first_result.entity]
531531

532532
updated_olx = '<problem display_name="Updated"><multiplechoiceresponse></multiplechoiceresponse></problem>'
533533
second_result = _migrate_component(
@@ -708,7 +708,7 @@ def test_migrate_component_duplicate_content_integrity_error(self):
708708
title="test_problem"
709709
)
710710

711-
context.existing_source_to_target_keys[source_key] = first_result.entity
711+
context.existing_source_to_target_keys[source_key] = [first_result.entity]
712712

713713
second_result = _migrate_component(
714714
context=context,
@@ -863,7 +863,7 @@ def test_migrate_container_replace_existing_false(self):
863863
children=[],
864864
)
865865

866-
context.existing_source_to_target_keys[source_key] = first_result.entity
866+
context.existing_source_to_target_keys[source_key] = [first_result.entity]
867867

868868
second_result = _migrate_container(
869869
context=context,
@@ -909,7 +909,7 @@ def test_migrate_container_same_title(self):
909909
children=[],
910910
)
911911

912-
context.existing_source_to_target_keys[source_key_1] = first_result.entity
912+
context.existing_source_to_target_keys[source_key_1] = [first_result.entity]
913913

914914
second_result = _migrate_container(
915915
context=context,
@@ -969,7 +969,7 @@ def test_migrate_container_replace_existing_true(self):
969969
children=[],
970970
)
971971

972-
context.existing_source_to_target_keys[source_key] = first_result.entity
972+
context.existing_source_to_target_keys[source_key] = [first_result.entity]
973973

974974
second_result = _migrate_container(
975975
context=context,

0 commit comments

Comments
 (0)