diff --git a/.github/DISCUSSION_TEMPLATE/support.yml b/.github/DISCUSSION_TEMPLATE/support.yml index 18c0812f2..311aaf7f3 100644 --- a/.github/DISCUSSION_TEMPLATE/support.yml +++ b/.github/DISCUSSION_TEMPLATE/support.yml @@ -51,5 +51,5 @@ body: id: logs attributes: label: Relevant logs or output - description: If you have logs, errors that might help, paste it here. + description: If you have logs, errors that might help, paste it here. For example other containers or services (database, redis, etc). render: bash diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 07e9e4690..b6baf49bf 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -6,8 +6,8 @@ body: - type: markdown attributes: value: | - ### ⚠️ Please remember: issues are for *bugs* - That is, something you believe affects every single user of Paperless-ngx, not just you. If you're not sure, start with one of the other options below. + ### ⚠️ Please remember: issues are for *bugs* only! ⚠️ + That is, something you believe affects every single user of Paperless-ngx (and the demo, for example), not just you. If you are not sure, start with one of the other options below. Also, note that **Paperless-ngx does not perform OCR or archive file creation itself**, those are handled by other tools. Problems with OCR or archive versions of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues - type: markdown @@ -59,6 +59,12 @@ body: label: Browser logs description: Logs from the web browser related to your issue, if needed render: bash + - type: textarea + id: logs_services + attributes: + label: Services logs + description: Logs from other services (or containers) related to your issue, if needed. For example, the database or redis logs. + render: bash - type: input id: version attributes: diff --git a/src/documents/mail.py b/src/documents/mail.py index 240b41e18..6af48ca9b 100644 --- a/src/documents/mail.py +++ b/src/documents/mail.py @@ -7,6 +7,8 @@ from django.conf import settings from django.core.mail import EmailMessage from filelock import FileLock +from documents.data_models import ConsumableDocument + if TYPE_CHECKING: from documents.models import Document @@ -15,7 +17,7 @@ def send_email( subject: str, body: str, to: list[str], - attachments: list[Document], + attachments: list[Document | ConsumableDocument], *, use_archive: bool, ) -> int: @@ -45,17 +47,20 @@ def send_email( # Something could be renaming the file concurrently so it can't be attached with FileLock(settings.MEDIA_LOCK): for document in attachments: - attachment_path = ( - document.archive_path - if use_archive and document.has_archive_version - else document.source_path - ) - - friendly_filename = _get_unique_filename( - document, - used_filenames, - archive=use_archive and document.has_archive_version, - ) + if isinstance(document, ConsumableDocument): + attachment_path = document.original_file + friendly_filename = document.original_file.name + else: + attachment_path = ( + document.archive_path + if use_archive and document.has_archive_version + else document.source_path + ) + friendly_filename = _get_unique_filename( + document, + used_filenames, + archive=use_archive and document.has_archive_version, + ) used_filenames.add(friendly_filename) with attachment_path.open("rb") as f: diff --git a/src/documents/migrations/1073_migrate_workflow_title_jinja.py b/src/documents/migrations/1073_migrate_workflow_title_jinja.py index c3f929eff..9d80a277f 100644 --- a/src/documents/migrations/1073_migrate_workflow_title_jinja.py +++ b/src/documents/migrations/1073_migrate_workflow_title_jinja.py @@ -3,7 +3,6 @@ import logging from django.db import migrations from django.db import models -from django.db import transaction from documents.templating.utils import convert_format_str_to_template_format @@ -11,21 +10,34 @@ logger = logging.getLogger("paperless.migrations") def convert_from_format_to_template(apps, schema_editor): - WorkflowActions = apps.get_model("documents", "WorkflowAction") + WorkflowAction = apps.get_model("documents", "WorkflowAction") - with transaction.atomic(): - for WorkflowAction in WorkflowActions.objects.all(): - if not WorkflowAction.assign_title: - continue - WorkflowAction.assign_title = convert_format_str_to_template_format( - WorkflowAction.assign_title, - ) - logger.debug( - "Converted WorkflowAction id %d title to template format: %s", - WorkflowAction.id, - WorkflowAction.assign_title, - ) - WorkflowAction.save() + batch_size = 500 + actions_to_update = [] + + queryset = ( + WorkflowAction.objects.filter(assign_title__isnull=False) + .exclude(assign_title="") + .only("id", "assign_title") + ) + + for action in queryset: + action.assign_title = convert_format_str_to_template_format( + action.assign_title, + ) + logger.debug( + "Converted WorkflowAction id %d title to template format: %s", + action.id, + action.assign_title, + ) + actions_to_update.append(action) + + if actions_to_update: + WorkflowAction.objects.bulk_update( + actions_to_update, + ["assign_title"], + batch_size=batch_size, + ) class Migration(migrations.Migration): diff --git a/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py b/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py new file mode 100644 index 000000000..4381eabb1 --- /dev/null +++ b/src/documents/migrations/1074_workflowrun_deleted_at_workflowrun_restored_at_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.6 on 2025-10-27 15:11 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1073_migrate_workflow_title_jinja"), + ] + + operations = [ + migrations.AddField( + model_name="workflowrun", + name="deleted_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="workflowrun", + name="restored_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="workflowrun", + name="transaction_id", + field=models.UUIDField(blank=True, null=True), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index 4794bc82f..12dab2b6d 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -1547,7 +1547,7 @@ class Workflow(models.Model): return f"Workflow: {self.name}" -class WorkflowRun(models.Model): +class WorkflowRun(SoftDeleteModel): workflow = models.ForeignKey( Workflow, on_delete=models.CASCADE, diff --git a/src/documents/templating/workflows.py b/src/documents/templating/workflows.py index 25f1e57ef..67f3ac930 100644 --- a/src/documents/templating/workflows.py +++ b/src/documents/templating/workflows.py @@ -80,7 +80,7 @@ def parse_w_workflow_placeholders( if doc_url is not None: formatting.update({"doc_url": doc_url}) - logger.debug(f"Jinja Template is : {text}") + logger.debug(f"Parsing Workflow Jinja template: {text}") try: template = _template_environment.from_string( text, diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index a6da01578..c25565ae6 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -30,6 +30,7 @@ from pytest_django.fixtures import SettingsWrapper from documents import tasks from documents.data_models import ConsumableDocument +from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentSource from documents.matching import document_matches_workflow from documents.matching import existing_document_matches_workflow @@ -2788,6 +2789,80 @@ class TestWorkflows( self.assertEqual(doc.tags.all().count(), 1) self.assertIn(self.t2, doc.tags.all()) + @override_settings( + PAPERLESS_EMAIL_HOST="localhost", + EMAIL_ENABLED=True, + PAPERLESS_URL="http://localhost:8000", + ) + @mock.patch("django.core.mail.message.EmailMessage.send") + def test_workflow_assignment_then_email_includes_attachment(self, mock_email_send): + """ + GIVEN: + - Workflow with assignment and email actions + - Email action configured to include the document + WHEN: + - Workflow is run on a newly created document + THEN: + - Email action sends the document as an attachment + """ + + storage_path = StoragePath.objects.create( + name="sp2", + path="workflow/{{ document.pk }}", + ) + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION, + ) + assignment_action = WorkflowAction.objects.create( + type=WorkflowAction.WorkflowActionType.ASSIGNMENT, + assign_storage_path=storage_path, + assign_owner=self.user2, + ) + assignment_action.assign_tags.add(self.t1) + + email_action_config = WorkflowActionEmail.objects.create( + subject="Doc ready {doc_title}", + body="Document URL: {doc_url}", + to="owner@example.com", + include_document=True, + ) + email_action = WorkflowAction.objects.create( + type=WorkflowAction.WorkflowActionType.EMAIL, + email=email_action_config, + ) + + workflow = Workflow.objects.create(name="Assignment then email", order=0) + workflow.triggers.add(trigger) + workflow.actions.set([assignment_action, email_action]) + + temp_working_copy = shutil.copy( + self.SAMPLE_DIR / "simple.pdf", + self.dirs.scratch_dir / "working-copy.pdf", + ) + + Document.objects.create( + title="workflow doc", + correspondent=self.c, + checksum="wf-assignment-email", + mime_type="application/pdf", + ) + + consumable_document = ConsumableDocument( + source=DocumentSource.ConsumeFolder, + original_file=temp_working_copy, + ) + + mock_email_send.return_value = 1 + + with self.assertNoLogs("paperless.handlers", level="ERROR"): + run_workflows( + WorkflowTrigger.WorkflowTriggerType.CONSUMPTION, + consumable_document, + overrides=DocumentMetadataOverrides(), + ) + + mock_email_send.assert_called_once() + @override_settings( PAPERLESS_EMAIL_HOST="localhost", EMAIL_ENABLED=True,