diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f18814589..60c84162e 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 d1db87e70..d26d05aa3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -336,6 +336,8 @@ addopts = [
"--maxprocesses=16",
"--quiet",
"--durations=50",
+ "--junitxml=junit.xml",
+ "-o junit_family=legacy",
]
norecursedirs = [ "src/locale/", ".venv/", "src-ui/" ]
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 @@
}
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 @@
}
-
+
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 = new EventEmitter()
- @Output() close: EventEmitter = new EventEmitter()
+ @Output() closed: EventEmitter = new EventEmitter()
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) {
-
+
}
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/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..e40ee8115 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -1,12 +1,7 @@
import datetime
-import logging
-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
@@ -379,36 +374,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"))
@@ -548,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\d{8}(\d{6})?Z) - (?P.*)$",
- flags=re.IGNORECASE,
- ),
- ),
- ("title", re.compile(r"(?P.*)$", 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/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_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/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,
)
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")