Skip to content

Commit 1322edf

Browse files
navinkarkeraUsamaSadiq
authored andcommitted
refactor: update sync model helper function docs and minor cleanup (#36599)
* refactor: update sync model helper function docs Adds some comments to explain certain confusing sections * refactor: sync library content method * feat: use edited_on block field * test: modified field in course block index * fix: extract uncommon methods to child class from base
1 parent 31730c3 commit 1322edf

File tree

7 files changed

+169
-101
lines changed

7 files changed

+169
-101
lines changed

cms/djangoapps/contentstore/models.py

Lines changed: 111 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,34 @@ class EntityLinkBase(models.Model):
106106
class Meta:
107107
abstract = True
108108

109+
110+
class ComponentLink(EntityLinkBase):
111+
"""
112+
This represents link between any two publishable entities or link between publishable entity and a course
113+
XBlock. It helps in tracking relationship between XBlocks imported from libraries and used in different courses.
114+
"""
115+
upstream_block = models.ForeignKey(
116+
Component,
117+
on_delete=models.SET_NULL,
118+
related_name="links",
119+
null=True,
120+
blank=True,
121+
)
122+
upstream_usage_key = UsageKeyField(
123+
max_length=255,
124+
help_text=_(
125+
"Upstream block usage key, this value cannot be null"
126+
" and useful to track upstream library blocks that do not exist yet"
127+
)
128+
)
129+
130+
class Meta:
131+
verbose_name = _("Component Link")
132+
verbose_name_plural = _("Component Links")
133+
134+
def __str__(self):
135+
return f"ComponentLink<{self.upstream_usage_key}->{self.downstream_usage_key}>"
136+
109137
@property
110138
def upstream_version_num(self) -> int | None:
111139
"""
@@ -177,34 +205,6 @@ def summarize_by_downstream_context(cls, downstream_context_key: CourseKey) -> Q
177205
)
178206
return result
179207

180-
181-
class ComponentLink(EntityLinkBase):
182-
"""
183-
This represents link between any two publishable entities or link between publishable entity and a course
184-
XBlock. It helps in tracking relationship between XBlocks imported from libraries and used in different courses.
185-
"""
186-
upstream_block = models.ForeignKey(
187-
Component,
188-
on_delete=models.SET_NULL,
189-
related_name="links",
190-
null=True,
191-
blank=True,
192-
)
193-
upstream_usage_key = UsageKeyField(
194-
max_length=255,
195-
help_text=_(
196-
"Upstream block usage key, this value cannot be null"
197-
" and useful to track upstream library blocks that do not exist yet"
198-
)
199-
)
200-
201-
class Meta:
202-
verbose_name = _("Component Link")
203-
verbose_name_plural = _("Component Links")
204-
205-
def __str__(self):
206-
return f"ComponentLink<{self.upstream_usage_key}->{self.downstream_usage_key}>"
207-
208208
@classmethod
209209
def update_or_create(
210210
cls,
@@ -232,25 +232,15 @@ def update_or_create(
232232
'version_declined': version_declined,
233233
}
234234
if upstream_block:
235-
new_values.update(
236-
{
237-
'upstream_block': upstream_block,
238-
}
239-
)
235+
new_values['upstream_block'] = upstream_block
240236
try:
241237
link = cls.objects.get(downstream_usage_key=downstream_usage_key)
242-
# TODO: until we save modified datetime for course xblocks in index, the modified time for links are updated
243-
# everytime a downstream/course block is updated. This allows us to order links[1] based on recently
244-
# modified downstream version.
245-
# pylint: disable=line-too-long
246-
# 1. https://github.com/open-craft/frontend-app-course-authoring/blob/0443d88824095f6f65a3a64b77244af590d4edff/src/course-libraries/ReviewTabContent.tsx#L222-L233
247-
has_changes = True # change to false once above condition is met.
248-
for key, value in new_values.items():
249-
prev = getattr(link, key)
250-
# None != None is True, so we need to check for it specially
251-
if prev != value and ~(prev is None and value is None):
238+
has_changes = False
239+
for key, new_value in new_values.items():
240+
prev_value = getattr(link, key)
241+
if prev_value != new_value:
252242
has_changes = True
253-
setattr(link, key, value)
243+
setattr(link, key, new_value)
254244
if has_changes:
255245
link.updated = created
256246
link.save()
@@ -290,6 +280,77 @@ class Meta:
290280
def __str__(self):
291281
return f"ContainerLink<{self.upstream_container_key}->{self.downstream_usage_key}>"
292282

283+
@property
284+
def upstream_version_num(self) -> int | None:
285+
"""
286+
Returns upstream container version number if available.
287+
"""
288+
published_version = get_published_version(self.upstream_container.publishable_entity.id)
289+
return published_version.version_num if published_version else None
290+
291+
@property
292+
def upstream_context_title(self) -> str:
293+
"""
294+
Returns upstream context title.
295+
"""
296+
return self.upstream_container.publishable_entity.learning_package.title
297+
298+
@classmethod
299+
def filter_links(
300+
cls,
301+
**link_filter,
302+
) -> QuerySet["EntityLinkBase"]:
303+
"""
304+
Get all links along with sync flag, upstream context title and version, with optional filtering.
305+
"""
306+
ready_to_sync = link_filter.pop('ready_to_sync', None)
307+
result = cls.objects.filter(**link_filter).select_related(
308+
"upstream_container__publishable_entity__published__version",
309+
"upstream_container__publishable_entity__learning_package"
310+
).annotate(
311+
ready_to_sync=(
312+
GreaterThan(
313+
Coalesce("upstream_container__publishable_entity__published__version__version_num", 0),
314+
Coalesce("version_synced", 0)
315+
) & GreaterThan(
316+
Coalesce("upstream_container__publishable_entity__published__version__version_num", 0),
317+
Coalesce("version_declined", 0)
318+
)
319+
)
320+
)
321+
if ready_to_sync is not None:
322+
result = result.filter(ready_to_sync=ready_to_sync)
323+
return result
324+
325+
@classmethod
326+
def summarize_by_downstream_context(cls, downstream_context_key: CourseKey) -> QuerySet:
327+
"""
328+
Returns a summary of links by upstream context for given downstream_context_key.
329+
Example:
330+
[
331+
{
332+
"upstream_context_title": "CS problems 3",
333+
"upstream_context_key": "lib:OpenedX:CSPROB3",
334+
"ready_to_sync_count": 11,
335+
"total_count": 14
336+
},
337+
{
338+
"upstream_context_title": "CS problems 2",
339+
"upstream_context_key": "lib:OpenedX:CSPROB2",
340+
"ready_to_sync_count": 15,
341+
"total_count": 24
342+
},
343+
]
344+
"""
345+
result = cls.filter_links(downstream_context_key=downstream_context_key).values(
346+
"upstream_context_key",
347+
upstream_context_title=F("upstream_container__publishable_entity__learning_package__title"),
348+
).annotate(
349+
ready_to_sync_count=Count("id", Q(ready_to_sync=True)),
350+
total_count=Count('id')
351+
)
352+
return result
353+
293354
@classmethod
294355
def update_or_create(
295356
cls,
@@ -317,25 +378,15 @@ def update_or_create(
317378
'version_declined': version_declined,
318379
}
319380
if upstream_container:
320-
new_values.update(
321-
{
322-
'upstream_container': upstream_container,
323-
}
324-
)
381+
new_values['upstream_container'] = upstream_container
325382
try:
326383
link = cls.objects.get(downstream_usage_key=downstream_usage_key)
327-
# TODO: until we save modified datetime for course xblocks in index, the modified time for links are updated
328-
# everytime a downstream/course block is updated. This allows us to order links[1] based on recently
329-
# modified downstream version.
330-
# pylint: disable=line-too-long
331-
# 1. https://github.com/open-craft/frontend-app-course-authoring/blob/0443d88824095f6f65a3a64b77244af590d4edff/src/course-libraries/ReviewTabContent.tsx#L222-L233
332-
has_changes = True # change to false once above condition is met.
333-
for key, value in new_values.items():
334-
prev = getattr(link, key)
335-
# None != None is True, so we need to check for it specially
336-
if prev != value and ~(prev is None and value is None):
384+
has_changes = False
385+
for key, new_value in new_values.items():
386+
prev_value = getattr(link, key)
387+
if prev_value != new_value:
337388
has_changes = True
338-
setattr(link, key, value)
389+
setattr(link, key, new_value)
339390
if has_changes:
340391
link.updated = created
341392
link.save()

cms/djangoapps/contentstore/utils.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2429,8 +2429,5 @@ def create_or_update_xblock_upstream_link(xblock, course_key: CourseKey, created
24292429
_create_or_update_component_link(course_key, created, xblock)
24302430
except InvalidKeyError:
24312431
# It is possible that the upstream is a container and UsageKeyV2 parse failed
2432-
# Create upstream container link
2433-
try:
2434-
_create_or_update_container_link(course_key, created, xblock)
2435-
except InvalidKeyError:
2436-
log.error(f"Invalid key: {xblock.upstream}")
2432+
# Create upstream container link and raise InvalidKeyError if xblock.upstream is a valid key.
2433+
_create_or_update_container_link(course_key, created, xblock)

cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,8 +548,7 @@ def sync_library_content(downstream: XBlock, request, store) -> StaticFileNotice
548548
notices = []
549549
# Store final children keys to update order of components in unit
550550
children = []
551-
for i in range(len(upstream_children)):
552-
upstream_child = upstream_children[i]
551+
for i, upstream_child in enumerate(upstream_children):
553552
assert isinstance(upstream_child, LibraryXBlockMetadata) # for now we only support units
554553
if upstream_child.usage_key not in downstream_children_keys:
555554
# This upstream_child is new, create it.
@@ -574,6 +573,7 @@ def sync_library_content(downstream: XBlock, request, store) -> StaticFileNotice
574573
for child in downstream_children:
575574
if child.usage_key not in children:
576575
# This downstream block was added, or deleted from upstream block.
576+
# NOTE: This will also delete any local additions to a unit in the next upstream sync.
577577
store.delete_item(child.usage_key, user_id=request.user.id)
578578
downstream.children = children
579579
store.update_item(downstream, request.user.id)

openedx/core/djangoapps/content/search/documents.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ class implementation returns only:
212212
Fields.access_id: _meili_access_id_from_context_key(block.usage_key.context_key),
213213
Fields.breadcrumbs: [],
214214
}
215+
if hasattr(block, "edited_on"):
216+
block_data[Fields.modified] = block.edited_on.timestamp()
215217
# Get the breadcrumbs (course, section, subsection, etc.):
216218
if block.usage_key.context_key.is_course: # Getting parent is not yet implemented in Learning Core (for libraries).
217219
cur_block = block

openedx/core/djangoapps/content/search/tests/test_api.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,27 @@ def setUp(self):
6161
# Clear the Meilisearch client to avoid side effects from other tests
6262
api.clear_meilisearch_client()
6363

64+
modified_date = datetime(2024, 5, 6, 7, 8, 9, tzinfo=timezone.utc)
6465
# Create course
65-
self.course = self.store.create_course(
66-
"org1",
67-
"test_course",
68-
"test_run",
69-
self.user_id,
70-
fields={"display_name": "Test Course"},
71-
)
72-
course_access, _ = SearchAccess.objects.get_or_create(context_key=self.course.id)
73-
self.course_block_key = "block-v1:org1+test_course+test_run+type@course+block@course"
74-
75-
# Create XBlocks
76-
self.sequential = self.store.create_child(self.user_id, self.course.location, "sequential", "test_sequential")
66+
with freeze_time(modified_date):
67+
self.course = self.store.create_course(
68+
"org1",
69+
"test_course",
70+
"test_run",
71+
self.user_id,
72+
fields={"display_name": "Test Course"},
73+
)
74+
course_access, _ = SearchAccess.objects.get_or_create(context_key=self.course.id)
75+
self.course_block_key = "block-v1:org1+test_course+test_run+type@course+block@course"
76+
77+
# Create XBlocks
78+
self.sequential = self.store.create_child(
79+
self.user_id,
80+
self.course.location,
81+
"sequential",
82+
"test_sequential"
83+
)
84+
self.store.create_child(self.user_id, self.sequential.location, "vertical", "test_vertical")
7785
self.doc_sequential = {
7886
"id": "block-v1org1test_coursetest_runtypesequentialblocktest_sequential-f702c144",
7987
"type": "course_block",
@@ -90,8 +98,8 @@ def setUp(self):
9098
],
9199
"content": {},
92100
"access_id": course_access.id,
101+
"modified": modified_date.timestamp(),
93102
}
94-
self.store.create_child(self.user_id, self.sequential.location, "vertical", "test_vertical")
95103
self.doc_vertical = {
96104
"id": "block-v1org1test_coursetest_runtypeverticalblocktest_vertical-e76a10a4",
97105
"type": "course_block",
@@ -112,6 +120,7 @@ def setUp(self):
112120
],
113121
"content": {},
114122
"access_id": course_access.id,
123+
"modified": modified_date.timestamp(),
115124
}
116125
# Make sure the CourseOverview for the course is created:
117126
CourseOverview.get_from_id(self.course.id)
@@ -130,7 +139,6 @@ def setUp(self):
130139
self.problem1 = library_api.create_library_block(self.library.key, "problem", "p1")
131140
self.problem2 = library_api.create_library_block(self.library.key, "problem", "p2")
132141
# Update problem1, freezing the date so we can verify modified date serializes correctly.
133-
modified_date = datetime(2024, 5, 6, 7, 8, 9, tzinfo=timezone.utc)
134142
with freeze_time(modified_date):
135143
library_api.set_library_block_olx(self.problem1.usage_key, "<problem />")
136144

openedx/core/djangoapps/content/search/tests/test_documents.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,23 @@ class StudioDocumentsTest(SharedModuleStoreTestCase):
5252
def setUpClass(cls):
5353
super().setUpClass()
5454
cls.store = modulestore()
55-
cls.org = Organization.objects.create(name="edX", short_name="edX")
56-
cls.toy_course = ToyCourseFactory.create() # See xmodule/modulestore/tests/sample_courses.py
57-
cls.toy_course_key = cls.toy_course.id
58-
59-
# Get references to some blocks in the toy course
60-
cls.html_block_key = cls.toy_course_key.make_usage_key("html", "toyjumpto")
61-
# Create a problem in course
62-
cls.problem_block = BlockFactory.create(
63-
category="problem",
64-
parent_location=cls.toy_course_key.make_usage_key("vertical", "vertical_test"),
65-
display_name='Test Problem',
66-
data="<problem>What is a test?<multiplechoiceresponse></multiplechoiceresponse></problem>",
67-
)
68-
6955
# Create a library and collection with a block
70-
created_date = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc)
71-
with freeze_time(created_date):
56+
cls.created_date = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc)
57+
with freeze_time(cls.created_date):
58+
# Get references to some blocks in the toy course
59+
cls.org = Organization.objects.create(name="edX", short_name="edX")
60+
cls.toy_course = ToyCourseFactory.create() # See xmodule/modulestore/tests/sample_courses.py
61+
cls.toy_course_key = cls.toy_course.id
62+
63+
cls.html_block_key = cls.toy_course_key.make_usage_key("html", "toyjumpto")
64+
# Create a problem in course
65+
cls.problem_block = BlockFactory.create(
66+
category="problem",
67+
parent_location=cls.toy_course_key.make_usage_key("vertical", "vertical_test"),
68+
display_name='Test Problem',
69+
data="<problem>What is a test?<multiplechoiceresponse></multiplechoiceresponse></problem>",
70+
)
71+
7272
cls.library = library_api.create_library(
7373
org=cls.org,
7474
slug="2012_Fall",
@@ -190,6 +190,7 @@ def test_problem_block(self):
190190
'usage_key': 'block-v1:edX+toy+2012_Fall+type@vertical+block@vertical_test',
191191
},
192192
],
193+
"modified": self.created_date.timestamp(),
193194
"content": {
194195
"capa_content": "What is a test?",
195196
"problem_types": ["multiplechoiceresponse"],
@@ -223,6 +224,7 @@ def test_html_block(self):
223224
"display_name": "Text",
224225
"description": "This is a link to another page and some Chinese 四節比分和七年前 Some "
225226
"more Chinese 四節比分和七年前 ",
227+
"modified": self.created_date.timestamp(),
226228
"breadcrumbs": [
227229
{
228230
'display_name': 'Toy Course',
@@ -276,6 +278,7 @@ def test_video_block_untagged(self):
276278
},
277279
],
278280
"content": {},
281+
"modified": self.created_date.timestamp(),
279282
# This video has no tags.
280283
}
281284

0 commit comments

Comments
 (0)