-
Notifications
You must be signed in to change notification settings - Fork 25
Delete tag updates meilisearch #571
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2018c05
400b1c4
76cde64
6a27b8c
e9978b7
6fbc422
880d1e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,4 +6,4 @@ | |
| """ | ||
|
|
||
| # The version for the entire repository | ||
| __version__ = "0.47.0" | ||
| __version__ = "0.48.0" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, cast | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import ddt # type: ignore[import] | ||
|
|
@@ -17,7 +18,11 @@ | |
| from openedx_tagging import api | ||
| from openedx_tagging.models import LanguageTaxonomy, ObjectTag, Tag, Taxonomy | ||
| from openedx_tagging.models.utils import RESERVED_TAG_CHARS | ||
| from openedx_tagging.tasks import emit_content_object_associations_changed_for_tag_task | ||
| from openedx_tagging.signal_handlers import _is_explicit_tag_delete | ||
| from openedx_tagging.tasks import ( | ||
| emit_content_object_associations_changed_for_object_ids_task, | ||
| emit_content_object_associations_changed_for_tag_task, | ||
| ) | ||
|
|
||
| from .utils import pretty_format_tags | ||
|
|
||
|
|
@@ -663,11 +668,21 @@ def test_object_tag_export_id(self): | |
| self.object_tag.save() | ||
| assert self.object_tag.export_id == self.taxonomy.export_id | ||
|
|
||
| # But if the taxonomy is deleted, then the object_tag's export_id reverts to our cached export_id | ||
| self.taxonomy.delete() | ||
| # But if the taxonomy is deleted, then the object_tag's export_id reverts to our cached export_id. | ||
| # Patch explicit-delete detection because this test is about ObjectTag fallback behavior, | ||
| # not tag deletion event-origins. | ||
| with patch("openedx_tagging.signal_handlers._is_explicit_tag_delete", return_value=False): | ||
| self.taxonomy.delete() | ||
| self.object_tag.refresh_from_db() | ||
| assert self.object_tag.export_id == "another-taxonomy" | ||
|
|
||
| def test_is_explicit_tag_delete_raises_for_unexpected_origin_type(self): | ||
| with pytest.raises( | ||
| TypeError, | ||
| match=r"Expected origin to be Tag, QuerySet\[Tag\], or None; got Taxonomy", | ||
| ): | ||
| _is_explicit_tag_delete(instance=self.tag, origin=cast(Any, self.taxonomy), using="default") | ||
|
|
||
| def test_object_tag_value(self): | ||
| # ObjectTag's value defaults to its tag's value | ||
| object_tag = ObjectTag.objects.create( | ||
|
|
@@ -824,8 +839,11 @@ def test_is_deleted(self): | |
| (self.bacteria.value, True), # <--- deleted! But the value is preserved. | ||
| ] | ||
|
|
||
| # Then delete the whole free text taxonomy | ||
| self.free_text_taxonomy.delete() | ||
| # Then delete the whole free text taxonomy. | ||
| # Patch explicit-delete detection because this | ||
| # test validates ObjectTag deleted-state behavior, not tag deletion event-origins. | ||
| with patch("openedx_tagging.signal_handlers._is_explicit_tag_delete", return_value=False): | ||
| self.free_text_taxonomy.delete() | ||
|
|
||
| assert [(t.value, t.is_deleted) for t in api.get_object_tags(object_id, include_deleted=True)] == [ | ||
| ("bar", True), # <--- Deleted, but the value is preserved | ||
|
|
@@ -1095,16 +1113,18 @@ def test_rename(self): | |
| assert self.bob.depth == 1 | ||
| assert self.bob.lineage == "Charlie\tBob\t" | ||
|
|
||
| # TODO: The following event-emission tests don't really belong in TestTagLineage. | ||
| # They should be moved to a separate test_events.py module. | ||
| @patch("openedx_tagging.signal_handlers.emit_content_object_associations_changed_for_tag_task.delay") | ||
| def test_rename_updates_search_index(self, mock_task_delay) -> None: | ||
| """ | ||
| Renaming a tag should enqueue an async task that emits | ||
| CONTENT_OBJECT_ASSOCIATIONS_CHANGED events. | ||
| """ | ||
| ObjectTag.objects.create( | ||
| api.tag_object( | ||
| object_id="content-v1:org+course+run+type@unit+block@123", | ||
| taxonomy=self.alice.taxonomy, | ||
| tag=self.alice, | ||
| tags=[self.alice.value], | ||
| ) | ||
|
|
||
| with self.captureOnCommitCallbacks(execute=True): | ||
|
|
@@ -1114,20 +1134,89 @@ def test_rename_updates_search_index(self, mock_task_delay) -> None: | |
| assert mock_task_delay.call_count == 1 | ||
| assert mock_task_delay.call_args[1]['tag_id'] == self.alice.id | ||
|
|
||
| @patch("openedx_tagging.signal_handlers.emit_content_object_associations_changed_for_object_ids_task.delay") | ||
| def test_delete_updates_search_index(self, mock_task_delay) -> None: | ||
| """ | ||
| Deleting a tag should enqueue an async task that emits | ||
| CONTENT_OBJECT_ASSOCIATIONS_CHANGED events for affected objects. | ||
|
|
||
| Note: this tests deleting a ``Tag`` (not an ``ObjectTag``). Deleting a | ||
| ``Tag`` triggers the event here in openedx-learning. Deleting an | ||
| ``ObjectTag`` (i.e. untagging a content object) triggers the same event | ||
| in openedx-platform instead, so that case is not tested here. | ||
| """ | ||
| object_id = "content-v1:org+course+run+type@unit+block@125" | ||
| api.tag_object( | ||
| object_id=object_id, | ||
| taxonomy=self.bob.taxonomy, | ||
| tags=[self.bob.value], | ||
| ) | ||
|
|
||
| with self.captureOnCommitCallbacks(execute=True): | ||
| self.bob.delete() | ||
|
|
||
| assert mock_task_delay.call_count == 1 | ||
| assert mock_task_delay.call_args[1]["object_ids"] == [object_id] | ||
|
bradenmacdonald marked this conversation as resolved.
|
||
|
|
||
| @patch("openedx_tagging.signal_handlers.emit_content_object_associations_changed_for_object_ids_task.delay") | ||
| def test_delete_with_descendants_updates_search_index(self, mock_task_delay) -> None: | ||
| """ | ||
| Deleting a tag should also enqueue updates for any deleted descendants. | ||
| """ | ||
| alice_object_id = "content-v1:org+course+run+type@unit+block@126" | ||
| delta_object_id = "content-v1:org+course+run+type@unit+block@127" | ||
| api.tag_object( | ||
| object_id=alice_object_id, | ||
| taxonomy=self.alice.taxonomy, | ||
| tags=[self.alice.value], | ||
| ) | ||
| api.tag_object( | ||
| object_id=delta_object_id, | ||
| taxonomy=self.delta.taxonomy, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is confusing when reading this test in isolation, because the tags I won't block the PR on this but I'd strongly prefer to see it at least flagged as a TODO if not moved before merge. |
||
| tags=[self.delta.value], | ||
| ) | ||
|
|
||
| with self.captureOnCommitCallbacks(execute=True): | ||
| api.delete_tags_from_taxonomy(self.alice.taxonomy, ["Alice"], with_subtags=True) | ||
|
|
||
| assert mock_task_delay.call_count == 1 | ||
| assert set(mock_task_delay.call_args.kwargs["object_ids"]) == { | ||
| alice_object_id, | ||
| delta_object_id, | ||
| } | ||
|
|
||
| @patch("openedx_tagging.tasks.CONTENT_OBJECT_ASSOCIATIONS_CHANGED", new_callable=MagicMock) | ||
| def test_emit_content_object_associations_changed_for_object_ids_task(self, mock_signal) -> None: | ||
| """Task emits one CONTENT_OBJECT_ASSOCIATIONS_CHANGED event per distinct object.""" | ||
| first_object_id = "content-v1:org+course+run+type@unit+block@123" | ||
| second_object_id = "content-v1:org+course+run+type@unit+block@124" | ||
|
|
||
| emitted_events = emit_content_object_associations_changed_for_object_ids_task( | ||
| [first_object_id, second_object_id, first_object_id] | ||
| ) | ||
|
|
||
| assert emitted_events == 2 | ||
| assert mock_signal.send_event.call_count == 2 | ||
| emitted_object_ids = { | ||
| call.kwargs["content_object"].object_id | ||
| for call in mock_signal.send_event.call_args_list | ||
| } | ||
| assert emitted_object_ids == {first_object_id, second_object_id} | ||
|
|
||
| @patch("openedx_tagging.tasks.CONTENT_OBJECT_ASSOCIATIONS_CHANGED", new_callable=MagicMock) | ||
| def test_emit_content_object_associations_changed_for_tag_task(self, mock_signal) -> None: | ||
| """Task emits one CONTENT_OBJECT_ASSOCIATIONS_CHANGED event per associated object.""" | ||
| first_object_id = "content-v1:org+course+run+type@unit+block@123" | ||
| second_object_id = "content-v1:org+course+run+type@unit+block@124" | ||
| ObjectTag.objects.create( | ||
| api.tag_object( | ||
| object_id=first_object_id, | ||
| taxonomy=self.alice.taxonomy, | ||
| tag=self.alice, | ||
| tags=[self.alice.value], | ||
| ) | ||
| ObjectTag.objects.create( | ||
| api.tag_object( | ||
| object_id=second_object_id, | ||
| taxonomy=self.alice.taxonomy, | ||
| tag=self.alice, | ||
| tags=[self.alice.value], | ||
| ) | ||
|
|
||
| emitted_events = emit_content_object_associations_changed_for_tag_task(self.alice.id) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious, is there any reason this approach (using
_is_explicit_tag_delete, manually emitting events for the descendants, and then suppressing events for the CASCADE) is preferable to just listening for each individualTagdeletion and emitting an event for each one, including the CASCADE deletes (but never manually emitting events for the descendants)?The latter seems like it would have much simpler code since you wouldn't need this
_is_explicit_tag_deletelogic. But perhaps it's not as performant?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's to prevent duplicate events per object so that it emits one event per object regardless of how many tags in the subtree are applied to it. This does improve performance with N cascade deletes --> N tasks --> N sets of DB queries in the simple approach, versus 1 task with 1 query in the current approach. For large taxonomies this could be significant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense, and would be worth mentioning in the code.
Even with that optimization, the number of events emitted could be huge, but since they're in a celery task it should be OK.