|
4 | 4 |
|
5 | 5 | import logging |
6 | 6 |
|
7 | | -from django.db.models.signals import m2m_changed, post_delete, post_save |
8 | 7 | from django.dispatch import receiver |
9 | | -from opaque_keys import OpaqueKey |
10 | | -from opaque_keys.edx.locator import LibraryLocatorV2 |
11 | | -from openedx_content.api import get_components, get_containers |
12 | | -from openedx_content.models_api import Collection, CollectionPublishableEntity, PublishableEntity |
13 | | -from openedx_events.content_authoring.data import ContentObjectChangedData, LibraryCollectionData |
| 8 | +from openedx_content.api import signals as content_signals |
| 9 | +from openedx_events.content_authoring.data import LibraryCollectionData |
14 | 10 | from openedx_events.content_authoring.signals import ( |
15 | | - CONTENT_OBJECT_ASSOCIATIONS_CHANGED, |
16 | 11 | LIBRARY_COLLECTION_CREATED, |
17 | 12 | LIBRARY_COLLECTION_DELETED, |
18 | 13 | LIBRARY_COLLECTION_UPDATED, |
19 | 14 | ) |
20 | 15 |
|
21 | | -from .api import library_collection_locator, library_component_usage_key, library_container_locator |
| 16 | +from . import tasks |
| 17 | +from .api import library_collection_locator |
22 | 18 | from .models import ContentLibrary |
23 | 19 |
|
24 | 20 | log = logging.getLogger(__name__) |
25 | 21 |
|
26 | 22 |
|
27 | | -@receiver(post_save, sender=Collection, dispatch_uid="library_collection_saved") |
28 | | -def library_collection_saved(sender, instance, created, **kwargs): |
| 23 | +@receiver(content_signals.LEARNING_PACKAGE_COLLECTION_CHANGED) |
| 24 | +def collection_updated( |
| 25 | + learning_package: content_signals.LearningPackageEventData, |
| 26 | + change: content_signals.CollectionChangeData, |
| 27 | + **kwargs, |
| 28 | +): |
29 | 29 | """ |
30 | | - Raises LIBRARY_COLLECTION_CREATED if the Collection is new, |
31 | | - or LIBRARY_COLLECTION_UPDATED if updated an existing Collection. |
| 30 | + A Collection has been updated - handle that as needed. |
| 31 | +
|
| 32 | + We receive this low-level event from `openedx_content`, and check if it |
| 33 | + happened in a library. If so, we emit more detailed library-specific events. |
| 34 | +
|
| 35 | + ⏳ This event is emitted synchronously and this handler is called |
| 36 | + synchronously. If a lot of entities were changed, we need to dispatch an |
| 37 | + asynchronous handler to deal with them to avoid slowdowns. |
32 | 38 | """ |
33 | 39 | try: |
34 | | - library = ContentLibrary.objects.get(learning_package_id=instance.learning_package_id) |
| 40 | + library = ContentLibrary.objects.get(learning_package_id=learning_package.id) |
35 | 41 | except ContentLibrary.DoesNotExist: |
36 | | - log.error("{instance} is not associated with a content library.") |
37 | | - return |
| 42 | + return # We don't care about non-library events. |
| 43 | + |
| 44 | + collection_key = library_collection_locator(library_key=library.library_key, collection_key=change.collection_code) |
| 45 | + entities_changed = change.entities_added + change.entities_removed |
38 | 46 |
|
39 | | - if created: |
| 47 | + if change.created: # This is a newly-created collection, or was "un-deleted": |
40 | 48 | # .. event_implemented_name: LIBRARY_COLLECTION_CREATED |
41 | 49 | # .. event_type: org.openedx.content_authoring.content_library.collection.created.v1 |
42 | | - LIBRARY_COLLECTION_CREATED.send_event( |
43 | | - library_collection=LibraryCollectionData( |
44 | | - collection_key=library_collection_locator( |
45 | | - library_key=library.library_key, |
46 | | - collection_key=instance.key, |
47 | | - ), |
48 | | - ) |
49 | | - ) |
50 | | - else: |
| 50 | + LIBRARY_COLLECTION_CREATED.send_event(library_collection=LibraryCollectionData(collection_key=collection_key)) |
| 51 | + # As an example of what this event triggers, Collections are listed in the Meilisearch index as items in the |
| 52 | + # library. So the handler will add this Collection as an entry in the Meilisearch index. |
| 53 | + elif change.metadata_modified or entities_changed: |
| 54 | + # The collection was renamed or its items were changed. |
| 55 | + # This event is ambiguous but because the search index of the colleciton itself may have something like |
| 56 | + # "contains 15 items", we _do_ need to emit it even when only the items have changed and not the metadata. |
51 | 57 | # .. event_implemented_name: LIBRARY_COLLECTION_UPDATED |
52 | 58 | # .. event_type: org.openedx.content_authoring.content_library.collection.updated.v1 |
53 | | - LIBRARY_COLLECTION_UPDATED.send_event( |
54 | | - library_collection=LibraryCollectionData( |
55 | | - collection_key=library_collection_locator( |
56 | | - library_key=library.library_key, |
57 | | - collection_key=instance.key, |
58 | | - ), |
59 | | - ) |
60 | | - ) |
61 | | - |
62 | | - |
63 | | -@receiver(post_delete, sender=Collection, dispatch_uid="library_collection_deleted") |
64 | | -def library_collection_deleted(sender, instance, **kwargs): |
65 | | - """ |
66 | | - Raises LIBRARY_COLLECTION_DELETED for the deleted Collection. |
67 | | - """ |
68 | | - try: |
69 | | - library = ContentLibrary.objects.get(learning_package_id=instance.learning_package_id) |
70 | | - except ContentLibrary.DoesNotExist: |
71 | | - log.error("{instance} is not associated with a content library.") |
72 | | - return |
73 | | - |
74 | | - # .. event_implemented_name: LIBRARY_COLLECTION_DELETED |
75 | | - # .. event_type: org.openedx.content_authoring.content_library.collection.deleted.v1 |
76 | | - LIBRARY_COLLECTION_DELETED.send_event( |
77 | | - library_collection=LibraryCollectionData( |
78 | | - collection_key=library_collection_locator( |
79 | | - library_key=library.library_key, |
80 | | - collection_key=instance.key, |
81 | | - ), |
82 | | - ) |
83 | | - ) |
84 | | - |
85 | | - |
86 | | -def _library_collection_entity_changed( |
87 | | - publishable_entity: PublishableEntity, |
88 | | - library_key: LibraryLocatorV2 | None = None, |
89 | | -) -> None: |
90 | | - """ |
91 | | - Sends a CONTENT_OBJECT_ASSOCIATIONS_CHANGED event for the entity. |
92 | | - """ |
93 | | - if not library_key: |
94 | | - try: |
95 | | - library = ContentLibrary.objects.get( |
96 | | - learning_package_id=publishable_entity.learning_package_id, |
97 | | - ) |
98 | | - except ContentLibrary.DoesNotExist: |
99 | | - log.error("{publishable_entity} is not associated with a content library.") |
100 | | - return |
101 | | - |
102 | | - library_key = library.library_key |
103 | | - |
104 | | - assert library_key |
105 | | - |
106 | | - opaque_key: OpaqueKey |
107 | | - |
108 | | - if hasattr(publishable_entity, 'component'): |
109 | | - opaque_key = library_component_usage_key( |
110 | | - library_key, |
111 | | - publishable_entity.component, |
| 59 | + LIBRARY_COLLECTION_UPDATED.send_event(library_collection=LibraryCollectionData(collection_key=collection_key)) |
| 60 | + elif change.deleted: |
| 61 | + # .. event_implemented_name: LIBRARY_COLLECTION_DELETED |
| 62 | + # .. event_type: org.openedx.content_authoring.content_library.collection.deleted.v1 |
| 63 | + LIBRARY_COLLECTION_DELETED.send_event(library_collection=LibraryCollectionData(collection_key=collection_key)) |
| 64 | + |
| 65 | + # Now, what about the actual entities (containers/components) in the collection? |
| 66 | + if entities_changed: |
| 67 | + if len(entities_changed) == 1: |
| 68 | + # If there's only one changed entity, emit the event synchronously: |
| 69 | + fn = tasks.send_collections_changed_events |
| 70 | + else: |
| 71 | + # If there are more than one changed entities, emit the events asynchronously: |
| 72 | + fn = tasks.send_collections_changed_events.delay |
| 73 | + fn( |
| 74 | + publishable_entity_ids=entities_changed, |
| 75 | + learning_package_id=learning_package.id, |
| 76 | + library_key_str=str(library.library_key), |
112 | 77 | ) |
113 | | - elif hasattr(publishable_entity, 'container'): |
114 | | - opaque_key = library_container_locator( |
115 | | - library_key, |
116 | | - publishable_entity.container, |
117 | | - ) |
118 | | - else: |
119 | | - log.error("Unknown publishable entity type: %s", publishable_entity) |
120 | | - return |
121 | | - |
122 | | - # .. event_implemented_name: CONTENT_OBJECT_ASSOCIATIONS_CHANGED |
123 | | - # .. event_type: org.openedx.content_authoring.content.object.associations.changed.v1 |
124 | | - CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( |
125 | | - content_object=ContentObjectChangedData( |
126 | | - object_id=str(opaque_key), |
127 | | - changes=["collections"], |
128 | | - ), |
129 | | - ) |
130 | | - |
131 | | - |
132 | | -@receiver(post_save, sender=CollectionPublishableEntity, dispatch_uid="library_collection_entity_saved") |
133 | | -def library_collection_entity_saved(sender, instance, created, **kwargs): |
134 | | - """ |
135 | | - Sends a CONTENT_OBJECT_ASSOCIATIONS_CHANGED event for components added to a collection. |
136 | | - """ |
137 | | - if created: |
138 | | - _library_collection_entity_changed(instance.entity) |
139 | | - |
140 | | - |
141 | | -@receiver(post_delete, sender=CollectionPublishableEntity, dispatch_uid="library_collection_entity_deleted") |
142 | | -def library_collection_entity_deleted(sender, instance, **kwargs): |
143 | | - """ |
144 | | - Sends a CONTENT_OBJECT_ASSOCIATIONS_CHANGED event for components removed from a collection. |
145 | | - """ |
146 | | - # Only trigger component updates if CollectionPublishableEntity was cascade deleted due to deletion of a collection. |
147 | | - if isinstance(kwargs.get('origin'), Collection): |
148 | | - _library_collection_entity_changed(instance.entity) |
149 | | - |
150 | | - |
151 | | -@receiver(m2m_changed, sender=CollectionPublishableEntity, dispatch_uid="library_collection_entities_changed") |
152 | | -def library_collection_entities_changed(sender, instance, action, pk_set, **kwargs): |
153 | | - """ |
154 | | - Sends a CONTENT_OBJECT_ASSOCIATIONS_CHANGED event for components added/removed/cleared from a collection. |
155 | | - """ |
156 | | - if action not in ["post_add", "post_remove", "post_clear"]: |
157 | | - return |
158 | | - |
159 | | - try: |
160 | | - library = ContentLibrary.objects.get( |
161 | | - learning_package_id=instance.learning_package_id, |
162 | | - ) |
163 | | - except ContentLibrary.DoesNotExist: |
164 | | - log.error("{instance} is not associated with a content library.") |
165 | | - return |
166 | | - |
167 | | - if isinstance(instance, PublishableEntity): |
168 | | - _library_collection_entity_changed(instance, library.library_key) |
169 | | - return |
170 | | - |
171 | | - # When action=="post_clear", pk_set==None |
172 | | - # Since the collection instance now has an empty entities set, |
173 | | - # we don't know which ones were removed, so we need to update associations for all library |
174 | | - # components and containers. |
175 | | - components = get_components(instance.learning_package_id) |
176 | | - containers = get_containers(instance.learning_package_id) |
177 | | - if pk_set: |
178 | | - components = components.filter(pk__in=pk_set) |
179 | | - containers = containers.filter(pk__in=pk_set) |
180 | | - |
181 | | - for entity in list(components.all()) + list(containers.all()): |
182 | | - _library_collection_entity_changed(entity.publishable_entity, library.library_key) |
0 commit comments