Skip to content

Commit a181330

Browse files
navinkarkeranaincy128
authored andcommitted
refactor!: use String field instead of Dict field to store top_level_downstream_parent_key (openedx#37448)
* refactor!: use String field instead of Dict field to store top_level_downstream_parent_key Since this is a new field no production instance should have this field yet. Developers need to delete their old courses as this change will raise error in all course pages. * chore: add `top_level_parent` field in ComponentLink and ContainerLink admin * refactor: use ":" as separator * refactor: block key parsing and tests
1 parent bef796b commit a181330

File tree

10 files changed

+80
-40
lines changed

10 files changed

+80
-40
lines changed

cms/djangoapps/contentstore/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class ComponentLinkAdmin(admin.ModelAdmin):
100100
"upstream_context_key",
101101
"downstream_usage_key",
102102
"downstream_context_key",
103+
"top_level_parent",
103104
"version_synced",
104105
"version_declined",
105106
"created",
@@ -139,6 +140,7 @@ class ContainerLinkAdmin(admin.ModelAdmin):
139140
"upstream_context_key",
140141
"downstream_usage_key",
141142
"downstream_context_key",
143+
"top_level_parent",
142144
"version_synced",
143145
"version_declined",
144146
"created",

cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstream_sync_integration.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@
1111
from freezegun import freeze_time
1212

1313
from openedx.core.djangoapps.content_libraries.tests import ContentLibrariesRestApiTest
14-
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import get_block_key_dict
14+
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import get_block_key_string
1515
from xmodule.modulestore.django import modulestore
1616
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
1717
from xmodule.modulestore.tests.factories import BlockFactory, CourseFactory
18-
from xmodule.xml_block import serialize_field
1918

2019

2120
@ddt.ddt
@@ -296,9 +295,9 @@ def test_unit_sync(self):
296295
parent_usage_key=str(self.course_subsection.usage_key),
297296
upstream_key=self.upstream_unit["id"],
298297
)
299-
downstream_unit_block_key = serialize_field(get_block_key_dict(
298+
downstream_unit_block_key = get_block_key_string(
300299
UsageKey.from_string(downstream_unit["locator"]),
301-
)).replace('"', '"')
300+
)
302301
status = self._get_sync_status(downstream_unit["locator"])
303302
self.assertDictContainsEntries(status, {
304303
'upstream_ref': self.upstream_unit["id"], # e.g. 'lct:CL-TEST:testlib:unit:u1'
@@ -898,9 +897,9 @@ def test_unit_sync_with_modified_downstream(self):
898897
parent_usage_key=str(self.course_subsection.usage_key),
899898
upstream_key=self.upstream_unit["id"],
900899
)
901-
downstream_unit_block_key = serialize_field(get_block_key_dict(
900+
downstream_unit_block_key = get_block_key_string(
902901
UsageKey.from_string(downstream_unit["locator"]),
903-
)).replace('"', '"')
902+
)
904903
status = self._get_sync_status(downstream_unit["locator"])
905904
self.assertDictContainsEntries(status, {
906905
'upstream_ref': self.upstream_unit["id"], # e.g. 'lct:CL-TEST:testlib:unit:u1'
@@ -1259,9 +1258,9 @@ def test_unit_decline_sync(self):
12591258
parent_usage_key=str(self.course_subsection.usage_key),
12601259
upstream_key=self.upstream_unit["id"],
12611260
)
1262-
downstream_unit_block_key = serialize_field(get_block_key_dict(
1261+
downstream_unit_block_key = get_block_key_string(
12631262
UsageKey.from_string(downstream_unit["locator"]),
1264-
)).replace('"', '"')
1263+
)
12651264
children_downstream_keys = self._get_course_block_children(downstream_unit["locator"])
12661265
downstream_problem1 = children_downstream_keys[1]
12671266
assert "type@problem" in downstream_problem1

cms/djangoapps/contentstore/rest_api/v2/views/tests/test_downstreams.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from cms.djangoapps.contentstore.helpers import StaticFileNotices
1717
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
1818
from cms.djangoapps.contentstore.xblock_storage_handlers import view_handlers as xblock_view_handlers
19-
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import get_block_key_dict
19+
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import get_block_key_string
2020
from cms.lib.xblock.upstream_sync import BadUpstream, UpstreamLink
2121
from common.djangoapps.student.auth import add_users
2222
from common.djangoapps.student.roles import CourseStaffRole
@@ -157,7 +157,7 @@ def setUp(self):
157157
parent=self.top_level_downstream_unit,
158158
upstream=self.html_lib_id_2,
159159
upstream_version=1,
160-
top_level_downstream_parent_key=get_block_key_dict(
160+
top_level_downstream_parent_key=get_block_key_string(
161161
self.top_level_downstream_unit.usage_key,
162162
)
163163
).usage_key
@@ -171,7 +171,7 @@ def setUp(self):
171171
parent=self.top_level_downstream_chapter,
172172
upstream=self.top_level_subsection_id,
173173
upstream_version=1,
174-
top_level_downstream_parent_key=get_block_key_dict(
174+
top_level_downstream_parent_key=get_block_key_string(
175175
self.top_level_downstream_chapter.usage_key,
176176
),
177177
)
@@ -180,7 +180,7 @@ def setUp(self):
180180
parent=self.top_level_downstream_sequential,
181181
upstream=self.top_level_unit_id_2,
182182
upstream_version=1,
183-
top_level_downstream_parent_key=get_block_key_dict(
183+
top_level_downstream_parent_key=get_block_key_string(
184184
self.top_level_downstream_chapter.usage_key,
185185
),
186186
)
@@ -189,7 +189,7 @@ def setUp(self):
189189
parent=self.top_level_downstream_unit_2,
190190
upstream=self.video_lib_id_2,
191191
upstream_version=1,
192-
top_level_downstream_parent_key=get_block_key_dict(
192+
top_level_downstream_parent_key=get_block_key_string(
193193
self.top_level_downstream_chapter.usage_key,
194194
)
195195
).usage_key
@@ -455,17 +455,14 @@ def test_unlink_parent_should_update_children_top_level_parent(self):
455455

456456
unit = modulestore().get_item(self.top_level_downstream_unit_2.usage_key)
457457
# The sequential is the top-level parent for the unit
458-
assert unit.top_level_downstream_parent_key == {
459-
"id": str(self.top_level_downstream_sequential.usage_key.block_id),
460-
"type": str(self.top_level_downstream_sequential.usage_key.block_type),
461-
}
458+
sequential_block_key = get_block_key_string(
459+
self.top_level_downstream_sequential.usage_key
460+
)
461+
assert unit.top_level_downstream_parent_key == sequential_block_key
462462

463463
video = modulestore().get_item(self.top_level_downstream_video_key)
464464
# The sequential is the top-level parent for the video
465-
assert video.top_level_downstream_parent_key == {
466-
"id": str(self.top_level_downstream_sequential.usage_key.block_id),
467-
"type": str(self.top_level_downstream_sequential.usage_key.block_type),
468-
}
465+
assert video.top_level_downstream_parent_key == sequential_block_key
469466

470467
all_downstreams = self.client.get(
471468
"/api/contentstore/v2/downstreams/",
@@ -1249,8 +1246,6 @@ def test_200_get_ready_to_sync_top_level_parents_with_components(self):
12491246
'downstream_is_modified': False,
12501247
},
12511248
]
1252-
print(data["results"])
1253-
print(expected)
12541249
self.assertListEqual(data["results"], expected)
12551250

12561251
def test_200_get_ready_to_sync_top_level_parents_with_containers(self):

cms/djangoapps/contentstore/tasks.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
from xmodule.modulestore.xml_exporter import export_course_to_xml, export_library_to_xml
9393
from xmodule.modulestore.xml_importer import CourseImportException, import_course_from_xml, import_library_from_xml
9494
from xmodule.tabs import StaticTab
95+
from xmodule.util.keys import BlockKey
9596

9697
from .models import ComponentLink, ContainerLink, LearningContextLinksStatus, LearningContextLinksStatusChoices
9798
from .outlines import update_outline_from_modulestore
@@ -1649,10 +1650,11 @@ def handle_create_xblock_upstream_link(usage_key):
16491650
if not xblock.upstream or not xblock.upstream_version:
16501651
return
16511652
if xblock.top_level_downstream_parent_key is not None:
1653+
block_key = BlockKey.from_string(xblock.top_level_downstream_parent_key)
16521654
top_level_parent_usage_key = BlockUsageLocator(
16531655
xblock.course_id,
1654-
xblock.top_level_downstream_parent_key.get('type'),
1655-
xblock.top_level_downstream_parent_key.get('id'),
1656+
block_key.type,
1657+
block_key.id,
16561658
)
16571659
try:
16581660
ContainerLink.get_by_downstream_usage_key(top_level_parent_usage_key)

cms/djangoapps/contentstore/utils.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
get_all_partitions_for_course, # lint-amnesty, pylint: disable=wrong-import-order
118118
)
119119
from xmodule.services import ConfigurationService, SettingsService, TeamsConfigurationService
120+
from xmodule.util.keys import BlockKey
120121

121122
from .models import ComponentLink, ContainerLink
122123

@@ -2411,10 +2412,11 @@ def _create_or_update_component_link(created: datetime | None, xblock):
24112412

24122413
top_level_parent_usage_key = None
24132414
if xblock.top_level_downstream_parent_key is not None:
2415+
block_key = BlockKey.from_string(xblock.top_level_downstream_parent_key)
24142416
top_level_parent_usage_key = BlockUsageLocator(
24152417
xblock.usage_key.course_key,
2416-
xblock.top_level_downstream_parent_key.get('type'),
2417-
xblock.top_level_downstream_parent_key.get('id'),
2418+
block_key.type,
2419+
block_key.id,
24182420
)
24192421

24202422
ComponentLink.update_or_create(
@@ -2444,10 +2446,11 @@ def _create_or_update_container_link(created: datetime | None, xblock):
24442446

24452447
top_level_parent_usage_key = None
24462448
if xblock.top_level_downstream_parent_key is not None:
2449+
block_key = BlockKey.from_string(xblock.top_level_downstream_parent_key)
24472450
top_level_parent_usage_key = BlockUsageLocator(
24482451
xblock.usage_key.course_key,
2449-
xblock.top_level_downstream_parent_key.get('type'),
2450-
xblock.top_level_downstream_parent_key.get('id'),
2452+
block_key.type,
2453+
block_key.id,
24512454
)
24522455

24532456
ContainerLink.update_or_create(

cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from pytz import UTC
3434
from xblock.core import XBlock
3535
from xblock.fields import Scope
36-
from .xblock_helpers import get_block_key_dict
36+
from .xblock_helpers import get_block_key_string
3737

3838
from cms.djangoapps.contentstore.config.waffle import SHOW_REVIEW_RULES_FLAG
3939
from cms.djangoapps.contentstore.helpers import StaticFileNotices
@@ -602,7 +602,7 @@ def sync_library_content(
602602
block_id=f"{block_type}{uuid4().hex[:8]}",
603603
fields={
604604
"upstream": upstream_key,
605-
"top_level_downstream_parent_key": get_block_key_dict(
605+
"top_level_downstream_parent_key": get_block_key_string(
606606
top_level_downstream_parent.usage_key,
607607
),
608608
},

cms/djangoapps/contentstore/xblock_storage_handlers/xblock_helpers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ def usage_key_with_run(usage_key_string: str) -> UsageKey:
1818
return usage_key
1919

2020

21-
def get_block_key_dict(usage_key: UsageKey) -> dict:
21+
def get_block_key_string(usage_key: UsageKey) -> str:
2222
"""
23-
Converts the usage_key in a dict with the form: `{"type": block_type, "id": block_id}`
23+
Extract block key from UsageKey in string format: `html:my-id`.
2424
"""
25-
return BlockKey.from_usage_key(usage_key)._asdict()
25+
return str(BlockKey.from_usage_key(usage_key))
2626

2727

2828
def get_tags_count(xblock: XBlock, include_children=False) -> dict[str, int]:

cms/lib/xblock/upstream_sync.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryUsageLocatorV2
2727
from opaque_keys.edx.keys import UsageKey
2828
from xblock.exceptions import XBlockNotFoundError
29-
from xblock.fields import Scope, String, Integer, Dict, List
29+
from xblock.fields import Scope, String, Integer, List
3030
from xblock.core import XBlockMixin, XBlock
3131
from xmodule.util.keys import BlockKey
3232

@@ -337,7 +337,7 @@ def decline_sync(downstream: XBlock, user_id=None) -> None:
337337

338338
def _update_children_top_level_parent(
339339
downstream: XBlock,
340-
new_top_level_parent_key: dict[str, str] | None
340+
new_top_level_parent_key: str | None,
341341
) -> list[XBlock]:
342342
"""
343343
Given a new top-level parent block, update the `top_level_downstream_parent_key` field on the downstream block
@@ -357,7 +357,7 @@ def _update_children_top_level_parent(
357357
# If the `new_top_level_parent_key` is None, the current level assume the top-level
358358
# parent key for its children.
359359
child_top_level_parent_key = new_top_level_parent_key if new_top_level_parent_key is not None else (
360-
BlockKey.from_usage_key(child.usage_key)._asdict()
360+
str(BlockKey.from_usage_key(child.usage_key))
361361
)
362362

363363
affected_blocks.extend(_update_children_top_level_parent(child, child_top_level_parent_key))
@@ -466,7 +466,7 @@ class UpstreamSyncMixin(XBlockMixin):
466466
default=None, scope=Scope.settings, hidden=True, enforce_type=True,
467467
)
468468

469-
top_level_downstream_parent_key = Dict(
469+
top_level_downstream_parent_key = String(
470470
help=(
471471
"The block key ('block_type@block_id') of the downstream block that is the top-level parent of "
472472
"this block. This is present if the creation of this block is a consequence of "

xmodule/tests/test_util_keys.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Tests for xmodule/util/keys.py
33
"""
44
import ddt
5+
import pytest
56
from unittest import TestCase
67
from unittest.mock import Mock
78

@@ -43,3 +44,29 @@ def test_derive_key(self, source, parent, expected):
4344
Test that derive_key returns the expected value.
4445
"""
4546
assert derive_key(source, parent) == expected
47+
48+
49+
@ddt.ddt
50+
class TestBlockKeyParsing(TestCase):
51+
"""
52+
Tests for parsing BlockKeys.
53+
"""
54+
55+
@ddt.data(['chapter:some-id', 'chapter', 'some-id'], ['section:one-more-id', 'section', 'one-more-id'])
56+
@ddt.unpack
57+
def test_block_key_from_string(self, block_key_str, blockType, blockId):
58+
block_key = BlockKey.from_string(block_key_str)
59+
assert block_key.type == blockType
60+
assert block_key.id == blockId
61+
62+
@ddt.data('chapter:invalid:some-id', 'sectionone-more-id')
63+
def test_block_key_from_string_error(self, block_key_str):
64+
with pytest.raises(ValueError):
65+
BlockKey.from_string(block_key_str)
66+
67+
@ddt.data(
68+
[BlockKey('chapter', 'some-id'), 'chapter:some-id'], [BlockKey('section', 'one-more-id'), 'section:one-more-id']
69+
)
70+
@ddt.unpack
71+
def test_block_key_to_string(self, block_key, block_key_str):
72+
assert str(block_key) == block_key_str

xmodule/util/keys.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
Consider moving these into opaque-keys if they generalize well.
55
"""
66
import hashlib
7-
from typing import NamedTuple
8-
7+
from typing import NamedTuple, Self
98

109
from opaque_keys.edx.keys import UsageKey
1110

@@ -28,6 +27,19 @@ class BlockKey(NamedTuple):
2827
def from_usage_key(cls, usage_key):
2928
return cls(usage_key.block_type, usage_key.block_id)
3029

30+
def __str__(self) -> str:
31+
return f"{self.type}:{self.id}"
32+
33+
@classmethod
34+
def from_string(cls, s: str) -> Self:
35+
"""
36+
Convert a BlockKey string into a BlockKey object.
37+
"""
38+
parts = s.split(':')
39+
if len(parts) != 2 or not parts[0] or not parts[1]:
40+
raise ValueError(f"Invalid string format for BlockKey: {s}")
41+
return cls(parts[0], parts[1])
42+
3143

3244
def derive_key(source: UsageKey, dest_parent: BlockKey) -> BlockKey:
3345
"""

0 commit comments

Comments
 (0)