mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-09 09:58:20 -05:00
Merge branch 'dev' into feature-use-extras
This commit is contained in:
commit
14857563d9
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -167,7 +167,9 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: backend-coverage-report
|
name: backend-coverage-report
|
||||||
path: coverage.xml
|
path: |
|
||||||
|
coverage.xml
|
||||||
|
junit.xml
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
-
|
-
|
||||||
@ -315,6 +317,14 @@ jobs:
|
|||||||
# future expansion
|
# future expansion
|
||||||
flags: backend
|
flags: backend
|
||||||
directory: src/
|
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
|
name: Use Node.js 20
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
@ -336,6 +336,8 @@ addopts = [
|
|||||||
"--maxprocesses=16",
|
"--maxprocesses=16",
|
||||||
"--quiet",
|
"--quiet",
|
||||||
"--durations=50",
|
"--durations=50",
|
||||||
|
"--junitxml=junit.xml",
|
||||||
|
"-o junit_family=legacy",
|
||||||
]
|
]
|
||||||
norecursedirs = [ "src/locale/", ".venv/", "src-ui/" ]
|
norecursedirs = [ "src/locale/", ".venv/", "src-ui/" ]
|
||||||
|
|
||||||
|
105
src-ui/package-lock.json
generated
105
src-ui/package-lock.json
generated
@ -44,11 +44,11 @@
|
|||||||
"@angular-devkit/build-angular": "^19.0.4",
|
"@angular-devkit/build-angular": "^19.0.4",
|
||||||
"@angular-devkit/core": "^19.2.0",
|
"@angular-devkit/core": "^19.2.0",
|
||||||
"@angular-devkit/schematics": "^19.2.0",
|
"@angular-devkit/schematics": "^19.2.0",
|
||||||
"@angular-eslint/builder": "19.1.0",
|
"@angular-eslint/builder": "19.2.0",
|
||||||
"@angular-eslint/eslint-plugin": "19.1.0",
|
"@angular-eslint/eslint-plugin": "19.2.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "19.1.0",
|
"@angular-eslint/eslint-plugin-template": "19.2.0",
|
||||||
"@angular-eslint/schematics": "19.1.0",
|
"@angular-eslint/schematics": "19.2.0",
|
||||||
"@angular-eslint/template-parser": "19.1.0",
|
"@angular-eslint/template-parser": "19.2.0",
|
||||||
"@angular/cli": "~19.2.0",
|
"@angular/cli": "~19.2.0",
|
||||||
"@angular/compiler-cli": "~19.2.0",
|
"@angular/compiler-cli": "~19.2.0",
|
||||||
"@codecov/webpack-plugin": "^1.9.0",
|
"@codecov/webpack-plugin": "^1.9.0",
|
||||||
@ -1148,9 +1148,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-eslint/builder": {
|
"node_modules/@angular-eslint/builder": {
|
||||||
"version": "19.1.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.2.0.tgz",
|
||||||
"integrity": "sha512-LWdQMTES/7GySlpTNFJn3k33ZGmjjWlHI/+IHV7B3xHQ9hj4MPK4ACmE/PNOAIQ9LwQm7sKS+3cTMxOZQ/cvSg==",
|
"integrity": "sha512-8Lx24MrMJT8RlgDtwqfiLiJo4DzSaktjco6RmELUdWO2chJgRe9y+2iIgOeB2pmyD9UCsubwsfjBXlrnV/MPhQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1163,21 +1163,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-eslint/bundled-angular-compiler": {
|
"node_modules/@angular-eslint/bundled-angular-compiler": {
|
||||||
"version": "19.1.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.2.0.tgz",
|
||||||
"integrity": "sha512-HUJyukRvnh8Z9lIdxdblBRuBaPYEVv4iAYZMw3d+dn4rrM27Nt5oh3/zkwYrrPkt36tZdeXdDWrOuz9jgjVN5w==",
|
"integrity": "sha512-hmmAogTpYGbBvnJ0j7DNLi8YQ+YEEuwFdx0heU8XjTpZlRoSRIP7MJJVlaQCt+ZT5f5XwdGtqi9lOXqqcyGHLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@angular-eslint/eslint-plugin": {
|
"node_modules/@angular-eslint/eslint-plugin": {
|
||||||
"version": "19.1.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.2.0.tgz",
|
||||||
"integrity": "sha512-TDO0+Ry+oNkxnaLHogKp1k2aey6IkJef5d7hathE4UFT6owjRizltWaRoX6bGw7Qu1yagVLL8L2Se8SddxSPAQ==",
|
"integrity": "sha512-QQWWDrTdJ22tBd7RLFG/FdPwNyYEhg7YwWgn29z6XcdnV00ZFtf7FRbv/te1kqVNPvfjtht7bvtHcPQ432aUdQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-eslint/bundled-angular-compiler": "19.1.0",
|
"@angular-eslint/bundled-angular-compiler": "19.2.0",
|
||||||
"@angular-eslint/utils": "19.1.0"
|
"@angular-eslint/utils": "19.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
|
"@typescript-eslint/utils": "^7.11.0 || ^8.0.0",
|
||||||
@ -1186,14 +1186,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-eslint/eslint-plugin-template": {
|
"node_modules/@angular-eslint/eslint-plugin-template": {
|
||||||
"version": "19.1.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.2.0.tgz",
|
||||||
"integrity": "sha512-bIUizkCY40mnU8oAO1tLV7uN2H/cHf1evLlhpqlb9JYwc5dT2moiEhNDo61OtOgkJmDGNuThAeO9Xk9hGQc7nA==",
|
"integrity": "sha512-lUSzmk5/Dr0bNc2Omb5CZDu3zQZh70bJyuXnN5MKd00V1b3u90eqvMSveFzWFJ6Eot8Hh8+FxtiozPwGqOE+Og==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-eslint/bundled-angular-compiler": "19.1.0",
|
"@angular-eslint/bundled-angular-compiler": "19.2.0",
|
||||||
"@angular-eslint/utils": "19.1.0",
|
"@angular-eslint/utils": "19.2.0",
|
||||||
"aria-query": "5.3.2",
|
"aria-query": "5.3.2",
|
||||||
"axobject-query": "4.1.0"
|
"axobject-query": "4.1.0"
|
||||||
},
|
},
|
||||||
@ -1204,17 +1204,47 @@
|
|||||||
"typescript": "*"
|
"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": {
|
"node_modules/@angular-eslint/schematics": {
|
||||||
"version": "19.1.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.2.0.tgz",
|
||||||
"integrity": "sha512-6S1FjmM7rZxc0u0W0KjqWYOkFQ0q89IGyjPkdUt1a8NwRnWg3VoXp4WYfeuZOjda/FEYuBS/E6rckLAMp0h6Aw==",
|
"integrity": "sha512-SQfbKgPEJNkK5TVXRsdnWp6TjvVZOczvf8lELF1n+I/Uwmp7ulUjTRgTo59ZQnXoPSs2qCPgS4gAOVR6CD91zQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/core": ">= 19.0.0 < 20.0.0",
|
"@angular-devkit/core": ">= 19.0.0 < 20.0.0",
|
||||||
"@angular-devkit/schematics": ">= 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": "19.2.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "19.1.0",
|
"@angular-eslint/eslint-plugin-template": "19.2.0",
|
||||||
"ignore": "7.0.3",
|
"ignore": "7.0.3",
|
||||||
"semver": "7.7.1",
|
"semver": "7.7.1",
|
||||||
"strip-json-comments": "3.1.1"
|
"strip-json-comments": "3.1.1"
|
||||||
@ -1231,13 +1261,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@angular-eslint/template-parser": {
|
"node_modules/@angular-eslint/template-parser": {
|
||||||
"version": "19.1.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.2.0.tgz",
|
||||||
"integrity": "sha512-wbMi7adlC+uYqZo7NHNBShpNhFJRZsXLqihqvFpAUt1Ei6uDX8HR6MyMEDZ9tUnlqtPVW5nmbedPyLVG7HkjAA==",
|
"integrity": "sha512-VqgvFrILhoMe0GHZrx+Bjy8kx7/LJfJTd+x/wzE/X1cCChSU81MBZFMVeFMnoI75OOQUf4fwaaKrtUhUvAkVyw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-eslint/bundled-angular-compiler": "19.1.0",
|
"@angular-eslint/bundled-angular-compiler": "19.2.0",
|
||||||
"eslint-scope": "^8.0.2"
|
"eslint-scope": "^8.0.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -1245,21 +1275,6 @@
|
|||||||
"typescript": "*"
|
"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": {
|
"node_modules/@angular/cdk": {
|
||||||
"version": "19.2.1",
|
"version": "19.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.1.tgz",
|
||||||
|
@ -46,11 +46,11 @@
|
|||||||
"@angular-devkit/build-angular": "^19.0.4",
|
"@angular-devkit/build-angular": "^19.0.4",
|
||||||
"@angular-devkit/core": "^19.2.0",
|
"@angular-devkit/core": "^19.2.0",
|
||||||
"@angular-devkit/schematics": "^19.2.0",
|
"@angular-devkit/schematics": "^19.2.0",
|
||||||
"@angular-eslint/builder": "19.1.0",
|
"@angular-eslint/builder": "19.2.0",
|
||||||
"@angular-eslint/eslint-plugin": "19.1.0",
|
"@angular-eslint/eslint-plugin": "19.2.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "19.1.0",
|
"@angular-eslint/eslint-plugin-template": "19.2.0",
|
||||||
"@angular-eslint/schematics": "19.1.0",
|
"@angular-eslint/schematics": "19.2.0",
|
||||||
"@angular-eslint/template-parser": "19.1.0",
|
"@angular-eslint/template-parser": "19.2.0",
|
||||||
"@angular/cli": "~19.2.0",
|
"@angular/cli": "~19.2.0",
|
||||||
"@angular/compiler-cli": "~19.2.0",
|
"@angular/compiler-cli": "~19.2.0",
|
||||||
"@codecov/webpack-plugin": "^1.9.0",
|
"@codecov/webpack-plugin": "^1.9.0",
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
<div class="scroll-list">
|
<div class="scroll-list">
|
||||||
@for (toast of toasts; track toast.id) {
|
@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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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>
|
<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>
|
</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>
|
</div>
|
||||||
</ngb-toast>
|
</ngb-toast>
|
||||||
|
@ -27,7 +27,7 @@ export class ToastComponent {
|
|||||||
|
|
||||||
@Output() hidden: EventEmitter<Toast> = new EventEmitter<Toast>()
|
@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
|
public copied: boolean = false
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
@for (toast of toasts; track toast.id) {
|
@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>
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,6 @@ from documents.models import CustomField
|
|||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import FileInfo
|
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
@ -705,8 +704,6 @@ class ConsumerPlugin(
|
|||||||
) -> Document:
|
) -> Document:
|
||||||
# If someone gave us the original filename, use it instead of doc.
|
# 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")
|
self.log.debug("Saving record to database")
|
||||||
|
|
||||||
if self.metadata.created is not None:
|
if self.metadata.created is not None:
|
||||||
@ -714,9 +711,6 @@ class ConsumerPlugin(
|
|||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"Creation date from post_documents parameter: {create_date}",
|
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:
|
elif date is not None:
|
||||||
create_date = date
|
create_date = date
|
||||||
self.log.debug(f"Creation date from parse_date: {create_date}")
|
self.log.debug(f"Creation date from parse_date: {create_date}")
|
||||||
@ -729,7 +723,11 @@ class ConsumerPlugin(
|
|||||||
|
|
||||||
storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
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:
|
if self.metadata.title is not None:
|
||||||
try:
|
try:
|
||||||
title = self._parse_title_placeholders(self.metadata.title)
|
title = self._parse_title_placeholders(self.metadata.title)
|
||||||
|
@ -36,7 +36,6 @@ from documents.models import CustomField
|
|||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import Log
|
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.models import ShareLink
|
from documents.models import ShareLink
|
||||||
from documents.models import StoragePath
|
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 ShareLinkFilterSet(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ShareLink
|
model = ShareLink
|
||||||
|
15
src/documents/migrations/1064_delete_log.py
Normal file
15
src/documents/migrations/1064_delete_log.py
Normal file
@ -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",
|
||||||
|
),
|
||||||
|
]
|
@ -1,12 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from collections import OrderedDict
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
import dateutil.parser
|
|
||||||
import pathvalidate
|
import pathvalidate
|
||||||
from celery import states
|
from celery import states
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -379,36 +374,6 @@ class Document(SoftDeleteModel, ModelWithOwner):
|
|||||||
return timezone.localdate(self.created)
|
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 SavedView(ModelWithOwner):
|
||||||
class DisplayMode(models.TextChoices):
|
class DisplayMode(models.TextChoices):
|
||||||
TABLE = ("table", _("Table"))
|
TABLE = ("table", _("Table"))
|
||||||
@ -548,91 +513,6 @@ class SavedViewFilterRule(models.Model):
|
|||||||
return f"SavedViewFilterRule: {self.rule_type} : {self.value}"
|
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
|
# Extending User Model Using a One-To-One Link
|
||||||
class UiSettings(models.Model):
|
class UiSettings(models.Model):
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
|
@ -632,7 +632,7 @@ def send_webhook(
|
|||||||
else:
|
else:
|
||||||
httpx.post(
|
httpx.post(
|
||||||
url,
|
url,
|
||||||
data=data,
|
content=data,
|
||||||
files=files,
|
files=files,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
).raise_for_status()
|
).raise_for_status()
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import tempfile
|
import tempfile
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import TestCase as UnittestTestCase
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@ -26,7 +24,6 @@ from documents.models import Correspondent
|
|||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import FileInfo
|
|
||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.parsers import DocumentParser
|
from documents.parsers import DocumentParser
|
||||||
@ -40,143 +37,6 @@ from paperless_mail.models import MailRule
|
|||||||
from paperless_mail.parsers import MailDocumentParser
|
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):
|
class _BaseTestParser(DocumentParser):
|
||||||
def get_settings(self):
|
def get_settings(self):
|
||||||
"""
|
"""
|
||||||
|
@ -2603,7 +2603,7 @@ class TestWorkflows(
|
|||||||
|
|
||||||
mock_post.assert_called_once_with(
|
mock_post.assert_called_once_with(
|
||||||
"http://paperless-ngx.com",
|
"http://paperless-ngx.com",
|
||||||
data="Test message",
|
content="Test message",
|
||||||
headers={},
|
headers={},
|
||||||
files=None,
|
files=None,
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,6 @@ import json
|
|||||||
import math
|
import math
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -1089,11 +1088,6 @@ FILENAME_DATE_ORDER = os.getenv("PAPERLESS_FILENAME_DATE_ORDER")
|
|||||||
# fewer dates shown.
|
# fewer dates shown.
|
||||||
NUMBER_OF_SUGGESTED_DATES = __get_int("PAPERLESS_NUMBER_OF_SUGGESTED_DATES", 3)
|
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
|
# Specify the filename format for out files
|
||||||
FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT")
|
FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user