Skip to content

Commit cab0f73

Browse files
fix: keep collection "# of entities" count updated when entities deleted
1 parent d23244a commit cab0f73

3 files changed

Lines changed: 81 additions & 1 deletion

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,9 @@ def searchable_doc_for_collection(
558558
if collection:
559559
assert collection.collection_code == collection_key.collection_id
560560

561+
# Collections themselves are not publishable entities, so don't have a "draft" or "published" version, but the
562+
# entities they contain are publishable, so the number of entities in the collection may be different between
563+
# draft and published views, if some draft entities are unpublished or are published but the draft is deleted.
561564
draft_num_children = content_api.filter_publishable_entities(
562565
collection.entities,
563566
has_draft=True,

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
from opaque_keys import InvalidKeyError
1010
from opaque_keys.edx.keys import UsageKey
1111
from opaque_keys.edx.locator import LibraryCollectionLocator, LibraryContainerLocator
12+
from openedx_content import api as content_api
13+
from openedx_content.api import signals as content_signals
14+
from openedx_content.models_api import LearningPackage, PublishableEntity
1215
from openedx_events.content_authoring.data import (
1316
ContentLibraryData,
1417
ContentObjectChangedData,
@@ -388,10 +391,74 @@ def library_container_deleted(**kwargs) -> None:
388391
# See https://github.com/openedx/edx-platform/pull/36640 discussion.
389392

390393

394+
@receiver(content_signals.ENTITIES_DRAFT_CHANGED)
395+
def entities_updated(
396+
learning_package: content_signals.LearningPackageEventData,
397+
change_log: content_signals.DraftChangeLogEventData,
398+
**kwargs,
399+
) -> None:
400+
"""
401+
When entities are deleted or un-deleted (as drafts), update any associated
402+
collections, so their "# of draft entities in collection" count is correct.
403+
404+
💾 This event is only received after the transaction has committed.
405+
⏳ This event is emitted synchronously and this handler is called
406+
synchronously, so we want to be as efficient as possible.
407+
"""
408+
deleted_or_undeleted_entity_ids = [
409+
r.entity_id for r in change_log.changes if r.new_version is None or (r.old_version is None and r.restored)
410+
]
411+
# Note: we only care about deleted or un-deleted, not newly created drafts, because it's currently impossible for a
412+
# newly-created draft to be part of a collection.
413+
if not deleted_or_undeleted_entity_ids:
414+
return # No need to do anything more; if nothing was deleted or un-deleted, it won't affect collection counts.
415+
notify_affected_collections(learning_package.id, deleted_or_undeleted_entity_ids)
416+
417+
418+
@receiver(content_signals.ENTITIES_PUBLISHED)
419+
def entities_published(
420+
learning_package: content_signals.LearningPackageEventData,
421+
change_log: content_signals.PublishLogEventData,
422+
**kwargs,
423+
) -> None:
424+
"""
425+
When entities get newly published or their published version is deleted,
426+
update the "# of published entities in collection" count of any associated
427+
collections.
428+
"""
429+
newly_published_or_unpublished_entity_ids = [
430+
r.entity_id for r in change_log.changes if r.new_version is None or r.old_version is None
431+
]
432+
if not newly_published_or_unpublished_entity_ids:
433+
return # No need to do anything more; if nothing was deleted or un-deleted, it won't affect collection counts.
434+
notify_affected_collections(learning_package.id, newly_published_or_unpublished_entity_ids)
435+
436+
437+
def notify_affected_collections(learning_package_id: LearningPackage.ID, entity_ids: PublishableEntity.ID):
438+
"""Helper for updating collections' "# of entities" count when draft/published entities affect it"""
439+
# Check if any collections are affected:
440+
affected_collections = (
441+
content_api.get_collections(learning_package_id, enabled=True).filter(entities__id__in=entity_ids)
442+
)
443+
# If any collections were affected, update them asynchronously:
444+
if not affected_collections:
445+
return
446+
# Collections are only used in libraries at the moment. Get the library key so we can form opaque keys for each
447+
# collection too.
448+
try:
449+
library_key = lib_api.get_library_key(learning_package_id)
450+
except lib_api.ContentLibraryNotFound:
451+
return
452+
453+
for collection in affected_collections:
454+
collection_key = lib_api.library_collection_locator(library_key, collection.collection_code)
455+
update_library_collection_index_doc.delay(str(collection_key)) # Async - no need to wait for this ever.
456+
457+
391458
@receiver([COURSE_IMPORT_COMPLETED, COURSE_RERUN_COMPLETED])
392459
def handle_reindex_on_signal(**kwargs):
393460
"""
394-
Automatically update Meiliesearch index for course in database on new import or rerun.
461+
Automatically update Meilisearch index for course in database on new import or rerun.
395462
"""
396463
course_data = kwargs.get("course", None)
397464
if not course_data or not isinstance(course_data, CourseData):

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"get_metadata",
9696
"require_permission_for_library_key",
9797
"get_library",
98+
"get_library_key",
9899
"create_library",
99100
"get_library_team",
100101
"get_library_user_permissions",
@@ -415,6 +416,15 @@ def get_library(library_key: LibraryLocatorV2) -> ContentLibraryMetadata:
415416
)
416417

417418

419+
def get_library_key(learning_package_id: LearningPackage.ID) -> LibraryLocatorV2:
420+
"""
421+
Get the library key for the library with the specified learning package ID.
422+
423+
Raises ContentLibraryNotFound if the library doesn't exist.
424+
"""
425+
return ContentLibrary.objects.get(learning_package_id=learning_package_id).library_key
426+
427+
418428
def create_library(
419429
org: str,
420430
slug: str,

0 commit comments

Comments
 (0)