Compare commits

...
5 changed files with 122 additions and 13 deletions

No files matched your search

+8
View File
@@ -1381,9 +1381,17 @@ def task_revoked_handler(
return return
try: try:
close_old_connections() close_old_connections()
reason = "Task revoked before completion."
if expired:
reason = "Task expired before execution."
elif terminated:
reason = "Task terminated during execution."
PaperlessTask.objects.filter(task_id=task_id).update( PaperlessTask.objects.filter(task_id=task_id).update(
status=PaperlessTask.Status.REVOKED, status=PaperlessTask.Status.REVOKED,
date_done=timezone.now(), date_done=timezone.now(),
result_data={
"reason": (reason + " Check worker logs for details."),
},
) )
except Exception: # pragma: no cover except Exception: # pragma: no cover
logger.exception("Updating PaperlessTask on revocation failed") logger.exception("Updating PaperlessTask on revocation failed")
+65
View File
@@ -46,6 +46,7 @@ from documents.models import UiSettings
from documents.models import Workflow from documents.models import Workflow
from documents.models import WorkflowAction from documents.models import WorkflowAction
from documents.models import WorkflowTrigger from documents.models import WorkflowTrigger
from documents.search import get_backend
from documents.signals.handlers import run_workflows from documents.signals.handlers import run_workflows
from documents.tests.utils import ConsumeTaskMixin from documents.tests.utils import ConsumeTaskMixin
from paperless_testing.dirs import DirectoriesMixin from paperless_testing.dirs import DirectoriesMixin
@@ -66,6 +67,70 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
cache.clear() cache.clear()
def test_detail_save_indexes_final_workflow_state(self) -> None:
viewer = UserFactory()
grant_global(viewer, "view_document")
inbox = TagFactory(is_inbox_tag=True)
doc = DocumentFactory(owner=self.user, content="distinctiveword")
doc.tags.add(inbox)
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
)
action = WorkflowAction.objects.create(assign_title="Workflow title")
action.assign_view_users.add(viewer)
action.remove_view_users.add(viewer)
workflow = Workflow.objects.create(name="Update document")
workflow.triggers.add(trigger)
workflow.actions.add(action)
backend = get_backend()
backend.add_or_update(doc)
for action_type in (
WorkflowAction.WorkflowActionType.ASSIGNMENT,
WorkflowAction.WorkflowActionType.REMOVAL,
WorkflowAction.WorkflowActionType.MOVE_TO_TRASH,
):
with self.subTest(action_type=action_type):
grant = action_type == WorkflowAction.WorkflowActionType.ASSIGNMENT
action.type = action_type
action.save()
trigger.filter_has_not_tags.set([inbox] if grant else [])
trigger.filter_has_tags.set([] if grant else [inbox])
self.client.force_authenticate(self.user)
response = self.client.patch(
f"/api/documents/{doc.pk}/",
{"tags": [] if grant else [inbox.pk]},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.client.force_authenticate(viewer)
self.assertEqual(
self.client.get(f"/api/documents/{doc.pk}/").status_code,
status.HTTP_200_OK if grant else status.HTTP_404_NOT_FOUND,
)
# Check the index directly: the API's DB permission filtering
# would otherwise hide stale grants and trashed index entries.
self.assertEqual(
list(backend.search_ids("distinctiveword", user=viewer)),
[doc.pk] if grant else [],
)
if grant:
response = self.client.get(
"/api/documents/",
{"query": "distinctiveword AND title:workflow"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[result["id"] for result in response.data["results"]],
[doc.pk],
)
elif action_type == WorkflowAction.WorkflowActionType.MOVE_TO_TRASH:
self.assertFalse(Document.objects.filter(pk=doc.pk).exists())
self.assertEqual(
list(backend.search_ids("distinctiveword", user=None)),
[],
)
def testDocuments(self) -> None: def testDocuments(self) -> None:
response = self.client.get("/api/documents/").data response = self.client.get("/api/documents/").data
+30
View File
@@ -429,6 +429,36 @@ class TestTaskRevokedHandler:
assert task.status == PaperlessTask.Status.REVOKED assert task.status == PaperlessTask.Status.REVOKED
assert task.date_done is not None assert task.date_done is not None
@pytest.mark.parametrize(
("expired", "terminated", "reason"),
[
(True, False, "Task expired before execution."),
(False, True, "Task terminated during execution."),
(False, False, "Task revoked before completion."),
],
)
def test_records_revocation_details(
self,
mocker,
expired,
terminated,
reason,
) -> None:
task = PaperlessTaskFactory(status=PaperlessTask.Status.PENDING)
request = mocker.MagicMock()
request.id = task.task_id
task_revoked_handler(
request=request,
expired=expired,
terminated=terminated,
)
task.refresh_from_db()
assert task.result_data == {
"reason": f"{reason} Check worker logs for details.",
}
def test_ignores_none_request(self) -> None: def test_ignores_none_request(self) -> None:
"""task_revoked_handler must not raise when request is None.""" """task_revoked_handler must not raise when request is None."""
+8 -2
View File
@@ -1304,13 +1304,19 @@ class DocumentViewSet(
from documents.search import get_backend from documents.search import get_backend
get_backend().add_or_update(refreshed_doc)
document_updated.send( document_updated.send(
sender=self.__class__, sender=self.__class__,
document=refreshed_doc, document=refreshed_doc,
) )
# Index the final workflow state, including deletion.
backend = get_backend()
final_doc = Document.objects.filter(pk=root_doc.pk).first()
if final_doc is None:
backend.remove(root_doc.pk)
else:
backend.add_or_update(final_doc)
return response return response
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
+11 -11
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-24 21:11+0000\n" "POT-Creation-Date: 2026-10-28 02:55+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n" "PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: English\n" "Language-Team: English\n"
@@ -1653,7 +1653,7 @@ msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840 #: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:360 documents/views.py:2724 #: documents/serialisers.py:2872 documents/views.py:360 documents/views.py:2730
#: paperless_mail/serialisers.py:156 #: paperless_mail/serialisers.py:156
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
@@ -1694,7 +1694,7 @@ msgstr ""
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4774 #: documents/serialisers.py:2958 documents/views.py:4780
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1949,36 +1949,36 @@ msgstr ""
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:353 documents/views.py:2721 #: documents/views.py:353 documents/views.py:2727
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1659 #: documents/views.py:1665
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1670 #: documents/views.py:1676
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:1682 #: documents/views.py:1688
msgid "AI backend rejected the request. Check logs for details." msgid "AI backend rejected the request. Check logs for details."
msgstr "" msgstr ""
#: documents/views.py:4790 #: documents/views.py:4796
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4837 #: documents/views.py:4843
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4903 #: documents/views.py:4909
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4917 #: documents/views.py:4923
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""