mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-10 21:03:18 +00:00
Ok! Backend stuff for the remote ocr workflow
This commit is contained in:
@@ -473,6 +473,16 @@ class ConsumerPlugin(
|
||||
f"Unsupported mime type {mime_type}",
|
||||
)
|
||||
|
||||
if self.metadata.remote_ocr and not getattr(
|
||||
parser_class,
|
||||
"uses_remote_service",
|
||||
False,
|
||||
):
|
||||
self.log.warning(
|
||||
"Remote OCR was requested for this document but no remote "
|
||||
"parser is available for it, processing locally instead.",
|
||||
)
|
||||
|
||||
# Notify all listeners that we're going to do some work.
|
||||
|
||||
document_consumption_started.send(
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.16 on 2026-08-10 17:27
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "0022_add_perf_indexes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="workflowaction",
|
||||
name="type",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, "Assignment"),
|
||||
(2, "Removal"),
|
||||
(3, "Email"),
|
||||
(4, "Webhook"),
|
||||
(5, "Password removal"),
|
||||
(6, "Move to trash"),
|
||||
(7, "Remote OCR"),
|
||||
],
|
||||
default=1,
|
||||
verbose_name="Workflow Action Type",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1599,6 +1599,10 @@ class WorkflowAction(models.Model):
|
||||
6,
|
||||
_("Move to trash"),
|
||||
)
|
||||
REMOTE_OCR = (
|
||||
7,
|
||||
_("Remote OCR"),
|
||||
)
|
||||
|
||||
type = models.PositiveSmallIntegerField(
|
||||
_("Workflow Action Type"),
|
||||
|
||||
@@ -3277,6 +3277,26 @@ class WorkflowSerializer(serializers.ModelSerializer[Workflow]):
|
||||
"actions",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
|
||||
triggers = attrs.get("triggers") or []
|
||||
actions = attrs.get("actions") or []
|
||||
|
||||
# Remote OCR can only work with consumption triggers
|
||||
if any(
|
||||
action.get("type") == WorkflowAction.WorkflowActionType.REMOTE_OCR
|
||||
for action in actions
|
||||
) and not any(
|
||||
trigger.get("type") == WorkflowTrigger.WorkflowTriggerType.CONSUMPTION
|
||||
for trigger in triggers
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Remote OCR actions require a consumption started trigger",
|
||||
)
|
||||
|
||||
return attrs
|
||||
|
||||
def update_triggers_and_actions(
|
||||
self,
|
||||
instance: Workflow,
|
||||
|
||||
@@ -971,6 +971,17 @@ def run_workflows(
|
||||
)
|
||||
elif action.type == WorkflowAction.WorkflowActionType.MOVE_TO_TRASH:
|
||||
has_move_to_trash_action = True
|
||||
elif action.type == WorkflowAction.WorkflowActionType.REMOTE_OCR:
|
||||
if use_overrides and overrides:
|
||||
overrides.remote_ocr = True
|
||||
else:
|
||||
# If a workflow has a consumption trigger *and* another type,
|
||||
# the document has already been parsed by the time the other one fires
|
||||
logger.debug(
|
||||
"Remote OCR action only applies to consumption "
|
||||
"triggers, ignoring",
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
|
||||
if not use_overrides:
|
||||
# limit title to 128 characters
|
||||
|
||||
@@ -390,6 +390,82 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
|
||||
self.assertEqual(Workflow.objects.count(), 1)
|
||||
|
||||
def test_api_create_remote_ocr_action_requires_consumption_trigger(
|
||||
self,
|
||||
) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a workflow with a remote OCR action
|
||||
- No consumption started trigger, so the action could never run
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP 400 response
|
||||
- No objects are created
|
||||
"""
|
||||
existing_count = Workflow.objects.count()
|
||||
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Remote OCR too late",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.REMOTE_OCR,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(Workflow.objects.count(), existing_count)
|
||||
|
||||
def test_api_create_remote_ocr_action_with_consumption_trigger(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a workflow with a remote OCR action
|
||||
- A consumption started trigger alongside another trigger type
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- The workflow is created, the action applies to consumption only
|
||||
"""
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Remote OCR on consume",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"filter_filename": "*.pdf",
|
||||
},
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.REMOTE_OCR,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_api_create_workflow_trigger_action_empty_fields(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
|
||||
@@ -5360,3 +5360,82 @@ class TestDateWorkflowLocalization(
|
||||
document = Document.objects.first()
|
||||
assert document is not None
|
||||
assert document.title == expected_title
|
||||
|
||||
|
||||
class TestRemoteOCRWorkflowAction(DirectoriesMixin, SampleDirMixin, APITestCase):
|
||||
def _make_workflow(self, trigger_type) -> None:
|
||||
trigger = WorkflowTrigger.objects.create(type=trigger_type)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.REMOTE_OCR,
|
||||
)
|
||||
w = Workflow.objects.create(name="Remote OCR", order=0)
|
||||
w.triggers.add(trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
def test_consumption_trigger_requests_remote_ocr(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- A consumption workflow with a remote OCR action
|
||||
WHEN:
|
||||
- A matching document is consumed
|
||||
THEN:
|
||||
- The overrides ask for remote OCR, which is what the consumer
|
||||
reads when choosing a parser
|
||||
"""
|
||||
self._make_workflow(WorkflowTrigger.WorkflowTriggerType.CONSUMPTION)
|
||||
|
||||
test_file = shutil.copy(
|
||||
self.SAMPLE_DIR / "simple.pdf",
|
||||
self.dirs.scratch_dir / "simple.pdf",
|
||||
)
|
||||
overrides = DocumentMetadataOverrides()
|
||||
|
||||
run_workflows(
|
||||
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
ConsumableDocument(
|
||||
source=DocumentSource.ConsumeFolder,
|
||||
original_file=test_file,
|
||||
),
|
||||
overrides=overrides,
|
||||
)
|
||||
|
||||
self.assertTrue(overrides.remote_ocr)
|
||||
|
||||
def test_other_trigger_types_are_ignored(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- A workflow with a remote OCR action that also has a
|
||||
non-consumption trigger, which is a valid combination
|
||||
WHEN:
|
||||
- The non-consumption trigger fires
|
||||
THEN:
|
||||
- The action is skipped, since the document has already been
|
||||
parsed by this point
|
||||
"""
|
||||
trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
)
|
||||
updated_trigger = WorkflowTrigger.objects.create(
|
||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
)
|
||||
action = WorkflowAction.objects.create(
|
||||
type=WorkflowAction.WorkflowActionType.REMOTE_OCR,
|
||||
)
|
||||
w = Workflow.objects.create(name="Remote OCR", order=0)
|
||||
w.triggers.add(trigger, updated_trigger)
|
||||
w.actions.add(action)
|
||||
w.save()
|
||||
|
||||
doc = Document.objects.create(
|
||||
title="sample test",
|
||||
original_filename="sample.pdf",
|
||||
)
|
||||
|
||||
with self.assertLogs("paperless.handlers", level="DEBUG") as cm:
|
||||
run_workflows(
|
||||
WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||
doc,
|
||||
)
|
||||
|
||||
self.assertIn("only applies to consumption triggers", "".join(cm.output))
|
||||
|
||||
Reference in New Issue
Block a user