From 964b38a17ae9aa960de3af923edea5b7028b84a6 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Tue, 6 Aug 2024 19:38:42 +0530 Subject: [PATCH 1/6] fix: delete discarded drafts from meilisearch index --- openedx/core/djangoapps/content/search/api.py | 37 ++++++++++++++++++- .../core/djangoapps/content/search/tasks.py | 1 + .../core/djangoapps/content_libraries/api.py | 5 ++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index 9473dabbe427..e1ae589f8916 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -18,7 +18,7 @@ from meilisearch.errors import MeilisearchError from meilisearch.models.task import TaskInfo from opaque_keys.edx.keys import UsageKey -from opaque_keys.edx.locator import LibraryLocatorV2 +from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2 from common.djangoapps.student.roles import GlobalStaff from rest_framework.request import Request from common.djangoapps.student.role_helpers import get_course_roles @@ -467,6 +467,26 @@ def delete_index_doc(usage_key: UsageKey) -> None: _wait_for_meili_tasks(tasks) +def delete_index_docs(meili_ids: list[str]) -> None: + """ + Deletes documents for the given XBlocks from the search index + + Args: + meili_ids (list[str]): List of meili_ids from usage keys of the XBlocks to be removed from the index + """ + current_rebuild_index_name = _get_running_rebuild_index_name() + + client = _get_meilisearch_client() + + tasks = [] + if current_rebuild_index_name: + # If there is a rebuild in progress, the document will also be deleted from the new index. + tasks.append(client.index(current_rebuild_index_name).delete_documents(meili_ids)) + tasks.append(client.index(STUDIO_INDEX_NAME).delete_documents(meili_ids)) + + _wait_for_meili_tasks(tasks) + + def upsert_library_block_index_doc(usage_key: UsageKey) -> None: """ Creates or updates the document for the given Library Block in the search index @@ -495,6 +515,21 @@ def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None: _update_index_docs(docs) +def delete_content_library_index_docs(library_key: LibraryLocatorV2) -> None: + """ + Deletes the discarded draft documents for the given Content Library in the search index + """ + meili_ids = [] + for component in lib_api.get_library_components(library_key, draft=False, published=False): + usage_key = LibraryUsageLocatorV2( + library_key, + component.component_type.name, + component.local_key, + ) + meili_ids.append(meili_id_from_opaque_key(usage_key)) + delete_index_docs(meili_ids) + + def upsert_block_tags_index_docs(usage_key: UsageKey): """ Updates the tags data in documents for the given Course/Library block diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index 06ea3e777c61..2eacd02c7669 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -80,4 +80,5 @@ def update_content_library_index_docs(library_key_str: str) -> None: log.info("Updating content index documents for library with id: %s", library_key) + api.delete_content_library_index_docs(library_key) api.upsert_content_library_index_docs(library_key) diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index 17bea80b3a96..5a956b961394 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -642,7 +642,7 @@ def _get_library_component_tags_count(library_key) -> dict: return get_object_tag_counts(library_key_pattern, count_implicit=True) -def get_library_components(library_key, text_search=None, block_types=None) -> QuerySet[Component]: +def get_library_components(library_key, text_search=None, block_types=None, draft=True, published=None) -> QuerySet[Component]: """ Get the library components and filter. @@ -653,7 +653,8 @@ def get_library_components(library_key, text_search=None, block_types=None) -> Q learning_package = lib.learning_package components = authoring_api.get_components( learning_package.id, - draft=True, + draft=draft, + published=published, namespace='xblock.v1', type_names=block_types, draft_title=text_search, From 1455c5d9982c42be030896d2bd941a8ef3babdf8 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 7 Aug 2024 12:12:25 +0530 Subject: [PATCH 2/6] refactor: parallelize content block update on discard --- openedx/core/djangoapps/content/search/api.py | 25 +++++++++++-------- .../djangoapps/content/search/handlers.py | 3 ++- .../core/djangoapps/content/search/tasks.py | 7 +++--- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index e1ae589f8916..776de6738ea0 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -143,7 +143,7 @@ def _wait_for_meili_task(info: TaskInfo) -> None: raise MeilisearchError(err_reason) -def _wait_for_meili_tasks(info_list: list[TaskInfo]) -> None: +def wait_for_meili_tasks(info_list: list[TaskInfo]) -> None: """ Simple helper method to wait for multiple Meilisearch tasks to complete """ @@ -231,7 +231,7 @@ def _recurse_children(block, fn, status_cb: Callable[[str], None] | None = None) fn(child) -def _update_index_docs(docs) -> None: +def _update_index_docs(docs, wait=True) -> list[TaskInfo]: """ Helper function that updates the documents in the search index @@ -249,7 +249,9 @@ def _update_index_docs(docs) -> None: tasks.append(client.index(current_rebuild_index_name).update_documents(docs)) tasks.append(client.index(STUDIO_INDEX_NAME).update_documents(docs)) - _wait_for_meili_tasks(tasks) + if wait: + wait_for_meili_tasks(tasks) + return tasks def only_if_meilisearch_enabled(f): @@ -464,10 +466,10 @@ def delete_index_doc(usage_key: UsageKey) -> None: tasks.append(client.index(current_rebuild_index_name).delete_document(meili_id_from_opaque_key(usage_key))) tasks.append(client.index(STUDIO_INDEX_NAME).delete_document(meili_id_from_opaque_key(usage_key))) - _wait_for_meili_tasks(tasks) + wait_for_meili_tasks(tasks) -def delete_index_docs(meili_ids: list[str]) -> None: +def delete_index_docs(meili_ids: list[str], wait=True) -> list[TaskInfo]: """ Deletes documents for the given XBlocks from the search index @@ -484,7 +486,9 @@ def delete_index_docs(meili_ids: list[str]) -> None: tasks.append(client.index(current_rebuild_index_name).delete_documents(meili_ids)) tasks.append(client.index(STUDIO_INDEX_NAME).delete_documents(meili_ids)) - _wait_for_meili_tasks(tasks) + if wait: + wait_for_meili_tasks(tasks) + return tasks def upsert_library_block_index_doc(usage_key: UsageKey) -> None: @@ -502,7 +506,7 @@ def upsert_library_block_index_doc(usage_key: UsageKey) -> None: _update_index_docs(docs) -def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None: +def upsert_content_library_index_docs(library_key: LibraryLocatorV2, wait=True) -> None: """ Creates or updates the documents for the given Content Library in the search index """ @@ -512,14 +516,15 @@ def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None: doc = searchable_doc_for_library_block(metadata) docs.append(doc) - _update_index_docs(docs) + return _update_index_docs(docs, wait=wait) -def delete_content_library_index_docs(library_key: LibraryLocatorV2) -> None: +def delete_content_library_index_docs(library_key: LibraryLocatorV2, wait=True) -> None: """ Deletes the discarded draft documents for the given Content Library in the search index """ meili_ids = [] + # draft version and published version equal to False/null means those drafts were discarded. for component in lib_api.get_library_components(library_key, draft=False, published=False): usage_key = LibraryUsageLocatorV2( library_key, @@ -527,7 +532,7 @@ def delete_content_library_index_docs(library_key: LibraryLocatorV2) -> None: component.local_key, ) meili_ids.append(meili_id_from_opaque_key(usage_key)) - delete_index_docs(meili_ids) + return delete_index_docs(meili_ids, wait=wait) def upsert_block_tags_index_docs(usage_key: UsageKey): diff --git a/openedx/core/djangoapps/content/search/handlers.py b/openedx/core/djangoapps/content/search/handlers.py index ba0e8c1a1680..02b2e66c47df 100644 --- a/openedx/core/djangoapps/content/search/handlers.py +++ b/openedx/core/djangoapps/content/search/handlers.py @@ -136,7 +136,8 @@ def content_library_updated_handler(**kwargs) -> None: log.error("Received null or incorrect data for event") return - update_content_library_index_docs.delay(str(content_library_data.library_key)) + if content_library_data.update_blocks: + update_content_library_index_docs.delay(str(content_library_data.library_key)) @receiver(CONTENT_OBJECT_TAGS_CHANGED) diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index 2eacd02c7669..a4e9eda2f861 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -9,9 +9,9 @@ from celery import shared_task from celery_utils.logged_task import LoggedTask from edx_django_utils.monitoring import set_code_owner_attribute +from meilisearch.errors import MeilisearchError from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2 -from meilisearch.errors import MeilisearchError from . import api @@ -80,5 +80,6 @@ def update_content_library_index_docs(library_key_str: str) -> None: log.info("Updating content index documents for library with id: %s", library_key) - api.delete_content_library_index_docs(library_key) - api.upsert_content_library_index_docs(library_key) + tasks = api.delete_content_library_index_docs(library_key, wait=False) + \ + api.upsert_content_library_index_docs(library_key, wait=False) + api.wait_for_meili_tasks(tasks) From 9618b60ce7a134a2022025c1fb7f7e44b3d4b646 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 7 Aug 2024 12:36:24 +0530 Subject: [PATCH 3/6] refactor: delete all documents on discard --- openedx/core/djangoapps/content/search/api.py | 47 +++++-------------- .../core/djangoapps/content/search/tasks.py | 5 +- 2 files changed, 14 insertions(+), 38 deletions(-) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index 776de6738ea0..79726df65de6 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -143,7 +143,7 @@ def _wait_for_meili_task(info: TaskInfo) -> None: raise MeilisearchError(err_reason) -def wait_for_meili_tasks(info_list: list[TaskInfo]) -> None: +def _wait_for_meili_tasks(info_list: list[TaskInfo]) -> None: """ Simple helper method to wait for multiple Meilisearch tasks to complete """ @@ -231,7 +231,7 @@ def _recurse_children(block, fn, status_cb: Callable[[str], None] | None = None) fn(child) -def _update_index_docs(docs, wait=True) -> list[TaskInfo]: +def _update_index_docs(docs) -> list[TaskInfo]: """ Helper function that updates the documents in the search index @@ -249,9 +249,7 @@ def _update_index_docs(docs, wait=True) -> list[TaskInfo]: tasks.append(client.index(current_rebuild_index_name).update_documents(docs)) tasks.append(client.index(STUDIO_INDEX_NAME).update_documents(docs)) - if wait: - wait_for_meili_tasks(tasks) - return tasks + _wait_for_meili_tasks(tasks) def only_if_meilisearch_enabled(f): @@ -466,29 +464,24 @@ def delete_index_doc(usage_key: UsageKey) -> None: tasks.append(client.index(current_rebuild_index_name).delete_document(meili_id_from_opaque_key(usage_key))) tasks.append(client.index(STUDIO_INDEX_NAME).delete_document(meili_id_from_opaque_key(usage_key))) - wait_for_meili_tasks(tasks) + _wait_for_meili_tasks(tasks) -def delete_index_docs(meili_ids: list[str], wait=True) -> list[TaskInfo]: +def delete_all_index_docs_for_library(library_key: LibraryLocatorV2) -> None: """ Deletes documents for the given XBlocks from the search index - - Args: - meili_ids (list[str]): List of meili_ids from usage keys of the XBlocks to be removed from the index """ current_rebuild_index_name = _get_running_rebuild_index_name() - client = _get_meilisearch_client() + filter = f'{Fields.context_key}="{library_key}"' tasks = [] if current_rebuild_index_name: - # If there is a rebuild in progress, the document will also be deleted from the new index. - tasks.append(client.index(current_rebuild_index_name).delete_documents(meili_ids)) - tasks.append(client.index(STUDIO_INDEX_NAME).delete_documents(meili_ids)) + # If there is a rebuild in progress, the documents will also be deleted from the new index. + tasks.append(client.index(current_rebuild_index_name).delete_documents(filter=filter)) + tasks.append(client.index(STUDIO_INDEX_NAME).delete_documents(filter=filter)) - if wait: - wait_for_meili_tasks(tasks) - return tasks + _wait_for_meili_tasks(tasks) def upsert_library_block_index_doc(usage_key: UsageKey) -> None: @@ -506,7 +499,7 @@ def upsert_library_block_index_doc(usage_key: UsageKey) -> None: _update_index_docs(docs) -def upsert_content_library_index_docs(library_key: LibraryLocatorV2, wait=True) -> None: +def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None: """ Creates or updates the documents for the given Content Library in the search index """ @@ -516,23 +509,7 @@ def upsert_content_library_index_docs(library_key: LibraryLocatorV2, wait=True) doc = searchable_doc_for_library_block(metadata) docs.append(doc) - return _update_index_docs(docs, wait=wait) - - -def delete_content_library_index_docs(library_key: LibraryLocatorV2, wait=True) -> None: - """ - Deletes the discarded draft documents for the given Content Library in the search index - """ - meili_ids = [] - # draft version and published version equal to False/null means those drafts were discarded. - for component in lib_api.get_library_components(library_key, draft=False, published=False): - usage_key = LibraryUsageLocatorV2( - library_key, - component.component_type.name, - component.local_key, - ) - meili_ids.append(meili_id_from_opaque_key(usage_key)) - return delete_index_docs(meili_ids, wait=wait) + return _update_index_docs(docs) def upsert_block_tags_index_docs(usage_key: UsageKey): diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index a4e9eda2f861..73dcfe284d02 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -80,6 +80,5 @@ def update_content_library_index_docs(library_key_str: str) -> None: log.info("Updating content index documents for library with id: %s", library_key) - tasks = api.delete_content_library_index_docs(library_key, wait=False) + \ - api.upsert_content_library_index_docs(library_key, wait=False) - api.wait_for_meili_tasks(tasks) + api.delete_all_index_docs_for_library(library_key) + api.upsert_content_library_index_docs(library_key) From 24bef38e6ec8643f00d67fcea1b05db906db0379 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 7 Aug 2024 13:14:17 +0530 Subject: [PATCH 4/6] refactor: delete documents that were never published on discard --- openedx/core/djangoapps/content/search/api.py | 17 +++++++++++------ openedx/core/djangoapps/content/search/tasks.py | 4 +++- .../core/djangoapps/content_libraries/api.py | 5 ++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index 79726df65de6..bad46dab91a6 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -18,7 +18,7 @@ from meilisearch.errors import MeilisearchError from meilisearch.models.task import TaskInfo from opaque_keys.edx.keys import UsageKey -from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2 +from opaque_keys.edx.locator import LibraryLocatorV2 from common.djangoapps.student.roles import GlobalStaff from rest_framework.request import Request from common.djangoapps.student.role_helpers import get_course_roles @@ -231,7 +231,7 @@ def _recurse_children(block, fn, status_cb: Callable[[str], None] | None = None) fn(child) -def _update_index_docs(docs) -> list[TaskInfo]: +def _update_index_docs(docs) -> None: """ Helper function that updates the documents in the search index @@ -467,13 +467,18 @@ def delete_index_doc(usage_key: UsageKey) -> None: _wait_for_meili_tasks(tasks) -def delete_all_index_docs_for_library(library_key: LibraryLocatorV2) -> None: +def delete_all_draft_docs_for_library(library_key: LibraryLocatorV2) -> None: """ - Deletes documents for the given XBlocks from the search index + Deletes draft documents for the given XBlocks from the search index """ current_rebuild_index_name = _get_running_rebuild_index_name() client = _get_meilisearch_client() - filter = f'{Fields.context_key}="{library_key}"' + # Delete all documents where last_published is null i.e. never published before. + filter = [ + f'{Fields.context_key}="{library_key}"', + # inner arrays are connected by an OR + [f'{Fields.last_published} IS EMPTY', f'{Fields.last_published} IS NULL'], + ] tasks = [] if current_rebuild_index_name: @@ -509,7 +514,7 @@ def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None: doc = searchable_doc_for_library_block(metadata) docs.append(doc) - return _update_index_docs(docs) + _update_index_docs(docs) def upsert_block_tags_index_docs(usage_key: UsageKey): diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index 73dcfe284d02..dfd603776981 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -80,5 +80,7 @@ def update_content_library_index_docs(library_key_str: str) -> None: log.info("Updating content index documents for library with id: %s", library_key) - api.delete_all_index_docs_for_library(library_key) api.upsert_content_library_index_docs(library_key) + # Delete all documents in this library that were not published by above function + # as this task is also triggered on discard event. + api.delete_all_draft_docs_for_library(library_key) diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index 5a956b961394..17bea80b3a96 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -642,7 +642,7 @@ def _get_library_component_tags_count(library_key) -> dict: return get_object_tag_counts(library_key_pattern, count_implicit=True) -def get_library_components(library_key, text_search=None, block_types=None, draft=True, published=None) -> QuerySet[Component]: +def get_library_components(library_key, text_search=None, block_types=None) -> QuerySet[Component]: """ Get the library components and filter. @@ -653,8 +653,7 @@ def get_library_components(library_key, text_search=None, block_types=None, draf learning_package = lib.learning_package components = authoring_api.get_components( learning_package.id, - draft=draft, - published=published, + draft=True, namespace='xblock.v1', type_names=block_types, draft_title=text_search, From f4312c998a19e6898aac50eb219e1a3e0e5deb57 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 7 Aug 2024 15:36:42 +0530 Subject: [PATCH 5/6] test: delete documents that were never published on discard --- openedx/core/djangoapps/content/search/api.py | 6 +++--- .../core/djangoapps/content/search/handlers.py | 3 +-- .../djangoapps/content/search/tests/test_api.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index bad46dab91a6..cc80d36cbd49 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -474,7 +474,7 @@ def delete_all_draft_docs_for_library(library_key: LibraryLocatorV2) -> None: current_rebuild_index_name = _get_running_rebuild_index_name() client = _get_meilisearch_client() # Delete all documents where last_published is null i.e. never published before. - filter = [ + delete_filter = [ f'{Fields.context_key}="{library_key}"', # inner arrays are connected by an OR [f'{Fields.last_published} IS EMPTY', f'{Fields.last_published} IS NULL'], @@ -483,8 +483,8 @@ def delete_all_draft_docs_for_library(library_key: LibraryLocatorV2) -> None: tasks = [] if current_rebuild_index_name: # If there is a rebuild in progress, the documents will also be deleted from the new index. - tasks.append(client.index(current_rebuild_index_name).delete_documents(filter=filter)) - tasks.append(client.index(STUDIO_INDEX_NAME).delete_documents(filter=filter)) + tasks.append(client.index(current_rebuild_index_name).delete_documents(filter=delete_filter)) + tasks.append(client.index(STUDIO_INDEX_NAME).delete_documents(filter=delete_filter)) _wait_for_meili_tasks(tasks) diff --git a/openedx/core/djangoapps/content/search/handlers.py b/openedx/core/djangoapps/content/search/handlers.py index 02b2e66c47df..ba0e8c1a1680 100644 --- a/openedx/core/djangoapps/content/search/handlers.py +++ b/openedx/core/djangoapps/content/search/handlers.py @@ -136,8 +136,7 @@ def content_library_updated_handler(**kwargs) -> None: log.error("Received null or incorrect data for event") return - if content_library_data.update_blocks: - update_content_library_index_docs.delay(str(content_library_data.library_key)) + update_content_library_index_docs.delay(str(content_library_data.library_key)) @receiver(CONTENT_OBJECT_TAGS_CHANGED) diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py index 9dcdfb76b4a6..e8616cee60a8 100644 --- a/openedx/core/djangoapps/content/search/tests/test_api.py +++ b/openedx/core/djangoapps/content/search/tests/test_api.py @@ -388,3 +388,18 @@ def test_index_content_library_metadata(self, mock_meilisearch): mock_meilisearch.return_value.index.return_value.update_documents.assert_called_once_with( [self.doc_problem1, self.doc_problem2] ) + + @override_settings(MEILISEARCH_ENABLED=True) + def test_delete_all_drafts(self, mock_meilisearch): + """ + Test deleting all draft documents from the index. + """ + api.delete_all_draft_docs_for_library(self.library.key) + + delete_filter = [ + f'context_key="{self.library.key}"', + ['last_published IS EMPTY', 'last_published IS NULL'], + ] + mock_meilisearch.return_value.index.return_value.delete_documents.assert_called_once_with( + filter=delete_filter + ) From 3dbbbac16cc9ed6ded31f79e0b825fc44f793298 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 14 Aug 2024 21:03:27 +0530 Subject: [PATCH 6/6] chore: add comment --- openedx/core/djangoapps/content/search/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index cc80d36cbd49..4262b8a7b13f 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -476,7 +476,8 @@ def delete_all_draft_docs_for_library(library_key: LibraryLocatorV2) -> None: # Delete all documents where last_published is null i.e. never published before. delete_filter = [ f'{Fields.context_key}="{library_key}"', - # inner arrays are connected by an OR + # This field should only be NULL or have a value, but we're also checking IS EMPTY just in case. + # Inner arrays are connected by an OR [f'{Fields.last_published} IS EMPTY', f'{Fields.last_published} IS NULL'], ]