From f3e6ed56b927794920fe957ddbe25145e3fda8bb Mon Sep 17 00:00:00 2001
From: Trenton H <797416+stumpylog@users.noreply.github.com>
Date: Tue, 4 Mar 2025 10:26:25 -0800
Subject: [PATCH 1/5] Removes the unused Log model and LogFilterSet (#9294)

---
 src/documents/filters.py                    |  7 -----
 src/documents/migrations/1064_delete_log.py | 15 ++++++++++
 src/documents/models.py                     | 31 ---------------------
 3 files changed, 15 insertions(+), 38 deletions(-)
 create mode 100644 src/documents/migrations/1064_delete_log.py

diff --git a/src/documents/filters.py b/src/documents/filters.py
index d3b0ad3ce..90161a1e6 100644
--- a/src/documents/filters.py
+++ b/src/documents/filters.py
@@ -36,7 +36,6 @@ from documents.models import CustomField
 from documents.models import CustomFieldInstance
 from documents.models import Document
 from documents.models import DocumentType
-from documents.models import Log
 from documents.models import PaperlessTask
 from documents.models import ShareLink
 from documents.models import StoragePath
@@ -761,12 +760,6 @@ class DocumentFilterSet(FilterSet):
         }
 
 
-class LogFilterSet(FilterSet):
-    class Meta:
-        model = Log
-        fields = {"level": INT_KWARGS, "created": DATE_KWARGS, "group": ID_KWARGS}
-
-
 class ShareLinkFilterSet(FilterSet):
     class Meta:
         model = ShareLink
diff --git a/src/documents/migrations/1064_delete_log.py b/src/documents/migrations/1064_delete_log.py
new file mode 100644
index 000000000..ec0830a91
--- /dev/null
+++ b/src/documents/migrations/1064_delete_log.py
@@ -0,0 +1,15 @@
+# Generated by Django 5.1.6 on 2025-02-28 15:19
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("documents", "1063_paperlesstask_type_alter_paperlesstask_task_name_and_more"),
+    ]
+
+    operations = [
+        migrations.DeleteModel(
+            name="Log",
+        ),
+    ]
diff --git a/src/documents/models.py b/src/documents/models.py
index 42e473438..57ff96df1 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -1,5 +1,4 @@
 import datetime
-import logging
 import os
 import re
 from collections import OrderedDict
@@ -379,36 +378,6 @@ class Document(SoftDeleteModel, ModelWithOwner):
         return timezone.localdate(self.created)
 
 
-class Log(models.Model):
-    LEVELS = (
-        (logging.DEBUG, _("debug")),
-        (logging.INFO, _("information")),
-        (logging.WARNING, _("warning")),
-        (logging.ERROR, _("error")),
-        (logging.CRITICAL, _("critical")),
-    )
-
-    group = models.UUIDField(_("group"), blank=True, null=True)
-
-    message = models.TextField(_("message"))
-
-    level = models.PositiveIntegerField(
-        _("level"),
-        choices=LEVELS,
-        default=logging.INFO,
-    )
-
-    created = models.DateTimeField(_("created"), auto_now_add=True)
-
-    class Meta:
-        ordering = ("-created",)
-        verbose_name = _("log")
-        verbose_name_plural = _("logs")
-
-    def __str__(self):
-        return self.message
-
-
 class SavedView(ModelWithOwner):
     class DisplayMode(models.TextChoices):
         TABLE = ("table", _("Table"))

From d82555e644b2be587f5635941a8f889bc3121511 Mon Sep 17 00:00:00 2001
From: Trenton H <797416+stumpylog@users.noreply.github.com>
Date: Tue, 4 Mar 2025 10:38:06 -0800
Subject: [PATCH 2/5] Enables Codecov test reporting for the backend (#9295)

---
 .github/workflows/ci.yml | 12 +++++++++++-
 pyproject.toml           |  2 ++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e2ec5e41a..c24678bf2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -167,7 +167,9 @@ jobs:
         uses: actions/upload-artifact@v4
         with:
           name: backend-coverage-report
-          path: coverage.xml
+          path: |
+            coverage.xml
+            junit.xml
           retention-days: 7
           if-no-files-found: error
       -
@@ -315,6 +317,14 @@ jobs:
           # future expansion
           flags: backend
           directory: src/
+      -
+        name: Upload test results to Codecov
+        if: ${{ !cancelled() }}
+        uses: codecov/test-results-action@v1
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          flags: backend
+          directory: src/
       -
         name: Use Node.js 20
         uses: actions/setup-node@v4
diff --git a/pyproject.toml b/pyproject.toml
index d6bb6b5fe..291033c13 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -329,6 +329,8 @@ addopts = [
   "--maxprocesses=16",
   "--quiet",
   "--durations=50",
+  "--junitxml=junit.xml",
+  "-o junit_family=legacy",
 ]
 norecursedirs = [ "src/locale/", ".venv/", "src-ui/" ]
 

From 817aad7c8ba74243aaabddc0f4887d52b91977c5 Mon Sep 17 00:00:00 2001
From: Trenton H <797416+stumpylog@users.noreply.github.com>
Date: Tue, 4 Mar 2025 10:56:28 -0800
Subject: [PATCH 3/5] Fix: Switches data to content to upload raw bytes/text
 content (#9293)

---
 src/documents/signals/handlers.py     | 2 +-
 src/documents/tests/test_workflows.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py
index 1fd34dd81..7241924e4 100644
--- a/src/documents/signals/handlers.py
+++ b/src/documents/signals/handlers.py
@@ -632,7 +632,7 @@ def send_webhook(
         else:
             httpx.post(
                 url,
-                data=data,
+                content=data,
                 files=files,
                 headers=headers,
             ).raise_for_status()
diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py
index b9205d4bb..94dcb7689 100644
--- a/src/documents/tests/test_workflows.py
+++ b/src/documents/tests/test_workflows.py
@@ -2603,7 +2603,7 @@ class TestWorkflows(
 
             mock_post.assert_called_once_with(
                 "http://paperless-ngx.com",
-                data="Test message",
+                content="Test message",
                 headers={},
                 files=None,
             )

From 344b2bc0eb48acf8403979178d38c7ab05e7e146 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 4 Mar 2025 19:09:38 +0000
Subject: [PATCH 4/5] Chore(deps-dev): Bump the frontend-angular-dependencies
 group in /src-ui with 5 updates (#9288)

* Chore(deps-dev): Bump the frontend-angular-dependencies group

Bumps the frontend-angular-dependencies group in /src-ui with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular-eslint/builder](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/builder) | `19.1.0` | `19.2.0` |
| [@angular-eslint/eslint-plugin](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin) | `19.1.0` | `19.2.0` |
| [@angular-eslint/eslint-plugin-template](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/eslint-plugin-template) | `19.1.0` | `19.2.0` |
| [@angular-eslint/schematics](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/schematics) | `19.1.0` | `19.2.0` |
| [@angular-eslint/template-parser](https://github.com/angular-eslint/angular-eslint/tree/HEAD/packages/template-parser) | `19.1.0` | `19.2.0` |


Updates `@angular-eslint/builder` from 19.1.0 to 19.2.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/builder/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v19.2.0/packages/builder)

Updates `@angular-eslint/eslint-plugin` from 19.1.0 to 19.2.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v19.2.0/packages/eslint-plugin)

Updates `@angular-eslint/eslint-plugin-template` from 19.1.0 to 19.2.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin-template/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v19.2.0/packages/eslint-plugin-template)

Updates `@angular-eslint/schematics` from 19.1.0 to 19.2.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/schematics/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v19.2.0/packages/schematics)

Updates `@angular-eslint/template-parser` from 19.1.0 to 19.2.0
- [Release notes](https://github.com/angular-eslint/angular-eslint/releases)
- [Changelog](https://github.com/angular-eslint/angular-eslint/blob/main/packages/template-parser/CHANGELOG.md)
- [Commits](https://github.com/angular-eslint/angular-eslint/commits/v19.2.0/packages/template-parser)

---
updated-dependencies:
- dependency-name: "@angular-eslint/builder"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/eslint-plugin-template"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/schematics"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-eslint/template-parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix lint error on toast close output name

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
---
 src-ui/package-lock.json                      | 105 ++++++++++--------
 src-ui/package.json                           |  10 +-
 .../toasts-dropdown.component.html            |   2 +-
 .../common/toast/toast.component.html         |   2 +-
 .../common/toast/toast.component.ts           |   2 +-
 .../common/toasts/toasts.component.html       |   2 +-
 6 files changed, 69 insertions(+), 54 deletions(-)

diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json
index 824333f27..2492a5f2d 100644
--- a/src-ui/package-lock.json
+++ b/src-ui/package-lock.json
@@ -44,11 +44,11 @@
         "@angular-devkit/build-angular": "^19.0.4",
         "@angular-devkit/core": "^19.2.0",
         "@angular-devkit/schematics": "^19.2.0",
-        "@angular-eslint/builder": "19.1.0",
-        "@angular-eslint/eslint-plugin": "19.1.0",
-        "@angular-eslint/eslint-plugin-template": "19.1.0",
-        "@angular-eslint/schematics": "19.1.0",
-        "@angular-eslint/template-parser": "19.1.0",
+        "@angular-eslint/builder": "19.2.0",
+        "@angular-eslint/eslint-plugin": "19.2.0",
+        "@angular-eslint/eslint-plugin-template": "19.2.0",
+        "@angular-eslint/schematics": "19.2.0",
+        "@angular-eslint/template-parser": "19.2.0",
         "@angular/cli": "~19.2.0",
         "@angular/compiler-cli": "~19.2.0",
         "@codecov/webpack-plugin": "^1.9.0",
@@ -1148,9 +1148,9 @@
       }
     },
     "node_modules/@angular-eslint/builder": {
-      "version": "19.1.0",
-      "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.1.0.tgz",
-      "integrity": "sha512-LWdQMTES/7GySlpTNFJn3k33ZGmjjWlHI/+IHV7B3xHQ9hj4MPK4ACmE/PNOAIQ9LwQm7sKS+3cTMxOZQ/cvSg==",
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.2.0.tgz",
+      "integrity": "sha512-8Lx24MrMJT8RlgDtwqfiLiJo4DzSaktjco6RmELUdWO2chJgRe9y+2iIgOeB2pmyD9UCsubwsfjBXlrnV/MPhQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1163,21 +1163,21 @@
       }
     },
     "node_modules/@angular-eslint/bundled-angular-compiler": {
-      "version": "19.1.0",
-      "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.1.0.tgz",
-      "integrity": "sha512-HUJyukRvnh8Z9lIdxdblBRuBaPYEVv4iAYZMw3d+dn4rrM27Nt5oh3/zkwYrrPkt36tZdeXdDWrOuz9jgjVN5w==",
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.2.0.tgz",
+      "integrity": "sha512-hmmAogTpYGbBvnJ0j7DNLi8YQ+YEEuwFdx0heU8XjTpZlRoSRIP7MJJVlaQCt+ZT5f5XwdGtqi9lOXqqcyGHLA==",
       "dev": true,
       "license": "MIT"
     },
     "node_modules/@angular-eslint/eslint-plugin": {
-      "version": "19.1.0",
-      "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.1.0.tgz",
-      "integrity": "sha512-TDO0+Ry+oNkxnaLHogKp1k2aey6IkJef5d7hathE4UFT6owjRizltWaRoX6bGw7Qu1yagVLL8L2Se8SddxSPAQ==",
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.2.0.tgz",
+      "integrity": "sha512-QQWWDrTdJ22tBd7RLFG/FdPwNyYEhg7YwWgn29z6XcdnV00ZFtf7FRbv/te1kqVNPvfjtht7bvtHcPQ432aUdQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@angular-eslint/bundled-angular-compiler": "19.1.0",
-        "@angular-eslint/utils": "19.1.0"
+        "@angular-eslint/bundled-angular-compiler": "19.2.0",
+        "@angular-eslint/utils": "19.2.0"
       },
       "peerDependencies": {
         "@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
@@ -1186,14 +1186,14 @@
       }
     },
     "node_modules/@angular-eslint/eslint-plugin-template": {
-      "version": "19.1.0",
-      "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.1.0.tgz",
-      "integrity": "sha512-bIUizkCY40mnU8oAO1tLV7uN2H/cHf1evLlhpqlb9JYwc5dT2moiEhNDo61OtOgkJmDGNuThAeO9Xk9hGQc7nA==",
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.2.0.tgz",
+      "integrity": "sha512-lUSzmk5/Dr0bNc2Omb5CZDu3zQZh70bJyuXnN5MKd00V1b3u90eqvMSveFzWFJ6Eot8Hh8+FxtiozPwGqOE+Og==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@angular-eslint/bundled-angular-compiler": "19.1.0",
-        "@angular-eslint/utils": "19.1.0",
+        "@angular-eslint/bundled-angular-compiler": "19.2.0",
+        "@angular-eslint/utils": "19.2.0",
         "aria-query": "5.3.2",
         "axobject-query": "4.1.0"
       },
@@ -1204,17 +1204,47 @@
         "typescript": "*"
       }
     },
+    "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@angular-eslint/utils": {
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.2.0.tgz",
+      "integrity": "sha512-1XQXzIqYadKUxcAgW1DPev56SVbR8Uld6TthgolU7rfIX23RYMIIRtQlrQCk7zoXLXm5fzcGqjTR4wHfoD+iWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@angular-eslint/bundled-angular-compiler": "19.2.0"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": "*"
+      }
+    },
+    "node_modules/@angular-eslint/eslint-plugin/node_modules/@angular-eslint/utils": {
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.2.0.tgz",
+      "integrity": "sha512-1XQXzIqYadKUxcAgW1DPev56SVbR8Uld6TthgolU7rfIX23RYMIIRtQlrQCk7zoXLXm5fzcGqjTR4wHfoD+iWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@angular-eslint/bundled-angular-compiler": "19.2.0"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": "*"
+      }
+    },
     "node_modules/@angular-eslint/schematics": {
-      "version": "19.1.0",
-      "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.1.0.tgz",
-      "integrity": "sha512-6S1FjmM7rZxc0u0W0KjqWYOkFQ0q89IGyjPkdUt1a8NwRnWg3VoXp4WYfeuZOjda/FEYuBS/E6rckLAMp0h6Aw==",
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.2.0.tgz",
+      "integrity": "sha512-SQfbKgPEJNkK5TVXRsdnWp6TjvVZOczvf8lELF1n+I/Uwmp7ulUjTRgTo59ZQnXoPSs2qCPgS4gAOVR6CD91zQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@angular-devkit/core": ">= 19.0.0 < 20.0.0",
         "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0",
-        "@angular-eslint/eslint-plugin": "19.1.0",
-        "@angular-eslint/eslint-plugin-template": "19.1.0",
+        "@angular-eslint/eslint-plugin": "19.2.0",
+        "@angular-eslint/eslint-plugin-template": "19.2.0",
         "ignore": "7.0.3",
         "semver": "7.7.1",
         "strip-json-comments": "3.1.1"
@@ -1231,13 +1261,13 @@
       }
     },
     "node_modules/@angular-eslint/template-parser": {
-      "version": "19.1.0",
-      "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.1.0.tgz",
-      "integrity": "sha512-wbMi7adlC+uYqZo7NHNBShpNhFJRZsXLqihqvFpAUt1Ei6uDX8HR6MyMEDZ9tUnlqtPVW5nmbedPyLVG7HkjAA==",
+      "version": "19.2.0",
+      "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.2.0.tgz",
+      "integrity": "sha512-VqgvFrILhoMe0GHZrx+Bjy8kx7/LJfJTd+x/wzE/X1cCChSU81MBZFMVeFMnoI75OOQUf4fwaaKrtUhUvAkVyw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@angular-eslint/bundled-angular-compiler": "19.1.0",
+        "@angular-eslint/bundled-angular-compiler": "19.2.0",
         "eslint-scope": "^8.0.2"
       },
       "peerDependencies": {
@@ -1245,21 +1275,6 @@
         "typescript": "*"
       }
     },
-    "node_modules/@angular-eslint/utils": {
-      "version": "19.1.0",
-      "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.1.0.tgz",
-      "integrity": "sha512-mcb7hPMH/u6wwUwvsewrmgb9y9NWN6ZacvpUvKlTOxF/jOtTdsu0XfV4YB43sp2A8NWzYzX0Str4c8K1xSmuBQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@angular-eslint/bundled-angular-compiler": "19.1.0"
-      },
-      "peerDependencies": {
-        "@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
-        "eslint": "^8.57.0 || ^9.0.0",
-        "typescript": "*"
-      }
-    },
     "node_modules/@angular/cdk": {
       "version": "19.2.1",
       "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.1.tgz",
diff --git a/src-ui/package.json b/src-ui/package.json
index c6d5a6e0c..afcba2c8f 100644
--- a/src-ui/package.json
+++ b/src-ui/package.json
@@ -46,11 +46,11 @@
     "@angular-devkit/build-angular": "^19.0.4",
     "@angular-devkit/core": "^19.2.0",
     "@angular-devkit/schematics": "^19.2.0",
-    "@angular-eslint/builder": "19.1.0",
-    "@angular-eslint/eslint-plugin": "19.1.0",
-    "@angular-eslint/eslint-plugin-template": "19.1.0",
-    "@angular-eslint/schematics": "19.1.0",
-    "@angular-eslint/template-parser": "19.1.0",
+    "@angular-eslint/builder": "19.2.0",
+    "@angular-eslint/eslint-plugin": "19.2.0",
+    "@angular-eslint/eslint-plugin-template": "19.2.0",
+    "@angular-eslint/schematics": "19.2.0",
+    "@angular-eslint/template-parser": "19.2.0",
     "@angular/cli": "~19.2.0",
     "@angular/compiler-cli": "~19.2.0",
     "@codecov/webpack-plugin": "^1.9.0",
diff --git a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html
index 6e49c1763..ee27d298a 100644
--- a/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html
+++ b/src-ui/src/app/components/app-frame/toasts-dropdown/toasts-dropdown.component.html
@@ -21,7 +21,7 @@
     }
     <div class="scroll-list">
       @for (toast of toasts; track toast.id) {
-        <pngx-toast [autohide]="false" [toast]="toast" (hidden)="onHidden(toast)" (close)="toastService.closeToast(toast)"></pngx-toast>
+        <pngx-toast [autohide]="false" [toast]="toast" (hidden)="onHidden(toast)" (closed)="toastService.closeToast(toast)"></pngx-toast>
       }
       </div>
   </div>
diff --git a/src-ui/src/app/components/common/toast/toast.component.html b/src-ui/src/app/components/common/toast/toast.component.html
index fc8e85e9c..05ad7f832 100644
--- a/src-ui/src/app/components/common/toast/toast.component.html
+++ b/src-ui/src/app/components/common/toast/toast.component.html
@@ -51,6 +51,6 @@
             <p class="mb-0 mt-2"><button class="btn btn-sm btn-outline-secondary" (click)="close.emit(toast); toast.action()">{{toast.actionName}}</button></p>
             }
         </div>
-        <button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="close.emit(toast);"></button>
+        <button type="button" class="btn-close ms-auto flex-shrink-0" data-bs-dismiss="toast" aria-label="Close" (click)="closed.emit(toast);"></button>
     </div>
 </ngb-toast>
diff --git a/src-ui/src/app/components/common/toast/toast.component.ts b/src-ui/src/app/components/common/toast/toast.component.ts
index 5ebfdbe82..5ce027a42 100644
--- a/src-ui/src/app/components/common/toast/toast.component.ts
+++ b/src-ui/src/app/components/common/toast/toast.component.ts
@@ -27,7 +27,7 @@ export class ToastComponent {
 
   @Output() hidden: EventEmitter<Toast> = new EventEmitter<Toast>()
 
-  @Output() close: EventEmitter<Toast> = new EventEmitter<Toast>()
+  @Output() closed: EventEmitter<Toast> = new EventEmitter<Toast>()
 
   public copied: boolean = false
 
diff --git a/src-ui/src/app/components/common/toasts/toasts.component.html b/src-ui/src/app/components/common/toasts/toasts.component.html
index 2178a2023..3bd39d1ec 100644
--- a/src-ui/src/app/components/common/toasts/toasts.component.html
+++ b/src-ui/src/app/components/common/toasts/toasts.component.html
@@ -1,3 +1,3 @@
 @for (toast of toasts; track toast.id) {
-  <pngx-toast [toast]="toast" [autohide]="true" (close)="closeToast()"></pngx-toast>
+  <pngx-toast [toast]="toast" [autohide]="true" (closed)="closeToast()"></pngx-toast>
 }

From f205c4d0e25e380ef9b609867662b6b909e7979b Mon Sep 17 00:00:00 2001
From: Trenton H <797416+stumpylog@users.noreply.github.com>
Date: Tue, 4 Mar 2025 13:49:47 -0800
Subject: [PATCH 5/5] Removes undocumented FileInfo (#9298)

---
 src/documents/consumer.py            |  12 +--
 src/documents/models.py              |  89 -----------------
 src/documents/tests/test_consumer.py | 140 ---------------------------
 src/paperless/settings.py            |   6 --
 4 files changed, 5 insertions(+), 242 deletions(-)

diff --git a/src/documents/consumer.py b/src/documents/consumer.py
index 81739fa7a..4bf9ab89b 100644
--- a/src/documents/consumer.py
+++ b/src/documents/consumer.py
@@ -26,7 +26,6 @@ from documents.models import CustomField
 from documents.models import CustomFieldInstance
 from documents.models import Document
 from documents.models import DocumentType
-from documents.models import FileInfo
 from documents.models import StoragePath
 from documents.models import Tag
 from documents.models import WorkflowTrigger
@@ -705,8 +704,6 @@ class ConsumerPlugin(
     ) -> Document:
         # If someone gave us the original filename, use it instead of doc.
 
-        file_info = FileInfo.from_filename(self.filename)
-
         self.log.debug("Saving record to database")
 
         if self.metadata.created is not None:
@@ -714,9 +711,6 @@ class ConsumerPlugin(
             self.log.debug(
                 f"Creation date from post_documents parameter: {create_date}",
             )
-        elif file_info.created is not None:
-            create_date = file_info.created
-            self.log.debug(f"Creation date from FileInfo: {create_date}")
         elif date is not None:
             create_date = date
             self.log.debug(f"Creation date from parse_date: {create_date}")
@@ -729,7 +723,11 @@ class ConsumerPlugin(
 
         storage_type = Document.STORAGE_TYPE_UNENCRYPTED
 
-        title = file_info.title
+        if self.metadata.filename:
+            title = Path(self.metadata.filename).stem
+        else:
+            title = self.input_doc.original_file.stem
+
         if self.metadata.title is not None:
             try:
                 title = self._parse_title_placeholders(self.metadata.title)
diff --git a/src/documents/models.py b/src/documents/models.py
index 57ff96df1..e40ee8115 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -1,11 +1,7 @@
 import datetime
-import os
-import re
-from collections import OrderedDict
 from pathlib import Path
 from typing import Final
 
-import dateutil.parser
 import pathvalidate
 from celery import states
 from django.conf import settings
@@ -517,91 +513,6 @@ class SavedViewFilterRule(models.Model):
         return f"SavedViewFilterRule: {self.rule_type} : {self.value}"
 
 
-# TODO: why is this in the models file?
-# TODO: how about, what is this and where is it documented?
-# It appears to parsing JSON from an environment variable to get a title and date from
-# the filename, if possible, as a higher priority than either document filename or
-# content parsing
-class FileInfo:
-    REGEXES = OrderedDict(
-        [
-            (
-                "created-title",
-                re.compile(
-                    r"^(?P<created>\d{8}(\d{6})?Z) - (?P<title>.*)$",
-                    flags=re.IGNORECASE,
-                ),
-            ),
-            ("title", re.compile(r"(?P<title>.*)$", flags=re.IGNORECASE)),
-        ],
-    )
-
-    def __init__(
-        self,
-        created=None,
-        correspondent=None,
-        title=None,
-        tags=(),
-        extension=None,
-    ):
-        self.created = created
-        self.title = title
-        self.extension = extension
-        self.correspondent = correspondent
-        self.tags = tags
-
-    @classmethod
-    def _get_created(cls, created):
-        try:
-            return dateutil.parser.parse(f"{created[:-1]:0<14}Z")
-        except ValueError:
-            return None
-
-    @classmethod
-    def _get_title(cls, title):
-        return title
-
-    @classmethod
-    def _mangle_property(cls, properties, name):
-        if name in properties:
-            properties[name] = getattr(cls, f"_get_{name}")(properties[name])
-
-    @classmethod
-    def from_filename(cls, filename) -> "FileInfo":
-        # Mutate filename in-place before parsing its components
-        # by applying at most one of the configured transformations.
-        for pattern, repl in settings.FILENAME_PARSE_TRANSFORMS:
-            (filename, count) = pattern.subn(repl, filename)
-            if count:
-                break
-
-        # do this after the transforms so that the transforms can do whatever
-        # with the file extension.
-        filename_no_ext = os.path.splitext(filename)[0]
-
-        if filename_no_ext == filename and filename.startswith("."):
-            # This is a very special case where there is no text before the
-            # file type.
-            # TODO: this should be handled better. The ext is not removed
-            #  because usually, files like '.pdf' are just hidden files
-            #  with the name pdf, but in our case, its more likely that
-            #  there's just no name to begin with.
-            filename = ""
-            # This isn't too bad either, since we'll just not match anything
-            # and return an empty title. TODO: actually, this is kinda bad.
-        else:
-            filename = filename_no_ext
-
-        # Parse filename components.
-        for regex in cls.REGEXES.values():
-            m = regex.match(filename)
-            if m:
-                properties = m.groupdict()
-                cls._mangle_property(properties, "created")
-                cls._mangle_property(properties, "title")
-                return cls(**properties)
-
-
 # Extending User Model Using a One-To-One Link
 class UiSettings(models.Model):
     user = models.OneToOneField(
diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py
index 6f576ab24..ff684804e 100644
--- a/src/documents/tests/test_consumer.py
+++ b/src/documents/tests/test_consumer.py
@@ -1,12 +1,10 @@
 import datetime
 import os
-import re
 import shutil
 import stat
 import tempfile
 import zoneinfo
 from pathlib import Path
-from unittest import TestCase as UnittestTestCase
 from unittest import mock
 from unittest.mock import MagicMock
 
@@ -26,7 +24,6 @@ from documents.models import Correspondent
 from documents.models import CustomField
 from documents.models import Document
 from documents.models import DocumentType
-from documents.models import FileInfo
 from documents.models import StoragePath
 from documents.models import Tag
 from documents.parsers import DocumentParser
@@ -40,143 +37,6 @@ from paperless_mail.models import MailRule
 from paperless_mail.parsers import MailDocumentParser
 
 
-class TestAttributes(UnittestTestCase):
-    TAGS = ("tag1", "tag2", "tag3")
-
-    def _test_guess_attributes_from_name(self, filename, sender, title, tags):
-        file_info = FileInfo.from_filename(filename)
-
-        if sender:
-            self.assertEqual(file_info.correspondent.name, sender, filename)
-        else:
-            self.assertIsNone(file_info.correspondent, filename)
-
-        self.assertEqual(file_info.title, title, filename)
-
-        self.assertEqual(tuple(t.name for t in file_info.tags), tags, filename)
-
-    def test_guess_attributes_from_name_when_title_starts_with_dash(self):
-        self._test_guess_attributes_from_name(
-            "- weird but should not break.pdf",
-            None,
-            "- weird but should not break",
-            (),
-        )
-
-    def test_guess_attributes_from_name_when_title_ends_with_dash(self):
-        self._test_guess_attributes_from_name(
-            "weird but should not break -.pdf",
-            None,
-            "weird but should not break -",
-            (),
-        )
-
-
-class TestFieldPermutations(TestCase):
-    valid_dates = (
-        "20150102030405Z",
-        "20150102Z",
-    )
-    valid_correspondents = ["timmy", "Dr. McWheelie", "Dash Gor-don", "o Θεpμaoτής", ""]
-    valid_titles = ["title", "Title w Spaces", "Title a-dash", "Tίτλoς", ""]
-    valid_tags = ["tag", "tig,tag", "tag1,tag2,tag-3"]
-
-    def _test_guessed_attributes(
-        self,
-        filename,
-        created=None,
-        correspondent=None,
-        title=None,
-        tags=None,
-    ):
-        info = FileInfo.from_filename(filename)
-
-        # Created
-        if created is None:
-            self.assertIsNone(info.created, filename)
-        else:
-            self.assertEqual(info.created.year, int(created[:4]), filename)
-            self.assertEqual(info.created.month, int(created[4:6]), filename)
-            self.assertEqual(info.created.day, int(created[6:8]), filename)
-
-        # Correspondent
-        if correspondent:
-            self.assertEqual(info.correspondent.name, correspondent, filename)
-        else:
-            self.assertEqual(info.correspondent, None, filename)
-
-        # Title
-        self.assertEqual(info.title, title, filename)
-
-        # Tags
-        if tags is None:
-            self.assertEqual(info.tags, (), filename)
-        else:
-            self.assertEqual([t.name for t in info.tags], tags.split(","), filename)
-
-    def test_just_title(self):
-        template = "{title}.pdf"
-        for title in self.valid_titles:
-            spec = dict(title=title)
-            filename = template.format(**spec)
-            self._test_guessed_attributes(filename, **spec)
-
-    def test_created_and_title(self):
-        template = "{created} - {title}.pdf"
-
-        for created in self.valid_dates:
-            for title in self.valid_titles:
-                spec = {"created": created, "title": title}
-                self._test_guessed_attributes(template.format(**spec), **spec)
-
-    def test_invalid_date_format(self):
-        info = FileInfo.from_filename("06112017Z - title.pdf")
-        self.assertEqual(info.title, "title")
-        self.assertIsNone(info.created)
-
-    def test_filename_parse_transforms(self):
-        filename = "tag1,tag2_20190908_180610_0001.pdf"
-        all_patt = re.compile("^.*$")
-        none_patt = re.compile("$a")
-        re.compile("^([a-z0-9,]+)_(\\d{8})_(\\d{6})_([0-9]+)\\.")
-
-        # No transformations configured (= default)
-        info = FileInfo.from_filename(filename)
-        self.assertEqual(info.title, "tag1,tag2_20190908_180610_0001")
-        self.assertEqual(info.tags, ())
-        self.assertIsNone(info.created)
-
-        # Pattern doesn't match (filename unaltered)
-        with self.settings(FILENAME_PARSE_TRANSFORMS=[(none_patt, "none.gif")]):
-            info = FileInfo.from_filename(filename)
-            self.assertEqual(info.title, "tag1,tag2_20190908_180610_0001")
-
-        # Simple transformation (match all)
-        with self.settings(FILENAME_PARSE_TRANSFORMS=[(all_patt, "all.gif")]):
-            info = FileInfo.from_filename(filename)
-            self.assertEqual(info.title, "all")
-
-        # Multiple transformations configured (first pattern matches)
-        with self.settings(
-            FILENAME_PARSE_TRANSFORMS=[
-                (all_patt, "all.gif"),
-                (all_patt, "anotherall.gif"),
-            ],
-        ):
-            info = FileInfo.from_filename(filename)
-            self.assertEqual(info.title, "all")
-
-        # Multiple transformations configured (second pattern matches)
-        with self.settings(
-            FILENAME_PARSE_TRANSFORMS=[
-                (none_patt, "none.gif"),
-                (all_patt, "anotherall.gif"),
-            ],
-        ):
-            info = FileInfo.from_filename(filename)
-            self.assertEqual(info.title, "anotherall")
-
-
 class _BaseTestParser(DocumentParser):
     def get_settings(self):
         """
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
index 0c8c71ab9..ff1829528 100644
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -3,7 +3,6 @@ import json
 import math
 import multiprocessing
 import os
-import re
 import tempfile
 from os import PathLike
 from pathlib import Path
@@ -1089,11 +1088,6 @@ FILENAME_DATE_ORDER = os.getenv("PAPERLESS_FILENAME_DATE_ORDER")
 # fewer dates shown.
 NUMBER_OF_SUGGESTED_DATES = __get_int("PAPERLESS_NUMBER_OF_SUGGESTED_DATES", 3)
 
-# Transformations applied before filename parsing
-FILENAME_PARSE_TRANSFORMS = []
-for t in json.loads(os.getenv("PAPERLESS_FILENAME_PARSE_TRANSFORMS", "[]")):
-    FILENAME_PARSE_TRANSFORMS.append((re.compile(t["pattern"]), t["repl"]))
-
 # Specify the filename format for out files
 FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT")