Skip to content

Commit 47b04b4

Browse files
feat: update content libraries API to use upstream entity published events
1 parent 955f772 commit 47b04b4

7 files changed

Lines changed: 157 additions & 99 deletions

File tree

cms/djangoapps/modulestore_migrator/tasks.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -895,13 +895,9 @@ def _migrate_container(
895895
).publishable_entity_version
896896

897897
# Publish the container
898-
# Call post publish events synchronously to avoid
899-
# an error when calling `wait_for_post_publish_events`
900-
# inside a celery task.
901898
libraries_api.publish_container_changes(
902899
container.container_key,
903900
context.created_by,
904-
call_post_publish_events_sync=True,
905901
)
906902
context.used_container_slugs.add(container.container_key.container_id)
907903
return container_publishable_entity_version, None

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
)
3838
from openedx.core.types import User as UserType
3939

40-
from .. import tasks
4140
from ..models import ContentLibrary
4241
from .block_metadata import LibraryXBlockMetadata, LibraryXBlockStaticFile
4342
from .container_metadata import container_subclass_for_olx_tag
@@ -818,13 +817,7 @@ def publish_component_changes(usage_key: LibraryUsageLocatorV2, user_id: int):
818817
# The core publishing API is based on draft objects, so find the draft that corresponds to this component:
819818
drafts_to_publish = content_api.get_all_drafts(learning_package.id).filter(entity__entity_ref=component.entity_ref)
820819
# Publish the component and update anything that needs to be updated (e.g. search index):
821-
publish_log = content_api.publish_from_drafts(
822-
learning_package.id, draft_qset=drafts_to_publish, published_by=user_id,
823-
)
824-
# Since this is a single component, it should be safe to process synchronously and in-process:
825-
tasks.send_events_after_publish(publish_log.pk, str(library_key))
826-
# IF this is found to be a performance issue, we could instead make it async where necessary:
827-
# tasks.wait_for_post_publish_events(publish_log, library_key=library_key)
820+
content_api.publish_from_drafts(learning_package.id, draft_qset=drafts_to_publish, published_by=user_id)
828821

829822

830823
def _component_exists(usage_key: UsageKeyV2) -> bool:

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from openedx_events.content_authoring.data import LibraryContainerData
1818
from openedx_events.content_authoring.signals import LIBRARY_CONTAINER_DELETED
1919

20-
from .. import tasks
2120
from ..models import ContentLibrary
2221
from .block_metadata import LibraryXBlockMetadata
2322
from .container_metadata import (
@@ -249,7 +248,6 @@ def get_containers_contains_item(key: LibraryUsageLocatorV2 | LibraryContainerLo
249248
def publish_container_changes(
250249
container_key: LibraryContainerLocator,
251250
user_id: int | None,
252-
call_post_publish_events_sync=False,
253251
) -> None:
254252
"""
255253
[ 🛑 UNSTABLE ] Publish all unpublished changes in a container and all its child
@@ -263,17 +261,7 @@ def publish_container_changes(
263261
# The core publishing API is based on draft objects, so find the draft that corresponds to this container:
264262
drafts_to_publish = content_api.get_all_drafts(learning_package.id).filter(entity__pk=container.id)
265263
# Publish the container, which will also auto-publish any unpublished child components:
266-
publish_log = content_api.publish_from_drafts(
267-
learning_package.id,
268-
draft_qset=drafts_to_publish,
269-
published_by=user_id,
270-
)
271-
# Update the search index (and anything else) for the affected container + blocks
272-
# This is mostly synchronous but may complete some work asynchronously if there are a lot of changes.
273-
if call_post_publish_events_sync:
274-
tasks.send_events_after_publish(publish_log.pk, str(library_key))
275-
else:
276-
tasks.wait_for_post_publish_events(publish_log, library_key)
264+
content_api.publish_from_drafts(learning_package.id, draft_qset=drafts_to_publish, published_by=user_id)
277265

278266

279267
def copy_container(container_key: LibraryContainerLocator, user_id: int) -> UserClipboardData:

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272

7373
from openedx.core.types import User as UserType
7474

75-
from .. import permissions, tasks
75+
from .. import permissions
7676
from ..constants import ALL_RIGHTS_RESERVED
7777
from ..models import ContentLibrary, ContentLibraryPermission
7878
from .exceptions import LibraryAlreadyExists, LibraryPermissionIntegrityError
@@ -758,15 +758,7 @@ def publish_changes(library_key: LibraryLocatorV2, user_id: int | None = None):
758758
"""
759759
learning_package = ContentLibrary.objects.get_by_key(library_key).learning_package
760760
assert learning_package is not None # shouldn't happen but it's technically possible.
761-
publish_log = content_api.publish_all_drafts(learning_package.id, published_by=user_id)
762-
763-
# Update the search index (and anything else) for the affected blocks
764-
# This is mostly synchronous but may complete some work asynchronously if there are a lot of changes.
765-
tasks.wait_for_post_publish_events(publish_log, library_key)
766-
767-
# Unlike revert_changes below, we do not have to re-index collections,
768-
# because publishing changes does not affect the component counts, and
769-
# collections themselves don't have draft/published/unpublished status.
761+
content_api.publish_all_drafts(learning_package.id, published_by=user_id)
770762

771763

772764
def revert_changes(library_key: LibraryLocatorV2, user_id: int | None = None) -> None:

openedx/core/djangoapps/content_libraries/signal_handlers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,39 @@ def entities_updated(
5656
fn(learning_package_id=learning_package.id, change_list=[asdict(chg) for chg in change_log.changes])
5757

5858

59+
@receiver(content_signals.LEARNING_PACKAGE_ENTITIES_PUBLISHED)
60+
def entities_published(
61+
learning_package: content_signals.LearningPackageEventData,
62+
change_log: content_signals.PublishLogEventData,
63+
**kwargs,
64+
) -> None:
65+
"""
66+
Entities (containers/components) have been published - handle that as needed.
67+
68+
We receive this low-level event from `openedx_content`, and check if it
69+
happened in a library. If so, we emit more detailed library-specific events.
70+
71+
💾 This event is only received after the transaction has committed.
72+
⏳ This event is emitted synchronously and this handler is called
73+
synchronously. If a lot of entities were published, we need to dispatch
74+
an asynchronous handler to deal with them to avoid slowdowns. If only one
75+
entity was published, we want to deal with that synchronously so that we
76+
can show the user correct data when the current requests completes.
77+
"""
78+
try:
79+
library = ContentLibrary.objects.get(learning_package_id=learning_package.id)
80+
except ContentLibrary.DoesNotExist:
81+
return # We don't care about non-library events.
82+
83+
if len(change_log.changes) == 1:
84+
fn = tasks.send_events_after_publish
85+
else:
86+
# More than one entity was published at once. Handle asynchronously:
87+
fn = tasks.send_events_after_publish.delay
88+
89+
fn(publish_log_id=change_log.publish_log_id, library_key_str=str(library.library_key))
90+
91+
5992
@receiver(content_signals.LEARNING_PACKAGE_COLLECTION_CHANGED)
6093
def collection_updated(
6194
learning_package: content_signals.LearningPackageEventData,

openedx/core/djangoapps/content_libraries/tasks.py

Lines changed: 14 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import logging
2222
import os
2323
import shutil
24+
from collections.abc import Iterable
2425
from datetime import datetime
2526
from io import StringIO
2627
from tempfile import NamedTemporaryFile, mkdtemp
@@ -204,14 +205,16 @@ def check_container_content_changes(
204205
else:
205206
old_entity_list_id = None
206207
if new_version_id:
207-
new_version = content_api.get_container_version(new_version_id) if new_version_id else None
208+
new_version = content_api.get_container_version(new_version_id)
208209
new_entity_list_id = new_version.entity_list_id
209210
else:
210211
new_entity_list_id = None
211212

213+
old_child_ids: Iterable[PublishableEntity.ID]
214+
new_child_ids: Iterable[PublishableEntity.ID]
212215
# If the title has changed, we notify ALL children that their parent container(s) have changed, e.g. to update the
213216
# list of "units this component is used in", "sections this subsection is used in", etc. in the search index
214-
title_changed: bool = old_version and new_version and old_version.title != new_version.title
217+
title_changed: bool = bool(old_version and new_version) and (old_version.title != new_version.title)
215218
if title_changed:
216219
# TODO: there is no "get entity list for container version" API in openedx_content
217220
new_child_ids = new_version.entity_list.entitylistrow_set.values_list("entity_id", flat=True)
@@ -297,7 +300,7 @@ def send_collections_changed_events(
297300

298301
@shared_task(base=LoggedTask)
299302
@set_code_owner_attribute
300-
def send_events_after_publish(publish_log_pk: int, library_key_str: str) -> None:
303+
def send_events_after_publish(publish_log_id: int, library_key_str: str) -> None:
301304
"""
302305
Send events to trigger actions like updating the search index, after we've
303306
published some items in a library.
@@ -311,12 +314,11 @@ def send_events_after_publish(publish_log_pk: int, library_key_str: str) -> None
311314
event handlers like updating the search index may a while to complete in
312315
that case.
313316
"""
314-
publish_log = PublishLog.objects.get(pk=publish_log_pk)
317+
publish_log = PublishLog.objects.get(id=publish_log_id)
315318
library_key = LibraryLocatorV2.from_string(library_key_str)
316319
affected_entities = publish_log.records.select_related(
317320
"entity", "entity__container", "entity__container__container_type", "entity__component",
318321
).all()
319-
affected_containers: set[LibraryContainerLocator] = set()
320322

321323
# Update anything that needs to be updated (e.g. search index):
322324
for record in affected_entities:
@@ -330,61 +332,21 @@ def send_events_after_publish(publish_log_pk: int, library_key_str: str) -> None
330332
LIBRARY_BLOCK_PUBLISHED.send_event(
331333
library_block=LibraryBlockData(library_key=library_key, usage_key=usage_key)
332334
)
333-
# Publishing a container will auto-publish its children, but publishing a single component or all changes
334-
# in the library will NOT usually include any parent containers. But we do need to notify listeners that the
335-
# parent container(s) have changed, e.g. so the search index can update the "has_unpublished_changes"
336-
try:
337-
for parent_container in api.get_containers_contains_item(usage_key):
338-
affected_containers.add(parent_container.container_key)
339-
# TODO: should this be a CONTAINER_CHILD_PUBLISHED event instead of CONTAINER_PUBLISHED ?
340-
except api.ContentLibraryBlockNotFound:
341-
# The component has been deleted.
342-
pass
343335
elif hasattr(record.entity, "container"):
344336
container_key = api.library_container_locator(library_key, record.entity.container)
345-
affected_containers.add(container_key)
346-
347-
try:
348-
# We do need to notify listeners that the parent container(s) have changed,
349-
# e.g. so the search index can update the "has_unpublished_changes"
350-
for parent_container in api.get_containers_contains_item(container_key):
351-
affected_containers.add(parent_container.container_key)
352-
except api.ContentLibraryContainerNotFound:
353-
# The deleted children remains in the entity, so, in this case, the container may not be found.
354-
pass
337+
# Note: this container may have been directly published, or perhaps one of its children was published and
338+
# it hasn't technically changed. Such ancestors of published entities are still included in the publish log.
339+
# .. event_implemented_name: LIBRARY_CONTAINER_PUBLISHED
340+
# .. event_type: org.openedx.content_authoring.content_library.container.published.v1
341+
LIBRARY_CONTAINER_PUBLISHED.send_event(
342+
library_container=LibraryContainerData(container_key=container_key)
343+
)
355344
else:
356345
log.warning(
357346
f"PublishableEntity {record.entity.pk} / {record.entity.entity_ref} "
358347
"was modified during publish operation but is of unknown type."
359348
)
360349

361-
for container_key in affected_containers:
362-
# .. event_implemented_name: LIBRARY_CONTAINER_PUBLISHED
363-
# .. event_type: org.openedx.content_authoring.content_library.container.published.v1
364-
LIBRARY_CONTAINER_PUBLISHED.send_event(
365-
library_container=LibraryContainerData(container_key=container_key)
366-
)
367-
368-
369-
def wait_for_post_publish_events(publish_log: PublishLog, library_key: LibraryLocatorV2):
370-
"""
371-
After publishing some changes, trigger the required event handlers (e.g.
372-
update the search index). Try to wait for that to complete before returning,
373-
up to some reasonable timeout, and then finish anything remaining
374-
asynchonrously.
375-
"""
376-
# Update the search index (and anything else) for the affected blocks
377-
result = send_events_after_publish.apply_async(args=(publish_log.pk, str(library_key)))
378-
# Try waiting a bit for those post-publish events to be handled:
379-
try:
380-
result.get(timeout=15)
381-
except TimeoutError:
382-
pass
383-
# This is fine! The search index is still being updated, and/or other
384-
# event handlers are still following up on the results, but the publish
385-
# already *did* succeed, and the events will continue to be processed in
386-
# the background by the celery worker until everything is updated.
387-
388350

389351
def _filter_child(store, usage_key, capa_type):
390352
"""

0 commit comments

Comments
 (0)