Skip to content

Commit 0a675b0

Browse files
committed
feat: store failed migration blocks in modulestore
1 parent b1f01ed commit 0a675b0

File tree

4 files changed

+44
-13
lines changed

4 files changed

+44
-13
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.2.7 on 2025-11-19 06:23
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
('modulestore_migrator', '0003_modulestoremigration_is_failed'),
10+
('oel_publishing', '0008_alter_draftchangelogrecord_options_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='modulestoreblockmigration',
16+
name='target',
17+
field=models.ForeignKey(
18+
blank=True,
19+
null=True,
20+
on_delete=django.db.models.deletion.CASCADE,
21+
to='oel_publishing.publishableentity',
22+
),
23+
),
24+
]

cms/djangoapps/modulestore_migrator/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ class ModulestoreBlockMigration(TimeStampedModel):
210210
target = models.ForeignKey(
211211
PublishableEntity,
212212
on_delete=models.CASCADE,
213+
null=True,
214+
blank=True,
213215
)
214216
change_log_record = models.OneToOneField(
215217
DraftChangeLogRecord,
@@ -222,6 +224,7 @@ class ModulestoreBlockMigration(TimeStampedModel):
222224
class Meta:
223225
unique_together = [
224226
('overall_migration', 'source'),
227+
# By default defining a unique index on a nullable column will only enforce unicity of non-null values.
225228
('overall_migration', 'target'),
226229
]
227230

cms/djangoapps/modulestore_migrator/tasks.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def get_existing_target(self, source_key: UsageKey) -> PublishableEntity:
154154
# NOTE: This is a list of PublishableEntities, but we always return the first one.
155155
return self.existing_source_to_target_keys[source_key][0]
156156

157-
def add_migration(self, source_key: UsageKey, target: PublishableEntity) -> None:
157+
def add_migration(self, source_key: UsageKey, target: PublishableEntity | None) -> None:
158158
"""Update the context with a new migration (keeps it current)"""
159159
if source_key not in self.existing_source_to_target_keys:
160160
self.existing_source_to_target_keys[source_key] = [target]
@@ -166,7 +166,7 @@ def get_existing_target_entity_keys(self, base_key: str) -> set[str]:
166166
publishable_entity.key
167167
for publishable_entity_list in self.existing_source_to_target_keys.values()
168168
for publishable_entity in publishable_entity_list
169-
if publishable_entity.key.startswith(base_key)
169+
if publishable_entity and publishable_entity.key.startswith(base_key)
170170
)
171171

172172
@property
@@ -934,10 +934,10 @@ class _MigratedNode:
934934
This happens, particularly, if the node is above the requested composition level
935935
but has descendents which are at or below that level.
936936
"""
937-
source_to_target: tuple[UsageKey, PublishableEntityVersion] | None
937+
source_to_target: tuple[UsageKey, PublishableEntityVersion | None] | None
938938
children: list[_MigratedNode]
939939

940-
def all_source_to_target_pairs(self) -> t.Iterable[tuple[UsageKey, PublishableEntityVersion]]:
940+
def all_source_to_target_pairs(self) -> t.Iterable[tuple[UsageKey, PublishableEntityVersion | None]]:
941941
"""
942942
Get all source_key->target_ver pairs via a pre-order traversal.
943943
"""
@@ -1010,7 +1010,7 @@ def _migrate_node(
10101010
children=[
10111011
migrated_child.source_to_target[1]
10121012
for migrated_child in migrated_children if
1013-
migrated_child.source_to_target
1013+
migrated_child.source_to_target and migrated_child.source_to_target[1]
10141014
],
10151015
)
10161016
if container_type else
@@ -1021,9 +1021,8 @@ def _migrate_node(
10211021
title=title,
10221022
)
10231023
)
1024-
if target_entity_version:
1025-
source_to_target = (source_key, target_entity_version)
1026-
context.add_migration(source_key, target_entity_version.entity)
1024+
source_to_target = (source_key, target_entity_version)
1025+
context.add_migration(source_key, target_entity_version.entity if target_entity_version else None)
10271026
else:
10281027
log.warning(
10291028
f"Cannot migrate node from {context.source_context_key} to {context.target_library_key} "
@@ -1334,7 +1333,7 @@ def _create_migration_artifacts_incrementally(
13341333
ModulestoreBlockMigration.objects.create(
13351334
overall_migration=migration,
13361335
source=block_source,
1337-
target_id=target_version.entity_id,
1336+
target_id=target_version.entity_id if target_version else None,
13381337
)
13391338

13401339
processed += 1

openedx/core/lib/xblock_serializer/block_serializer.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,15 @@ def _serialize_block(self, block) -> etree.Element:
7979
else:
8080
olx = self._serialize_normal_block(block)
8181

82-
# The url_name attribute can come either because it was already in the
83-
# block's field data, or because this class adds it in the calls above.
84-
# However it gets set though, we can remove it here:
85-
if not self.write_url_name:
82+
if self.write_url_name:
83+
# Handles a weird case where url_name is not part of olx.attrib even if it is
84+
# set in block. Known case is with openassessment blocks.
85+
if "url_name" not in olx.attrib and hasattr(block, "url_name"):
86+
olx.attrib['url_name'] = block.url_name
87+
else:
88+
# The url_name attribute can come either because it was already in the
89+
# block's field data, or because this class adds it in the calls above.
90+
# However it gets set though, we can remove it here:
8691
olx.attrib.pop("url_name", None)
8792

8893
# Add copied_from_block and copied_from_version attribute the XBlock's OLX node, to help identify the source of

0 commit comments

Comments
 (0)