Skip to content

Commit 5b3caa9

Browse files
authored
feat: store content.child_usage_keys in Container search document [FC-0083] (#36528)
* feat: store content.child_usage_keys in Container search document Stores the draft children + published children (if applicable) Related fixes: * fix: lib_api.get_container does not take a "user" arg * refactor: fetch_customizable_fields_from_container does not need a "user" arg * refactor: moves tags_count into LibraryItem because anything that appears in a library may be tagged. * refactor: remove get_container_from_key from public API API users must use get_container and ContainerMetadata. * refactor: made set_library_item_collections take an entity_key string instead of a PublishableEntity instance, so we don't need to fetch a Container object to call it. * refactor: changed ContainerLink.update_or_create to take the container PK instead of a Container object, since this is enough. Added container_pk to ContainerMetadata to support this.
1 parent 6508010 commit 5b3caa9

15 files changed

Lines changed: 95 additions & 44 deletions

File tree

cms/djangoapps/contentstore/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ def summarize_by_downstream_context(cls, downstream_context_key: CourseKey) -> Q
354354
@classmethod
355355
def update_or_create(
356356
cls,
357-
upstream_container: Container | None,
357+
upstream_container_id: int | None,
358358
/,
359359
upstream_container_key: LibraryContainerLocator,
360360
upstream_context_key: str,
@@ -377,8 +377,8 @@ def update_or_create(
377377
'version_synced': version_synced,
378378
'version_declined': version_declined,
379379
}
380-
if upstream_container:
381-
new_values['upstream_container'] = upstream_container
380+
if upstream_container_id:
381+
new_values['upstream_container_id'] = upstream_container_id
382382
try:
383383
link = cls.objects.get(downstream_usage_key=downstream_usage_key)
384384
has_changes = False

cms/djangoapps/contentstore/rest_api/v2/views/downstreams.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def put(self, request: _AuthenticatedRequest, usage_key_string: str) -> Response
267267
fetch_customizable_fields_from_block(downstream=downstream, user=request.user)
268268
else:
269269
assert isinstance(link.upstream_key, LibraryContainerLocator)
270-
fetch_customizable_fields_from_container(downstream=downstream, user=request.user)
270+
fetch_customizable_fields_from_container(downstream=downstream)
271271
except BadDownstream as exc:
272272
logger.exception(
273273
"'%s' is an invalid downstream; refusing to set its upstream to '%s'",

cms/djangoapps/contentstore/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
from common.djangoapps.xblock_django.api import deprecated_xblocks
8888
from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService
8989
from openedx.core import toggles as core_toggles
90-
from openedx.core.djangoapps.content_libraries.api import get_container_from_key
90+
from openedx.core.djangoapps.content_libraries.api import get_container
9191
from openedx.core.djangoapps.content_tagging.toggles import is_tagging_feature_disabled
9292
from openedx.core.djangoapps.credit.api import get_credit_requirements, is_credit_course
9393
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND
@@ -2402,7 +2402,7 @@ def _create_or_update_container_link(course_key: CourseKey, created: datetime |
24022402
"""
24032403
upstream_container_key = LibraryContainerLocator.from_string(xblock.upstream)
24042404
try:
2405-
lib_component = get_container_from_key(upstream_container_key)
2405+
lib_component = get_container(upstream_container_key).container_pk
24062406
except ObjectDoesNotExist:
24072407
log.error(f"Library component not found for {upstream_container_key}")
24082408
lib_component = None

cms/lib/xblock/upstream_sync_container.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def sync_from_upstream_container(
4545
user,
4646
permission=lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY,
4747
)
48-
upstream_meta = lib_api.get_container(link.upstream_key, user)
48+
upstream_meta = lib_api.get_container(link.upstream_key)
4949
upstream_children = lib_api.get_container_children(link.upstream_key, published=True)
5050
_update_customizable_fields(upstream=upstream_meta, downstream=downstream, only_fetch=False)
5151
_update_non_customizable_fields(upstream=upstream_meta, downstream=downstream)
@@ -54,15 +54,15 @@ def sync_from_upstream_container(
5454
return upstream_children
5555

5656

57-
def fetch_customizable_fields_from_container(*, downstream: XBlock, user: User) -> None:
57+
def fetch_customizable_fields_from_container(*, downstream: XBlock) -> None:
5858
"""
5959
Fetch upstream-defined value of customizable fields and save them on the downstream.
6060
6161
The container version only retrieves values from *published* containers.
6262
6363
Basically, this sets the value of "upstream_display_name" on the downstream block.
6464
"""
65-
upstream = lib_api.get_container(LibraryContainerLocator.from_string(downstream.upstream), user)
65+
upstream = lib_api.get_container(LibraryContainerLocator.from_string(downstream.upstream))
6666
_update_customizable_fields(upstream=upstream, downstream=downstream, only_fetch=True)
6767

6868

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Fields:
7171
# The "content" field is a dictionary of arbitrary data, depending on the block_type.
7272
# It comes from each XBlock's index_dictionary() method (if present) plus some processing.
7373
# Text (html) blocks have an "html_content" key in here, capa has "capa_content" and "problem_types", and so on.
74+
# Containers store their list of child usage keys here.
7475
content = "content"
7576

7677
# Collections use this field to communicate how many entities/components they contain.
@@ -87,6 +88,7 @@ class Fields:
8788
published = "published"
8889
published_display_name = "display_name"
8990
published_description = "description"
91+
published_content = "content"
9092
published_num_children = "num_children"
9193

9294
# Note: new fields or values can be added at any time, but if they need to be indexed for filtering or keyword
@@ -347,13 +349,10 @@ def _collections_for_content_object(object_id: OpaqueKey) -> dict:
347349
collections = authoring_api.get_entity_collections(
348350
component.learning_package_id,
349351
component.key,
350-
)
352+
).values('key', 'title')
351353
elif isinstance(object_id, LibraryContainerLocator):
352-
container = lib_api.get_container_from_key(object_id)
353-
collections = authoring_api.get_entity_collections(
354-
container.publishable_entity.learning_package_id,
355-
container.key,
356-
)
354+
container = lib_api.get_container(object_id, include_collections=True)
355+
collections = container.collections
357356
else:
358357
log.warning(f"Unexpected key type for {object_id}")
359358

@@ -364,8 +363,8 @@ def _collections_for_content_object(object_id: OpaqueKey) -> dict:
364363
return result
365364

366365
for collection in collections:
367-
result[Fields.collections][Fields.collections_display_name].append(collection.title)
368-
result[Fields.collections][Fields.collections_key].append(collection.key)
366+
result[Fields.collections][Fields.collections_display_name].append(collection["title"])
367+
result[Fields.collections][Fields.collections_key].append(collection["key"])
369368

370369
return result
371370

@@ -581,9 +580,13 @@ def searchable_doc_for_container(
581580
container = lib_api.get_container(container_key)
582581
except lib_api.ContentLibraryContainerNotFound:
583582
# Container not found, so we can only return the base doc
583+
log.error(f"Container {container_key} not found")
584584
return doc
585585

586-
draft_num_children = lib_api.get_container_children_count(container_key, published=False)
586+
draft_children = lib_api.get_container_children(
587+
container_key,
588+
published=False,
589+
)
587590
publish_status = PublishStatus.published
588591
if container.last_published is None:
589592
publish_status = PublishStatus.never
@@ -594,7 +597,13 @@ def searchable_doc_for_container(
594597
Fields.display_name: container.display_name,
595598
Fields.created: container.created.timestamp(),
596599
Fields.modified: container.modified.timestamp(),
597-
Fields.num_children: draft_num_children,
600+
Fields.num_children: len(draft_children),
601+
Fields.content: {
602+
"child_usage_keys": [
603+
str(child.usage_key)
604+
for child in draft_children
605+
],
606+
},
598607
Fields.publish_status: publish_status,
599608
Fields.last_published: container.last_published.timestamp() if container.last_published else None,
600609
})
@@ -603,10 +612,19 @@ def searchable_doc_for_container(
603612
doc[Fields.breadcrumbs] = [{"display_name": library.title}]
604613

605614
if container.published_version_num is not None:
606-
published_num_children = lib_api.get_container_children_count(container_key, published=True)
615+
published_children = lib_api.get_container_children(
616+
container_key,
617+
published=True,
618+
)
607619
doc[Fields.published] = {
608-
Fields.published_num_children: published_num_children,
609620
Fields.published_display_name: container.published_display_name,
621+
Fields.published_num_children: len(published_children),
622+
Fields.published_content: {
623+
"child_usage_keys": [
624+
str(child.usage_key)
625+
for child in published_children
626+
],
627+
},
610628
}
611629

612630
return doc

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ def setUp(self):
238238
"display_name": "Unit 1",
239239
# description is not set for containers
240240
"num_children": 0,
241+
"content": {"child_usage_keys": []},
241242
"publish_status": "never",
242243
"context_key": "lib:org1:lib",
243244
"org": "org1",

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,9 @@ def test_draft_container(self):
531531
"display_name": "A Unit in the Search Index",
532532
# description is not set for containers
533533
"num_children": 0,
534+
"content": {
535+
"child_usage_keys": [],
536+
},
534537
"publish_status": "never",
535538
"context_key": "lib:edX:2012_Fall",
536539
"access_id": self.library_access_id,
@@ -571,6 +574,11 @@ def test_published_container(self):
571574
"display_name": "A Unit in the Search Index",
572575
# description is not set for containers
573576
"num_children": 1,
577+
"content": {
578+
"child_usage_keys": [
579+
"lb:edX:2012_Fall:html:text2",
580+
],
581+
},
574582
"publish_status": "published",
575583
"context_key": "lib:edX:2012_Fall",
576584
"access_id": self.library_access_id,
@@ -585,6 +593,11 @@ def test_published_container(self):
585593
"published": {
586594
"num_children": 1,
587595
"display_name": "A Unit in the Search Index",
596+
"content": {
597+
"child_usage_keys": [
598+
"lb:edX:2012_Fall:html:text2",
599+
],
600+
},
588601
},
589602
}
590603

@@ -627,6 +640,12 @@ def test_published_container_with_changes(self):
627640
"display_name": "A Unit in the Search Index",
628641
# description is not set for containers
629642
"num_children": 2,
643+
"content": {
644+
"child_usage_keys": [
645+
"lb:edX:2012_Fall:html:text2",
646+
"lb:edX:2012_Fall:html:text3",
647+
],
648+
},
630649
"publish_status": "modified",
631650
"context_key": "lib:edX:2012_Fall",
632651
"access_id": self.library_access_id,
@@ -641,6 +660,11 @@ def test_published_container_with_changes(self):
641660
"published": {
642661
"num_children": 1,
643662
"display_name": "A Unit in the Search Index",
663+
"content": {
664+
"child_usage_keys": [
665+
"lb:edX:2012_Fall:html:text2",
666+
],
667+
},
644668
},
645669
}
646670

openedx/core/djangoapps/content_libraries/api/block_metadata.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ class LibraryXBlockMetadata(PublishableItem):
2626
Class that represents the metadata about an XBlock in a content library.
2727
"""
2828
usage_key: LibraryUsageLocatorV2
29-
# TODO: move tags_count to LibraryItem as all objects under a library can be tagged.
30-
tags_count: int = 0
3129

3230
@classmethod
3331
def from_component(cls, library_key, component, associated_collections=None):

openedx/core/djangoapps/content_libraries/api/collections.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def update_library_collection_items(
181181

182182
def set_library_item_collections(
183183
library_key: LibraryLocatorV2,
184-
publishable_entity: PublishableEntity,
184+
entity_key: str,
185185
*,
186186
collection_keys: list[str],
187187
created_by: int | None = None,
@@ -207,6 +207,11 @@ def set_library_item_collections(
207207
assert content_library.learning_package_id
208208
assert content_library.library_key == library_key
209209

210+
publishable_entity = authoring_api.get_publishable_entity_by_key(
211+
content_library.learning_package_id,
212+
key=entity_key,
213+
)
214+
210215
# Note: Component.key matches its PublishableEntity.key
211216
collection_qs = authoring_api.get_collections(content_library.learning_package_id).filter(
212217
key__in=collection_keys

openedx/core/djangoapps/content_libraries/api/containers.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"ContainerMetadata",
4444
"ContainerType",
4545
# API methods
46-
"get_container_from_key",
4746
"get_container",
4847
"create_container",
4948
"get_container_children",
@@ -111,6 +110,7 @@ class ContainerMetadata(PublishableItem):
111110
"""
112111
container_key: LibraryContainerLocator
113112
container_type: ContainerType
113+
container_pk: int
114114
published_display_name: str | None
115115

116116
@classmethod
@@ -139,6 +139,7 @@ def from_container(cls, library_key, container: Container, associated_collection
139139
return cls(
140140
container_key=container_key,
141141
container_type=container_type,
142+
container_pk=container.pk,
142143
display_name=draft.title,
143144
created=container.created,
144145
modified=draft.created,
@@ -173,7 +174,7 @@ def library_container_locator(
173174
)
174175

175176

176-
def get_container_from_key(container_key: LibraryContainerLocator, isDeleted=False) -> Container:
177+
def _get_container_from_key(container_key: LibraryContainerLocator, isDeleted=False) -> Container:
177178
"""
178179
Internal method to fetch the Container object from its LibraryContainerLocator
179180
@@ -192,11 +193,15 @@ def get_container_from_key(container_key: LibraryContainerLocator, isDeleted=Fal
192193
raise ContentLibraryContainerNotFound
193194

194195

195-
def get_container(container_key: LibraryContainerLocator, include_collections=False) -> ContainerMetadata:
196+
def get_container(
197+
container_key: LibraryContainerLocator,
198+
*,
199+
include_collections=False,
200+
) -> ContainerMetadata:
196201
"""
197202
Get a container (a Section, Subsection, or Unit).
198203
"""
199-
container = get_container_from_key(container_key)
204+
container = _get_container_from_key(container_key)
200205
if include_collections:
201206
associated_collections = authoring_api.get_entity_collections(
202207
container.publishable_entity.learning_package_id,
@@ -268,7 +273,7 @@ def update_container(
268273
"""
269274
Update a container (e.g. a Unit) title.
270275
"""
271-
container = get_container_from_key(container_key)
276+
container = _get_container_from_key(container_key)
272277
library_key = container_key.lib_key
273278

274279
assert container.unit
@@ -297,7 +302,7 @@ def delete_container(
297302
No-op if container doesn't exist or has already been soft-deleted.
298303
"""
299304
library_key = container_key.lib_key
300-
container = get_container_from_key(container_key)
305+
container = _get_container_from_key(container_key)
301306

302307
affected_collections = authoring_api.get_entity_collections(
303308
container.publishable_entity.learning_package_id,
@@ -332,7 +337,7 @@ def restore_container(container_key: LibraryContainerLocator) -> None:
332337
Restore the specified library container.
333338
"""
334339
library_key = container_key.lib_key
335-
container = get_container_from_key(container_key, isDeleted=True)
340+
container = _get_container_from_key(container_key, isDeleted=True)
336341

337342
affected_collections = authoring_api.get_entity_collections(
338343
container.publishable_entity.learning_package_id,
@@ -372,12 +377,13 @@ def restore_container(container_key: LibraryContainerLocator) -> None:
372377

373378
def get_container_children(
374379
container_key: LibraryContainerLocator,
380+
*,
375381
published=False,
376382
) -> list[LibraryXBlockMetadata | ContainerMetadata]:
377383
"""
378384
Get the entities contained in the given container (e.g. the components/xblocks in a unit)
379385
"""
380-
container = get_container_from_key(container_key)
386+
container = _get_container_from_key(container_key)
381387
if container_key.container_type == ContainerType.Unit.value:
382388
child_components = authoring_api.get_components_in_unit(container.unit, published=published)
383389
return [LibraryXBlockMetadata.from_component(
@@ -399,7 +405,7 @@ def get_container_children_count(
399405
"""
400406
Get the count of entities contained in the given container (e.g. the components/xblocks in a unit)
401407
"""
402-
container = get_container_from_key(container_key)
408+
container = _get_container_from_key(container_key)
403409
return authoring_api.get_container_children_count(container, published=published)
404410

405411

@@ -414,7 +420,7 @@ def update_container_children(
414420
"""
415421
library_key = container_key.lib_key
416422
container_type = container_key.container_type
417-
container = get_container_from_key(container_key)
423+
container = _get_container_from_key(container_key)
418424
match container_type:
419425
case ContainerType.Unit.value:
420426
components = [get_component_from_usage_key(key) for key in children_ids] # type: ignore[arg-type]
@@ -459,7 +465,7 @@ def publish_container_changes(container_key: LibraryContainerLocator, user_id: i
459465
Publish all unpublished changes in a container and all its child
460466
containers/blocks.
461467
"""
462-
container = get_container_from_key(container_key)
468+
container = _get_container_from_key(container_key)
463469
library_key = container_key.lib_key
464470
content_library = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined]
465471
learning_package = content_library.learning_package

0 commit comments

Comments
 (0)