Skip to content

Commit 353d2fd

Browse files
committed
feat: collections support for containers
1 parent dc4144e commit 353d2fd

24 files changed

Lines changed: 503 additions & 233 deletions

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

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,37 @@
1717
from meilisearch import Client as MeilisearchClient
1818
from meilisearch.errors import MeilisearchApiError, MeilisearchError
1919
from meilisearch.models.task import TaskInfo
20-
from opaque_keys.edx.keys import UsageKey, OpaqueKey
21-
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2, LibraryCollectionLocator
20+
from opaque_keys import OpaqueKey
21+
from opaque_keys.edx.keys import UsageKey
22+
from opaque_keys.edx.locator import LibraryCollectionLocator, LibraryContainerLocator, LibraryLocatorV2
2223
from openedx_learning.api import authoring as authoring_api
23-
from common.djangoapps.student.roles import GlobalStaff
2424
from rest_framework.request import Request
25+
2526
from common.djangoapps.student.role_helpers import get_course_roles
27+
from common.djangoapps.student.roles import GlobalStaff
2628
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
27-
from openedx.core.djangoapps.content.search.models import get_access_ids_for_request, IncrementalIndexCompleted
2829
from openedx.core.djangoapps.content.search.index_config import (
2930
INDEX_DISTINCT_ATTRIBUTE,
3031
INDEX_FILTERABLE_ATTRIBUTES,
31-
INDEX_SEARCHABLE_ATTRIBUTES,
32-
INDEX_SORTABLE_ATTRIBUTES,
3332
INDEX_RANKING_RULES,
33+
INDEX_SEARCHABLE_ATTRIBUTES,
34+
INDEX_SORTABLE_ATTRIBUTES
3435
)
36+
from openedx.core.djangoapps.content.search.models import IncrementalIndexCompleted, get_access_ids_for_request
3537
from openedx.core.djangoapps.content_libraries import api as lib_api
3638
from xmodule.modulestore.django import modulestore
3739

3840
from .documents import (
3941
Fields,
4042
meili_id_from_opaque_key,
41-
searchable_doc_for_course_block,
43+
searchable_doc_collections,
4244
searchable_doc_for_collection,
4345
searchable_doc_for_container,
46+
searchable_doc_for_course_block,
4447
searchable_doc_for_library_block,
4548
searchable_doc_for_key,
46-
searchable_doc_collections,
4749
searchable_doc_tags,
48-
searchable_doc_tags_for_collection,
50+
searchable_doc_tags_for_collection
4951
)
5052

5153
log = logging.getLogger(__name__)
@@ -487,6 +489,7 @@ def index_container_batch(batch, num_done, library_key) -> int:
487489
)
488490
doc = searchable_doc_for_container(container_key)
489491
doc.update(searchable_doc_tags(container_key))
492+
doc.update(searchable_doc_collections(container_key))
490493
docs.append(doc)
491494
except Exception as err: # pylint: disable=broad-except
492495
status_cb(f"Error indexing container {container.key}: {err}")
@@ -709,15 +712,15 @@ def upsert_library_collection_index_doc(library_key: LibraryLocatorV2, collectio
709712
If the Collection is not found or disabled (i.e. soft-deleted), then delete it from the search index.
710713
"""
711714
doc = searchable_doc_for_collection(library_key, collection_key)
712-
update_components = False
715+
update_items = False
713716

714717
# Soft-deleted/disabled collections are removed from the index
715718
# and their components updated.
716719
if doc.get('_disabled'):
717720

718721
_delete_index_doc(doc[Fields.id])
719722

720-
update_components = True
723+
update_items = True
721724

722725
# Hard-deleted collections are also deleted from the index,
723726
# but their components are automatically updated as part of the deletion process, so we don't have to.
@@ -735,10 +738,12 @@ def upsert_library_collection_index_doc(library_key: LibraryLocatorV2, collectio
735738
_update_index_docs([doc])
736739

737740
# Asynchronously update the collection's components "collections" field
738-
if update_components:
739-
from .tasks import update_library_components_collections as update_task
741+
if update_items:
742+
from .tasks import update_library_components_collections as update_components_task
743+
from .tasks import update_library_containers_collections as update_containers_task
740744

741-
update_task.delay(str(library_key), collection_key)
745+
update_components_task.delay(str(library_key), collection_key)
746+
update_containers_task.delay(str(library_key), collection_key)
742747

743748

744749
def update_library_components_collections(
@@ -773,6 +778,38 @@ def update_library_components_collections(
773778
_update_index_docs(docs)
774779

775780

781+
def update_library_containers_collections(
782+
library_key: LibraryLocatorV2,
783+
collection_key: str,
784+
batch_size: int = 1000,
785+
) -> None:
786+
"""
787+
Updates the "collections" field for all containers associated with a given Library Collection.
788+
789+
Because there may be a lot of containers, we send these updates to Meilisearch in batches.
790+
"""
791+
library = lib_api.get_library(library_key)
792+
containers = authoring_api.get_collection_containers(library.learning_package_id, collection_key)
793+
794+
paginator = Paginator(containers, batch_size)
795+
for page in paginator.page_range:
796+
docs = []
797+
798+
for container in paginator.page(page).object_list:
799+
container_key = lib_api.library_container_locator(
800+
library_key,
801+
container,
802+
)
803+
doc = searchable_doc_collections(container_key)
804+
docs.append(doc)
805+
806+
log.info(
807+
f"Updating document.collections for library {library_key} containers"
808+
f" page {page} / {paginator.num_pages}"
809+
)
810+
_update_index_docs(docs)
811+
812+
776813
def upsert_library_container_index_doc(container_key: LibraryContainerLocator) -> None:
777814
"""
778815
Creates, updates, or deletes the document for the given Library Container in the search index.
@@ -819,12 +856,12 @@ def upsert_content_object_tags_index_doc(key: OpaqueKey):
819856
_update_index_docs([doc])
820857

821858

822-
def upsert_block_collections_index_docs(usage_key: UsageKey):
859+
def upsert_item_collections_index_docs(opaque_key: OpaqueKey):
823860
"""
824-
Updates the collections data in documents for the given Course/Library block
861+
Updates the collections data in documents for the given Course/Library block, or Container
825862
"""
826-
doc = {Fields.id: meili_id_from_opaque_key(usage_key)}
827-
doc.update(searchable_doc_collections(usage_key))
863+
doc = {Fields.id: meili_id_from_opaque_key(opaque_key)}
864+
doc.update(searchable_doc_collections(opaque_key))
828865
_update_index_docs([doc])
829866

830867

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def _tags_for_content_object(object_id: OpaqueKey) -> dict:
309309
return {Fields.tags: result}
310310

311311

312-
def _collections_for_content_object(object_id: UsageKey | LearningContextKey) -> dict:
312+
def _collections_for_content_object(object_id: OpaqueKey) -> dict:
313313
"""
314314
Given an XBlock, course, library, etc., get the collections for its index doc.
315315
@@ -340,11 +340,21 @@ def _collections_for_content_object(object_id: UsageKey | LearningContextKey) ->
340340
# Gather the collections associated with this object
341341
collections = None
342342
try:
343-
component = lib_api.get_component_from_usage_key(object_id)
344-
collections = authoring_api.get_entity_collections(
345-
component.learning_package_id,
346-
component.key,
347-
)
343+
if isinstance(object_id, UsageKey):
344+
component = lib_api.get_component_from_usage_key(object_id)
345+
collections = authoring_api.get_entity_collections(
346+
component.learning_package_id,
347+
component.key,
348+
)
349+
elif isinstance(object_id, LibraryContainerLocator):
350+
container = lib_api.get_container_from_key(object_id)
351+
collections = authoring_api.get_entity_collections(
352+
container.publishable_entity.learning_package_id,
353+
container.key,
354+
)
355+
else:
356+
return result
357+
348358
except ObjectDoesNotExist:
349359
log.warning(f"No component found for {object_id}")
350360

@@ -438,13 +448,13 @@ def searchable_doc_tags(key: OpaqueKey) -> dict:
438448
return doc
439449

440450

441-
def searchable_doc_collections(usage_key: UsageKey) -> dict:
451+
def searchable_doc_collections(opaque_key: OpaqueKey) -> dict:
442452
"""
443453
Generate a dictionary document suitable for ingestion into a search engine
444454
like Meilisearch or Elasticsearch, with the collections data for the given content object.
445455
"""
446-
doc = searchable_doc_for_key(usage_key)
447-
doc.update(_collections_for_content_object(usage_key))
456+
doc = searchable_doc_for_key(opaque_key)
457+
doc.update(_collections_for_content_object(opaque_key))
448458

449459
return doc
450460

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040

4141
from .api import (
4242
only_if_meilisearch_enabled,
43-
upsert_block_collections_index_docs,
4443
upsert_content_object_tags_index_doc,
4544
upsert_collection_tags_index_docs,
45+
upsert_item_collections_index_docs,
4646
)
4747
from .tasks import (
4848
delete_library_block_index_doc,
@@ -213,15 +213,15 @@ def content_object_associations_changed_handler(**kwargs) -> None:
213213

214214
try:
215215
# Check if valid course or library block
216-
usage_key = UsageKey.from_string(str(content_object.object_id))
216+
opaque_key = UsageKey.from_string(str(content_object.object_id))
217217
except InvalidKeyError:
218218
try:
219219
# Check if valid library collection
220-
usage_key = LibraryCollectionLocator.from_string(str(content_object.object_id))
220+
opaque_key = LibraryCollectionLocator.from_string(str(content_object.object_id))
221221
except InvalidKeyError:
222222
try:
223223
# Check if valid library container
224-
usage_key = LibraryContainerLocator.from_string(str(content_object.object_id))
224+
opaque_key = LibraryContainerLocator.from_string(str(content_object.object_id))
225225
except InvalidKeyError:
226226
# Invalid content object id
227227
log.error("Received invalid content object id")
@@ -230,12 +230,12 @@ def content_object_associations_changed_handler(**kwargs) -> None:
230230
# This event's changes may contain both "tags" and "collections", but this will happen rarely, if ever.
231231
# So we allow a potential double "upsert" here.
232232
if not content_object.changes or "tags" in content_object.changes:
233-
if isinstance(usage_key, LibraryCollectionLocator):
234-
upsert_collection_tags_index_docs(usage_key)
233+
if isinstance(opaque_key, LibraryCollectionLocator):
234+
upsert_collection_tags_index_docs(opaque_key)
235235
else:
236-
upsert_content_object_tags_index_doc(usage_key)
236+
upsert_content_object_tags_index_doc(opaque_key)
237237
if not content_object.changes or "collections" in content_object.changes:
238-
upsert_block_collections_index_docs(usage_key)
238+
upsert_item_collections_index_docs(opaque_key)
239239

240240

241241
@receiver(LIBRARY_CONTAINER_CREATED)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,19 @@ def update_library_components_collections(library_key_str: str, collection_key:
116116
api.update_library_components_collections(library_key, collection_key)
117117

118118

119+
@shared_task(base=LoggedTask, autoretry_for=(MeilisearchError, ConnectionError))
120+
@set_code_owner_attribute
121+
def update_library_containers_collections(library_key_str: str, collection_key: str) -> None:
122+
"""
123+
Celery task to update the "collections" field for containers in the given content library collection.
124+
"""
125+
library_key = LibraryLocatorV2.from_string(library_key_str)
126+
127+
log.info("Updating document.collections for library %s collection %s containers", library_key, collection_key)
128+
129+
api.update_library_containers_collections(library_key, collection_key)
130+
131+
119132
@shared_task(base=LoggedTask, autoretry_for=(MeilisearchError, ConnectionError))
120133
@set_code_owner_attribute
121134
def update_library_container_index_doc(library_key_str: str, container_key_str: str) -> None:

0 commit comments

Comments
 (0)