Compare commits

..
168 changed files with 33692 additions and 46091 deletions

No files matched your search

-86
View File
@@ -1,91 +1,5 @@
# Changelog
## paperless-ngx 3.4.0
### Features / Enhancements
- Enhancement: support custom fields in workflow title templates [@shamoon](https://github.com/shamoon) ([#14564](https://github.com/paperless-ngx/paperless-ngx/pull/14564))
- Enhancement: relative date custom field queries [@shamoon](https://github.com/shamoon) ([#14549](https://github.com/paperless-ngx/paperless-ngx/pull/14549))
- Performance: reduce tasks badge load times [@JanKleine](https://github.com/JanKleine) ([#14552](https://github.com/paperless-ngx/paperless-ngx/pull/14552))
- Enhancement: skip local 2FA for trusted OIDC AMR [@shamoon](https://github.com/shamoon) ([#14403](https://github.com/paperless-ngx/paperless-ngx/pull/14403))
- Feature: Generate PDF thumbnails via poppler utils [@stumpylog](https://github.com/stumpylog) ([#14420](https://github.com/paperless-ngx/paperless-ngx/pull/14420))
- Enhancement: add upload count to button, link to tray, reduce notifications [@shamoon](https://github.com/shamoon) ([#14501](https://github.com/paperless-ngx/paperless-ngx/pull/14501))
- Enhancement: notifications improvements [@shamoon](https://github.com/shamoon) ([#14500](https://github.com/paperless-ngx/paperless-ngx/pull/14500))
- Enhancement: doc details ux improvements [@shamoon](https://github.com/shamoon) ([#14485](https://github.com/paperless-ngx/paperless-ngx/pull/14485))
- Enhancement (QoL): allow independently toggling child + parent tags in WF tag fields [@shamoon](https://github.com/shamoon) ([#14400](https://github.com/paperless-ngx/paperless-ngx/pull/14400))
- Tweak: drop the user dropdown caret [@shamoon](https://github.com/shamoon) ([#14444](https://github.com/paperless-ngx/paperless-ngx/pull/14444))
- Enhancement: navbar upload button [@shamoon](https://github.com/shamoon) ([#14442](https://github.com/paperless-ngx/paperless-ngx/pull/14442))
- Enhancement: AI chat UX tweaks [@shamoon](https://github.com/shamoon) ([#14415](https://github.com/paperless-ngx/paperless-ngx/pull/14415))
- Enhancement: support markdown in AI chat [@shamoon](https://github.com/shamoon) ([#14409](https://github.com/paperless-ngx/paperless-ngx/pull/14409))
### Bug Fixes
- Fix: truncate long version labels in dropdown [@shamoon](https://github.com/shamoon) ([#14578](https://github.com/paperless-ngx/paperless-ngx/pull/14578))
- Fixhancement: show better result for revoked / expired tasks [@shamoon](https://github.com/shamoon) ([#14566](https://github.com/paperless-ngx/paperless-ngx/pull/14566))
- Fix: move re-index after workflow runs on doc edit [@shamoon](https://github.com/shamoon) ([#14558](https://github.com/paperless-ngx/paperless-ngx/pull/14558))
- Fix: folder export with --delete fails on a stale manifest.json.tmp [@benpetty](https://github.com/benpetty) ([#14526](https://github.com/paperless-ngx/paperless-ngx/pull/14526))
- Fix: fix list type for api schema document object filters [@shamoon](https://github.com/shamoon) ([#14524](https://github.com/paperless-ngx/paperless-ngx/pull/14524))
- Fix: standardize some dropdown sizing [@shamoon](https://github.com/shamoon) ([#14510](https://github.com/paperless-ngx/paperless-ngx/pull/14510))
- Fix: prevent doc permissions queries on schema view [@shamoon](https://github.com/shamoon) ([#14503](https://github.com/paperless-ngx/paperless-ngx/pull/14503))
- Fix: dont defer tag assignment when barcode will not result in a split [@shamoon](https://github.com/shamoon) ([#14497](https://github.com/paperless-ngx/paperless-ngx/pull/14497))
### Dependencies
<details>
<summary>6 changes</summary>
- Chore: bump some frontend deps for security fixes [@shamoon](https://github.com/shamoon) ([#14581](https://github.com/paperless-ngx/paperless-ngx/pull/14581))
- Chore: bump brotli, djangorestframework, filelock, msgpack, setuptools for security fixes [@stumpylog](https://github.com/stumpylog) ([#14575](https://github.com/paperless-ngx/paperless-ngx/pull/14575))
- Chore: bump django-allauth to 65.20 and oauthlib to 4.0 [@stumpylog](https://github.com/stumpylog) ([#14570](https://github.com/paperless-ngx/paperless-ngx/pull/14570))
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14517](https://github.com/paperless-ngx/paperless-ngx/pull/14517))
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14493](https://github.com/paperless-ngx/paperless-ngx/pull/14493))
- Chore: Drop ImageMagik as a dependency [@stumpylog](https://github.com/stumpylog) ([#14440](https://github.com/paperless-ngx/paperless-ngx/pull/14440))
</details>
### All App Changes
<details>
<summary>35 changes</summary>
- Chore: Consolidate test sample files [@stumpylog](https://github.com/stumpylog) ([#14430](https://github.com/paperless-ngx/paperless-ngx/pull/14430))
- Chore: bump some frontend deps for security fixes [@shamoon](https://github.com/shamoon) ([#14581](https://github.com/paperless-ngx/paperless-ngx/pull/14581))
- Enhancement: support custom fields in workflow title templates [@shamoon](https://github.com/shamoon) ([#14564](https://github.com/paperless-ngx/paperless-ngx/pull/14564))
- Enhancement: relative date custom field queries [@shamoon](https://github.com/shamoon) ([#14549](https://github.com/paperless-ngx/paperless-ngx/pull/14549))
- Fix: truncate long version labels in dropdown [@shamoon](https://github.com/shamoon) ([#14578](https://github.com/paperless-ngx/paperless-ngx/pull/14578))
- Chore: bump brotli, djangorestframework, filelock, msgpack, setuptools for security fixes [@stumpylog](https://github.com/stumpylog) ([#14575](https://github.com/paperless-ngx/paperless-ngx/pull/14575))
- Chore: bump django-allauth to 65.20 and oauthlib to 4.0 [@stumpylog](https://github.com/stumpylog) ([#14570](https://github.com/paperless-ngx/paperless-ngx/pull/14570))
- Fixhancement: show better result for revoked / expired tasks [@shamoon](https://github.com/shamoon) ([#14566](https://github.com/paperless-ngx/paperless-ngx/pull/14566))
- Fix: move re-index after workflow runs on doc edit [@shamoon](https://github.com/shamoon) ([#14558](https://github.com/paperless-ngx/paperless-ngx/pull/14558))
- Performance: reduce tasks badge load times [@JanKleine](https://github.com/JanKleine) ([#14552](https://github.com/paperless-ngx/paperless-ngx/pull/14552))
- Enhancement: skip local 2FA for trusted OIDC AMR [@shamoon](https://github.com/shamoon) ([#14403](https://github.com/paperless-ngx/paperless-ngx/pull/14403))
- Chore: consolidate direct permission grant lookups into one helper [@stumpylog](https://github.com/stumpylog) ([#14456](https://github.com/paperless-ngx/paperless-ngx/pull/14456))
- Chore: drop get\_groups\_with\_only\_permission [@stumpylog](https://github.com/stumpylog) ([#14454](https://github.com/paperless-ngx/paperless-ngx/pull/14454))
- Chore: Consolidate to a single way to set permission on an object [@stumpylog](https://github.com/stumpylog) ([#14453](https://github.com/paperless-ngx/paperless-ngx/pull/14453))
- Chore: Refactor bulk edit to remove PDF knowledge [@stumpylog](https://github.com/stumpylog) ([#14445](https://github.com/paperless-ngx/paperless-ngx/pull/14445))
- Fix: folder export with --delete fails on a stale manifest.json.tmp [@benpetty](https://github.com/benpetty) ([#14526](https://github.com/paperless-ngx/paperless-ngx/pull/14526))
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14517](https://github.com/paperless-ngx/paperless-ngx/pull/14517))
- Fix: fix list type for api schema document object filters [@shamoon](https://github.com/shamoon) ([#14524](https://github.com/paperless-ngx/paperless-ngx/pull/14524))
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14493](https://github.com/paperless-ngx/paperless-ngx/pull/14493))
- Chore: drop get\_objects\_for\_user\_owner\_aware [@stumpylog](https://github.com/stumpylog) ([#14457](https://github.com/paperless-ngx/paperless-ngx/pull/14457))
- Chore: update Angular components to 22.2.2 [@shamoon](https://github.com/shamoon) ([#14515](https://github.com/paperless-ngx/paperless-ngx/pull/14515))
- Chore: share one base class for the method-mapped permission classes [@stumpylog](https://github.com/stumpylog) ([#14458](https://github.com/paperless-ngx/paperless-ngx/pull/14458))
- Fix: standardize some dropdown sizing [@shamoon](https://github.com/shamoon) ([#14510](https://github.com/paperless-ngx/paperless-ngx/pull/14510))
- Chore: Drop ImageMagik as a dependency [@stumpylog](https://github.com/stumpylog) ([#14440](https://github.com/paperless-ngx/paperless-ngx/pull/14440))
- Feature: Generate PDF thumbnails via poppler utils [@stumpylog](https://github.com/stumpylog) ([#14420](https://github.com/paperless-ngx/paperless-ngx/pull/14420))
- Fix: prevent doc permissions queries on schema view [@shamoon](https://github.com/shamoon) ([#14503](https://github.com/paperless-ngx/paperless-ngx/pull/14503))
- Enhancement: add upload count to button, link to tray, reduce notifications [@shamoon](https://github.com/shamoon) ([#14501](https://github.com/paperless-ngx/paperless-ngx/pull/14501))
- Enhancement: notifications improvements [@shamoon](https://github.com/shamoon) ([#14500](https://github.com/paperless-ngx/paperless-ngx/pull/14500))
- Enhancement: doc details ux improvements [@shamoon](https://github.com/shamoon) ([#14485](https://github.com/paperless-ngx/paperless-ngx/pull/14485))
- Enhancement (QoL): allow independently toggling child + parent tags in WF tag fields [@shamoon](https://github.com/shamoon) ([#14400](https://github.com/paperless-ngx/paperless-ngx/pull/14400))
- Tweak: drop the user dropdown caret [@shamoon](https://github.com/shamoon) ([#14444](https://github.com/paperless-ngx/paperless-ngx/pull/14444))
- Enhancement: navbar upload button [@shamoon](https://github.com/shamoon) ([#14442](https://github.com/paperless-ngx/paperless-ngx/pull/14442))
- Enhancement: AI chat UX tweaks [@shamoon](https://github.com/shamoon) ([#14415](https://github.com/paperless-ngx/paperless-ngx/pull/14415))
- Enhancement: support markdown in AI chat [@shamoon](https://github.com/shamoon) ([#14409](https://github.com/paperless-ngx/paperless-ngx/pull/14409))
- Fix: dont defer tag assignment when barcode will not result in a split [@shamoon](https://github.com/shamoon) ([#14497](https://github.com/paperless-ngx/paperless-ngx/pull/14497))
</details>
## paperless-ngx 3.3.4
### All App Changes
+3 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "3.4.0"
version = "3.3.4"
description = """\
A community-supported supercharged document management system: scan, index and archive all your physical documents\
"""
@@ -256,8 +256,8 @@ isort.force-single-line = true
[tool.codespell]
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish,NIN,nin,reprot"
skip = """\
src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless/tests/samples/mail/*,src/documents/tests/samples\
/*,src/paperless_testing/sample_files/*,*.po,*.json\
src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/paperless/tests/samples\
/mail/*,src/documents/tests/samples/*,*.po,*.json\
"""
write-changes = true
+1 -3
View File
@@ -134,9 +134,7 @@ def seed_database() -> None:
originals.mkdir(parents=True)
thumbnails = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "thumbnails"
thumbnails.mkdir(parents=True)
sample_pdf = (
SOURCE_ROOT / "paperless_testing" / "sample_files" / "simple-digital.pdf"
)
sample_pdf = SOURCE_ROOT / "documents" / "tests" / "samples" / "simple.pdf"
for document in documents:
shutil.copyfile(sample_pdf, originals / document.filename)
shutil.copyfile(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
"version": "3.4.0",
"version": "3.3.4",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
+35 -228
View File
@@ -4,10 +4,6 @@ settings:
autoInstallPeers: false
excludeLinksFromLockfile: false
overrides:
vite@>=8.3.0 <8.3.3: ^8.3.3
zod@<4.6.6: ^4.6.6
importers:
.:
@@ -1654,9 +1650,6 @@ packages:
'@oxc-project/types@0.150.0':
resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
'@oxc-project/types@0.155.0':
resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==}
'@parcel/watcher-android-arm64@2.6.0':
resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==}
engines: {node: '>= 10.0.0'}
@@ -1755,85 +1748,42 @@ packages:
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@rolldown/binding-android-arm-eabi@1.2.15':
resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@rolldown/binding-android-arm-eabi@1.2.8':
resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@rolldown/binding-android-arm64@1.2.15':
resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-android-arm64@1.2.8':
resolution: {integrity: sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.2.15':
resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-arm64@1.2.8':
resolution: {integrity: sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.2.15':
resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.2.8':
resolution: {integrity: sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.2.15':
resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-freebsd-x64@1.2.8':
resolution: {integrity: sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.2.15':
resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm-gnueabihf@1.2.8':
resolution: {integrity: sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.2.15':
resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-gnu@1.2.8':
resolution: {integrity: sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1841,13 +1791,6 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.2.15':
resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-arm64-musl@1.2.8':
resolution: {integrity: sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1855,13 +1798,6 @@ packages:
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.2.15':
resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-ppc64-gnu@1.2.8':
resolution: {integrity: sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1869,13 +1805,6 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.2.15':
resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.2.8':
resolution: {integrity: sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1883,13 +1812,6 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.2.15':
resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.2.8':
resolution: {integrity: sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1897,13 +1819,6 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.2.15':
resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-musl@1.2.8':
resolution: {integrity: sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1911,36 +1826,18 @@ packages:
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.2.15':
resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-openharmony-arm64@1.2.8':
resolution: {integrity: sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-win32-arm64-msvc@1.2.15':
resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-arm64-msvc@1.2.8':
resolution: {integrity: sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.2.15':
resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.2.8':
resolution: {integrity: sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2235,7 +2132,7 @@ packages:
resolution: {integrity: sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
peerDependencies:
vite: ^8.3.3
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@@ -2364,8 +2261,8 @@ packages:
peerDependencies:
'@popperjs/core': ^2.11.8
brace-expansion@2.1.7:
resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==}
brace-expansion@2.1.4:
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
brace-expansion@5.0.12:
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
@@ -2700,8 +2597,8 @@ packages:
fast-string-width@3.0.2:
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
fast-uri@3.1.9:
resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==}
fast-uri@3.1.8:
resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
fast-wrap-ansi@0.2.2:
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
@@ -3132,8 +3029,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.15.2:
resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==}
js-yaml@3.15.1:
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
hasBin: true
jsdom@26.1.0:
@@ -3383,11 +3280,6 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.20:
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
napi-postinstall@0.3.4:
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@@ -3618,10 +3510,6 @@ packages:
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.31:
resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -3713,11 +3601,6 @@ packages:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
rolldown@1.2.15:
resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
rolldown@1.2.8:
resolution: {integrity: sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -3890,8 +3773,8 @@ packages:
resolution: {integrity: sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA==}
engines: {node: '>=22'}
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
@@ -4122,13 +4005,13 @@ packages:
resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==}
engines: {node: '>=18.12.0'}
vite@8.3.5:
resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==}
vite@8.3.0:
resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.7.1 || ^0.8.0
'@vitejs/devtools': ^0.7.1
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
@@ -4285,8 +4168,8 @@ packages:
resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==}
engines: {node: '>=18'}
zod@4.6.7:
resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==}
zod@4.6.5:
resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==}
snapshots:
@@ -4486,7 +4369,7 @@ snapshots:
'@babel/core': 8.0.5
'@inquirer/confirm': 6.3.2(@types/node@26.6.2)
'@parcel/watcher': 2.6.0
'@vitejs/plugin-basic-ssl': 2.3.0(vite@8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1))
'@vitejs/plugin-basic-ssl': 2.3.0(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1))
beasties: 0.5.4
browserslist: 4.29.1
chokidar: 5.0.0
@@ -4508,7 +4391,7 @@ snapshots:
tinyglobby: 0.2.17
tslib: 2.8.1
typescript: 6.0.3
vite: 8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)
vite: 8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)
xxhash-wasm: 1.1.0
optionalDependencies:
'@angular/core': 22.2.2(@angular/compiler@22.2.2)(rxjs@7.8.2)
@@ -4554,7 +4437,7 @@ snapshots:
parse5-html-rewriting-stream: 8.0.1
semver: 7.8.5
yargs: 18.1.0
zod: 4.6.7
zod: 4.6.5
transitivePeerDependencies:
- '@types/node'
- chokidar
@@ -4598,7 +4481,7 @@ snapshots:
'@standard-schema/spec': 1.1.0
rxjs: 7.8.2
tslib: 2.8.1
zod: 4.6.7
zod: 4.6.5
'@angular/localize@22.2.2(@angular/compiler-cli@22.2.2(@angular/compiler@22.2.2)(typescript@6.0.3))(@angular/compiler@22.2.2)':
dependencies:
@@ -5235,7 +5118,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.15.2
js-yaml: 3.15.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.6': {}
@@ -5529,12 +5412,12 @@ snapshots:
'@modelcontextprotocol/core@2.3.1':
dependencies:
zod: 4.6.7
zod: 4.6.5
'@modelcontextprotocol/server@2.3.1':
dependencies:
'@modelcontextprotocol/core': 2.3.1
zod: 4.6.7
zod: 4.6.5
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
optional: true
@@ -5769,8 +5652,6 @@ snapshots:
'@oxc-project/types@0.150.0': {}
'@oxc-project/types@0.155.0': {}
'@parcel/watcher-android-arm64@2.6.0':
optional: true
@@ -5838,93 +5719,48 @@ snapshots:
'@popperjs/core@2.11.8': {}
'@rolldown/binding-android-arm-eabi@1.2.15':
optional: true
'@rolldown/binding-android-arm-eabi@1.2.8':
optional: true
'@rolldown/binding-android-arm64@1.2.15':
optional: true
'@rolldown/binding-android-arm64@1.2.8':
optional: true
'@rolldown/binding-darwin-arm64@1.2.15':
optional: true
'@rolldown/binding-darwin-arm64@1.2.8':
optional: true
'@rolldown/binding-darwin-x64@1.2.15':
optional: true
'@rolldown/binding-darwin-x64@1.2.8':
optional: true
'@rolldown/binding-freebsd-x64@1.2.15':
optional: true
'@rolldown/binding-freebsd-x64@1.2.8':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.2.15':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.2.8':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.2.15':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.2.8':
optional: true
'@rolldown/binding-linux-arm64-musl@1.2.15':
optional: true
'@rolldown/binding-linux-arm64-musl@1.2.8':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.2.15':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.2.8':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.2.15':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.2.8':
optional: true
'@rolldown/binding-linux-x64-gnu@1.2.15':
optional: true
'@rolldown/binding-linux-x64-gnu@1.2.8':
optional: true
'@rolldown/binding-linux-x64-musl@1.2.15':
optional: true
'@rolldown/binding-linux-x64-musl@1.2.8':
optional: true
'@rolldown/binding-openharmony-arm64@1.2.15':
optional: true
'@rolldown/binding-openharmony-arm64@1.2.8':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.2.15':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.2.8':
optional: true
'@rolldown/binding-win32-x64-msvc@1.2.15':
optional: true
'@rolldown/binding-win32-x64-msvc@1.2.8':
optional: true
@@ -6215,9 +6051,9 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true
'@vitejs/plugin-basic-ssl@2.3.0(vite@8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1))':
'@vitejs/plugin-basic-ssl@2.3.0(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1))':
dependencies:
vite: 8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)
vite: 8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)
acorn-jsx@5.3.2(acorn@8.18.0):
dependencies:
@@ -6247,7 +6083,7 @@ snapshots:
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.9
fast-uri: 3.1.8
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@@ -6362,7 +6198,7 @@ snapshots:
dependencies:
'@popperjs/core': 2.11.8
brace-expansion@2.1.7:
brace-expansion@2.1.4:
dependencies:
balanced-match: 1.0.2
@@ -6707,7 +6543,7 @@ snapshots:
dependencies:
fast-string-truncated-width: 3.0.3
fast-uri@3.1.9: {}
fast-uri@3.1.8: {}
fast-wrap-ansi@0.2.2:
dependencies:
@@ -7357,7 +7193,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.15.2:
js-yaml@3.15.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
@@ -7557,7 +7393,7 @@ snapshots:
minimatch@9.0.9:
dependencies:
brace-expansion: 2.1.7
brace-expansion: 2.1.4
minimist@1.2.8: {}
@@ -7592,8 +7428,6 @@ snapshots:
nanoid@3.3.18: {}
nanoid@3.3.20: {}
napi-postinstall@0.3.4: {}
natural-compare@1.4.0: {}
@@ -7840,13 +7674,7 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.2
postcss@8.5.31:
dependencies:
nanoid: 3.3.20
picocolors: 1.1.1
source-map-js: 1.2.2
source-map-js: 1.2.1
prelude-ls@1.2.1: {}
@@ -7914,27 +7742,6 @@ snapshots:
onetime: 7.0.0
signal-exit: 4.1.0
rolldown@1.2.15:
dependencies:
'@oxc-project/types': 0.155.0
'@rolldown/pluginutils': 1.0.1
optionalDependencies:
'@rolldown/binding-android-arm-eabi': 1.2.15
'@rolldown/binding-android-arm64': 1.2.15
'@rolldown/binding-darwin-arm64': 1.2.15
'@rolldown/binding-darwin-x64': 1.2.15
'@rolldown/binding-freebsd-x64': 1.2.15
'@rolldown/binding-linux-arm-gnueabihf': 1.2.15
'@rolldown/binding-linux-arm64-gnu': 1.2.15
'@rolldown/binding-linux-arm64-musl': 1.2.15
'@rolldown/binding-linux-ppc64-gnu': 1.2.15
'@rolldown/binding-linux-s390x-gnu': 1.2.15
'@rolldown/binding-linux-x64-gnu': 1.2.15
'@rolldown/binding-linux-x64-musl': 1.2.15
'@rolldown/binding-openharmony-arm64': 1.2.15
'@rolldown/binding-win32-arm64-msvc': 1.2.15
'@rolldown/binding-win32-x64-msvc': 1.2.15
rolldown@1.2.8:
dependencies:
'@oxc-project/types': 0.149.0
@@ -8055,7 +7862,7 @@ snapshots:
dependencies:
chokidar: 5.0.0
immutable: 5.1.9
source-map-js: 1.2.2
source-map-js: 1.2.1
optionalDependencies:
'@parcel/watcher': 2.6.0
@@ -8084,7 +7891,7 @@ snapshots:
ansi-styles: 6.2.3
is-fullwidth-code-point: 5.1.0
source-map-js@1.2.2: {}
source-map-js@1.2.1: {}
source-map-support@0.5.21:
dependencies:
@@ -8311,12 +8118,12 @@ snapshots:
verkit@0.3.2: {}
vite@8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1):
vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
postcss: 8.5.31
rolldown: 1.2.15
postcss: 8.5.28
rolldown: 1.2.8
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 26.6.2
@@ -8434,4 +8241,4 @@ snapshots:
yoctocolors@2.2.0: {}
zod@4.6.7: {}
zod@4.6.5: {}
-3
View File
@@ -24,6 +24,3 @@ onlyBuiltDependencies:
ignoredBuiltDependencies:
- normalize-diacritics
- unrs-resolver
overrides:
"vite@>=8.3.0 <8.3.3": "^8.3.3"
"zod@<4.6.6": "^4.6.6"
@@ -1,3 +1,10 @@
.custom-fields-dropdown {
min-width: 300px;
// correct position on mobile
@media (max-width: 575.98px) {
&.show {
margin-left: -245px !important;
}
}
}
@@ -308,7 +308,7 @@
</tr>
<tr class="align-middle">
<td i18n>Original SHA256 checksum</td>
<td><span class="badge bg-light text-dark font-monospace text-wrap text-break text-start">{{metadata()?.original_checksum}}</span></td>
<td><span class="badge bg-light font-monospace text-wrap text-break text-start">{{metadata()?.original_checksum}}</span></td>
</tr>
<tr>
<td i18n>Original file size</td>
+1 -1
View File
@@ -8,7 +8,7 @@ export const environment = {
apiVersion: '10', // match src/paperless/settings.py
appTitle: DEFAULT_APP_TITLE,
tag: 'prod',
version: '3.4.0',
version: '3.3.4',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-62
View File
@@ -11,8 +11,6 @@ from typing import TYPE_CHECKING
import pytest
from paperless_testing import samples
if TYPE_CHECKING:
from collections.abc import Generator
from pathlib import Path
@@ -77,30 +75,6 @@ def _clear_django_caches() -> None:
cache.clear()
@pytest.fixture(autouse=True)
def _keep_connection_open_in_atomic(monkeypatch: pytest.MonkeyPatch) -> None:
"""Make ``close_old_connections()`` a no-op inside the test's transaction.
The Celery signal handlers call it, as a worker would between tasks. Tests
run inside an open atomic block, where Postgres really closes the
connection, taking the test transaction and every later test on the
worker with it. SQLite in-memory ignores the close, which hid this.
"""
from django.db.backends.base.base import BaseDatabaseWrapper
original = BaseDatabaseWrapper.close_if_unusable_or_obsolete
def close_unless_atomic(self: BaseDatabaseWrapper) -> None:
if not self.in_atomic_block:
original(self)
monkeypatch.setattr(
BaseDatabaseWrapper,
"close_if_unusable_or_obsolete",
close_unless_atomic,
)
@pytest.fixture
def paperless_dirs(
tmp_path: Path,
@@ -175,39 +149,3 @@ def fake_progress_manager(
monkeypatch.setattr("documents.tasks.ProgressManager", FakeProgressManager)
return FakeProgressManager
@pytest.fixture(scope="session")
def shared_samples_dir() -> Path:
"""Directory of sample files used by more than one app's tests."""
return samples.SHARED_SAMPLES_DIR
@pytest.fixture(scope="session")
def simple_digital_pdf_file() -> Path:
"""One-page PDF with a text layer."""
return samples.SIMPLE_DIGITAL_PDF
@pytest.fixture(scope="session")
def multi_page_digital_pdf_file() -> Path:
"""Three-page PDF with a text layer."""
return samples.MULTI_PAGE_DIGITAL_PDF
@pytest.fixture(scope="session")
def with_form_pdf_file() -> Path:
"""PDF containing a fillable form."""
return samples.WITH_FORM_PDF
@pytest.fixture(scope="session")
def multi_page_images_pdf_file() -> Path:
"""Multi-page PDF of scanned images, no text layer."""
return samples.MULTI_PAGE_IMAGES_PDF
@pytest.fixture(scope="session")
def thumbnail_webp_file() -> Path:
"""Small WebP thumbnail."""
return samples.THUMBNAIL_WEBP
+3 -45
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import logging
import re
from fnmatch import fnmatch
from fnmatch import translate as fnmatch_translate
from typing import TYPE_CHECKING
from rest_framework import serializers
@@ -547,48 +548,6 @@ def existing_document_matches_workflow(
return (True, None)
def glob_to_database_regex(pattern: str) -> str:
"""
Translates an fnmatch style glob into a regular expression which the
database can evaluate, matching the whole value.
fnmatch.translate is not usable here: it emits syntax such as (?s:...),
atomic groups and named group backreferences, which PostgreSQL rejects.
Only constructs common to Python, PostgreSQL and MariaDB are produced.
"""
parts: list[str] = []
i = 0
n = len(pattern)
while i < n:
char = pattern[i]
i += 1
if char == "*":
parts.append(".*")
elif char == "?":
parts.append(".")
elif char == "[":
j = i
if j < n and pattern[j] == "!":
j += 1
if j < n and pattern[j] == "]":
j += 1
while j < n and pattern[j] != "]":
j += 1
if j >= n:
parts.append(r"\[")
else:
stuff = pattern[i:j].replace("\\", r"\\").replace("[", r"\[")
i = j + 1
if stuff.startswith("!"):
stuff = "^" + stuff[1:]
elif stuff.startswith("^"):
stuff = "\\" + stuff
parts.append(f"[{stuff}]")
else:
parts.append(re.escape(char))
return "^" + "".join(parts) + "$"
def prefilter_documents_by_workflowtrigger(
documents: QuerySet[Document],
trigger: WorkflowTrigger,
@@ -669,9 +628,8 @@ def prefilter_documents_by_workflowtrigger(
documents = documents.annotate(**annotations).filter(custom_field_q)
if trigger.filter_filename:
documents = documents.filter(
original_filename__iregex=glob_to_database_regex(trigger.filter_filename),
)
regex = fnmatch_translate(trigger.filter_filename).lstrip("^").rstrip("$")
documents = documents.filter(original_filename__iregex=regex)
return documents
+6 -6
View File
@@ -476,13 +476,13 @@ def update_filename_and_move_files(
return
with FileLock(settings.MEDIA_LOCK):
try:
# If this was waiting for the lock, the filename or archive_filename
# of this document may have been updated. This happens if multiple updates
# get queued from the UI for the same document
# So freshen up the data before doing anything
instance.refresh_from_db()
# If this was waiting for the lock, the filename or archive_filename
# of this document may have been updated. This happens if multiple updates
# get queued from the UI for the same document
# So freshen up the data before doing anything
instance.refresh_from_db()
try:
old_filename = instance.filename
old_source_path = instance.source_path
move_original = False
+4 -1
View File
@@ -44,7 +44,10 @@ def localize_date(value: date | datetime | str, format: str, locale: str) -> str
def format_datetime(value: str | datetime, format: str) -> str:
if isinstance(value, str):
value = parse_date(value)
parsed = parse_datetime(value) or parse_date(value)
if parsed is None:
raise ValueError(f"Invalid date or datetime string: {value!r}")
return parsed.strftime(format=format)
return value.strftime(format=format)
+10 -6
View File
@@ -12,25 +12,29 @@ if TYPE_CHECKING:
from paperless_testing.dirs import PaperlessDirs
@pytest.fixture(scope="session")
def document_samples_dir() -> Path:
"""Path to the shared test sample documents."""
return Path(__file__).parent / "samples" / "documents"
@pytest.fixture()
def sample_doc(
paperless_dirs: "PaperlessDirs",
simple_digital_pdf_file: Path,
multi_page_images_pdf_file: Path,
thumbnail_webp_file: Path,
document_samples_dir: Path,
) -> "Document":
"""Create a document with valid files and matching checksums."""
with filelock.FileLock(paperless_dirs.media_lock):
shutil.copy(
simple_digital_pdf_file,
document_samples_dir / "originals" / "0000001.pdf",
paperless_dirs.originals_dir / "0000001.pdf",
)
shutil.copy(
multi_page_images_pdf_file,
document_samples_dir / "archive" / "0000001.pdf",
paperless_dirs.archive_dir / "0000001.pdf",
)
shutil.copy(
thumbnail_webp_file,
document_samples_dir / "thumbnails" / "0000001.webp",
paperless_dirs.thumbnail_dir / "0000001.webp",
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.
+1
View File
@@ -0,0 +1 @@
This is a test file.
@@ -53,7 +53,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
archive_checksum="D",
)
shutil.copy(self.SIMPLE_PDF, self.doc2.source_path)
shutil.copy(self.SAMPLE_DIR / "simple.pdf", self.doc2.source_path)
shutil.copy(self.SAMPLE_DIR / "simple.png", self.doc2b.source_path)
shutil.copy(self.SAMPLE_DIR / "simple.jpg", self.doc3.source_path)
shutil.copy(self.SAMPLE_DIR / "test_with_bom.pdf", self.doc3.archive_path)
+13 -13
View File
@@ -1062,7 +1062,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [doc2.id, doc3.id, doc4.id],
"value": [2, 3, 4],
},
],
},
@@ -1073,13 +1073,13 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(CustomFieldInstance.objects.count(), 4)
_cf_5 = doc2.custom_fields.first()
assert _cf_5 is not None
self.assertEqual(_cf_5.value, [doc1.id])
self.assertEqual(_cf_5.value, [1])
_cf_6 = doc3.custom_fields.first()
assert _cf_6 is not None
self.assertEqual(_cf_6.value, [doc1.id])
self.assertEqual(_cf_6.value, [1])
_cf_7 = doc4.custom_fields.first()
assert _cf_7 is not None
self.assertEqual(_cf_7.value, [doc1.id])
self.assertEqual(_cf_7.value, [1])
# Add links appends if necessary
resp = self.client.patch(
@@ -1088,7 +1088,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [doc1.id, doc4.id],
"value": [1, 4],
},
],
},
@@ -1098,7 +1098,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_200_OK)
_cf_8 = doc4.custom_fields.first()
assert _cf_8 is not None
self.assertEqual(_cf_8.value, [doc1.id, doc3.id])
self.assertEqual(_cf_8.value, [1, 3])
# Remove one of the links, removed on other doc
resp = self.client.patch(
@@ -1107,7 +1107,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
"custom_fields": [
{
"field": custom_field_doclink.id,
"value": [doc2.id, doc3.id],
"value": [2, 3],
},
],
},
@@ -1117,13 +1117,13 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_200_OK)
_cf_9 = doc2.custom_fields.first()
assert _cf_9 is not None
self.assertEqual(_cf_9.value, [doc1.id])
self.assertEqual(_cf_9.value, [1])
_cf_10 = doc3.custom_fields.first()
assert _cf_10 is not None
self.assertEqual(_cf_10.value, [doc1.id, doc4.id])
self.assertEqual(_cf_10.value, [1, 4])
_cf_11 = doc4.custom_fields.first()
assert _cf_11 is not None
self.assertEqual(_cf_11.value, [doc3.id])
self.assertEqual(_cf_11.value, [3])
# Removes the field entirely
resp = self.client.patch(
@@ -1140,10 +1140,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(_cf_12.value, [])
_cf_13 = doc3.custom_fields.first()
assert _cf_13 is not None
self.assertEqual(_cf_13.value, [doc4.id])
self.assertEqual(_cf_13.value, [4])
_cf_14 = doc4.custom_fields.first()
assert _cf_14 is not None
self.assertEqual(_cf_14.value, [doc3.id])
self.assertEqual(_cf_14.value, [3])
# If field exists on target doc but value is None
doc5 = Document.objects.create(
@@ -1170,7 +1170,7 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(resp.status_code, status.HTTP_200_OK)
_cf_15 = doc5.custom_fields.first()
assert _cf_15 is not None
self.assertEqual(_cf_15.value, [doc1.id])
self.assertEqual(_cf_15.value, [1])
def test_documentlink_patch_requires_change_permission_on_target_documents(
self,
+33 -49
View File
@@ -57,8 +57,6 @@ from paperless_testing.http import read_streaming_response
from paperless_testing.permissions import grant_all_global
from paperless_testing.permissions import grant_global
from paperless_testing.permissions import grant_object
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
from paperless_testing.samples import THUMBNAIL_WEBP
class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
@@ -1919,11 +1917,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SimpleUploadedFile(
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f},
@@ -1951,7 +1945,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
payload = SimpleUploadedFile(
"../../outside.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
(Path(__file__).parent / "samples" / "simple.pdf").read_bytes(),
content_type="application/pdf",
)
@@ -1980,7 +1974,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
pdf_bytes = SIMPLE_DIGITAL_PDF.read_bytes()
pdf_bytes = (Path(__file__).parent / "samples" / "simple.pdf").read_bytes()
boundary = "paperless-boundary"
payload = (
(
@@ -2056,7 +2050,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
def test_upload_insufficient_permissions(self) -> None:
self.client.force_authenticate(user=UserFactory(username="testuser2"))
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f},
@@ -2069,11 +2063,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SimpleUploadedFile(
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{
@@ -2106,7 +2096,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"documenst": f},
@@ -2132,7 +2122,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "title": "my custom title"},
@@ -2152,7 +2142,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
)
c = Correspondent.objects.create(name="test-corres")
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "correspondent": c.id},
@@ -2171,7 +2161,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "correspondent": 3456},
@@ -2186,7 +2176,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
)
dt = DocumentType.objects.create(name="invoice")
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "document_type": dt.id},
@@ -2205,7 +2195,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "document_type": 34578},
@@ -2220,7 +2210,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
)
sp = StoragePath.objects.create(name="invoices")
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "storage_path": sp.id},
@@ -2239,7 +2229,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "storage_path": 34578},
@@ -2255,7 +2245,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
t1 = Tag.objects.create(name="tag1")
t2 = Tag.objects.create(name="tag2")
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "tags": [t2.id, t1.id]},
@@ -2276,7 +2266,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
t1 = Tag.objects.create(name="tag1")
t2 = Tag.objects.create(name="tag2")
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "tags": [t2.id, t1.id, 734563]},
@@ -2300,7 +2290,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
0,
tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"),
)
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "created": created},
@@ -2316,11 +2306,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SimpleUploadedFile(
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "archive_serial_number": 500},
@@ -2347,11 +2333,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
data_type=CustomField.FieldDataType.STRING,
)
with SimpleUploadedFile(
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{
@@ -2406,7 +2388,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
w1.actions.add(action1)
w1.save()
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{
@@ -2447,11 +2429,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
data_type=CustomField.FieldDataType.INT,
)
with SimpleUploadedFile(
"simple.pdf",
SIMPLE_DIGITAL_PDF.read_bytes(),
content_type="application/pdf",
) as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{
@@ -2500,7 +2478,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
]
for payload in error_payloads:
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
data = {"document": f, **payload}
response = self.client.post(
"/api/documents/post_document/",
@@ -2558,7 +2536,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
id=str(uuid.uuid4()),
)
with SIMPLE_DIGITAL_PDF.open("rb") as f:
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
response = self.client.post(
"/api/documents/post_document/",
{"document": f, "from_webui": True},
@@ -2597,8 +2575,14 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
archive_filename="archive.pdf",
)
source_file: Path = THUMBNAIL_WEBP
archive_file: Path = SIMPLE_DIGITAL_PDF
source_file: Path = (
Path(__file__).parent
/ "samples"
/ "documents"
/ "thumbnails"
/ "0000001.webp"
)
archive_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
shutil.copy(source_file, doc.source_path)
shutil.copy(archive_file, doc.archive_path)
@@ -2631,7 +2615,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
mime_type="application/pdf",
)
shutil.copy(SIMPLE_DIGITAL_PDF, doc.source_path)
shutil.copy(Path(__file__).parent / "samples" / "simple.pdf", doc.source_path)
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -4397,8 +4381,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
filename="test2.pdf",
)
archive_file = SIMPLE_DIGITAL_PDF
source_file = SIMPLE_DIGITAL_PDF
archive_file = Path(__file__).parent / "samples" / "simple.pdf"
source_file = Path(__file__).parent / "samples" / "simple.pdf"
shutil.copy(archive_file, doc.archive_path)
shutil.copy(source_file, doc2.source_path)
+5 -8
View File
@@ -12,9 +12,6 @@ from documents.tests.utils import SampleDirMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
from paperless_testing.permissions import grant_global
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
from paperless_testing.samples import WITH_FORM_PDF
class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
@@ -45,15 +42,15 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
# Copy sample files to document paths (using different files to distinguish versions)
shutil.copy(
SIMPLE_DIGITAL_PDF,
self.SAMPLE_DIR / "documents" / "originals" / "0000001.pdf",
self.doc1.archive_path,
)
shutil.copy(
MULTI_PAGE_DIGITAL_PDF,
self.SAMPLE_DIR / "documents" / "originals" / "0000002.pdf",
self.doc1.source_path,
)
shutil.copy(
WITH_FORM_PDF,
self.SAMPLE_DIR / "documents" / "originals" / "0000003.pdf",
self.doc2.source_path,
)
@@ -380,7 +377,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
checksum="3",
filename="test3.pdf",
)
shutil.copy(self.SIMPLE_PDF, doc3.source_path)
shutil.copy(self.SAMPLE_DIR / "simple.pdf", doc3.source_path)
doc4 = Document.objects.create(
title="test1",
@@ -389,7 +386,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
checksum="4",
filename="test4.pdf",
)
shutil.copy(self.SIMPLE_PDF, doc4.source_path)
shutil.copy(self.SAMPLE_DIR / "simple.pdf", doc4.source_path)
response = self.client.post(
self.ENDPOINT,
@@ -6,7 +6,6 @@ from unittest.mock import patch
from urllib.parse import quote
import time_machine
from django.db import connection
from django.test import override_settings
from django.utils import timezone
from rest_framework.test import APITestCase
@@ -21,13 +20,6 @@ from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import UserFactory
def _exact_equal(value: str | None, expected: str) -> bool:
"""`exact` follows the column collation, which is case insensitive on MariaDB."""
if connection.vendor == "mysql" and value is not None:
return value.casefold() == expected.casefold()
return value == expected
class DocumentWrapper:
"""
Allows Pythonic access to the custom fields associated with the wrapped document.
@@ -295,8 +287,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
self._assert_query_match_predicate(
["string_field", "exact", "paperless"],
lambda document: (
"string_field" in document
and _exact_equal(document["string_field"], "paperless")
"string_field" in document and document["string_field"] == "paperless"
),
)
@@ -305,10 +296,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
["string_field", "in", ["paperless", "Paperless"]],
lambda document: (
"string_field" in document
and any(
_exact_equal(document["string_field"], expected)
for expected in ("paperless", "Paperless")
)
and document["string_field"] in ("paperless", "Paperless")
),
)
@@ -640,23 +628,22 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
# Subset check (document link field only) #
# ==========================================================#
def test_document_link_contains(self) -> None:
linked = {self.documents[1].id, self.documents[2].id}
# Document link field "contains" performs a subset check.
self._assert_query_match_predicate(
["documentlink_field", "contains", sorted(linked)],
["documentlink_field", "contains", [1, 2]],
lambda document: (
"documentlink_field" in document
and document["documentlink_field"] is not None
and set(document["documentlink_field"]) >= linked
and set(document["documentlink_field"]) >= {1, 2}
),
)
# The order of IDs don't matter - this is the same as above.
self._assert_query_match_predicate(
["documentlink_field", "contains", sorted(linked, reverse=True)],
["documentlink_field", "contains", [2, 1]],
lambda document: (
"documentlink_field" in document
and document["documentlink_field"] is not None
and set(document["documentlink_field"]) >= linked
and set(document["documentlink_field"]) >= {1, 2}
),
)
@@ -773,7 +760,7 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
lambda document: (
not (
"string_field" in document
and _exact_equal(document["string_field"], "paperless")
and document["string_field"] == "paperless"
)
),
)
+4 -6
View File
@@ -944,7 +944,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data["results"]
self.assertEqual({r["id"] for r in results}, {d1.id, d2.id})
self.assertEqual({r["id"] for r in results}, {1, 2})
@mock.patch("documents.search._backend.parse_user_query")
def test_search_parser_bug_surfaces_as_500_not_400(self, m) -> None:
@@ -1801,7 +1801,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
def test_search_sorting(self) -> None:
u1 = UserFactory(username="user1")
u2 = UserFactory(username="user2")
u3 = UserFactory(username="user3")
c1 = Correspondent.objects.create(name="corres Ax")
c2 = Correspondent.objects.create(name="corres Cx")
c3 = Correspondent.objects.create(name="corres Bx")
@@ -1827,7 +1826,6 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
content="test",
archive_serial_number=1,
title="1",
owner=u3,
)
Note.objects.create(
note="This is a note.",
@@ -1880,14 +1878,14 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
search_query("&ordering=-num_notes"),
[d1.id, d3.id, d2.id],
)
# owner sort: ORM orders by owner_id (integer)
# owner sort: ORM orders by owner_id (integer); NULLs first in SQLite ASC
self.assertListEqual(
search_query("&ordering=owner"),
[d1.id, d2.id, d3.id],
[d3.id, d1.id, d2.id],
)
self.assertListEqual(
search_query("&ordering=-owner"),
[d3.id, d2.id, d1.id],
[d2.id, d1.id, d3.id],
)
def test_search_ordering_by_score(self) -> None:
+48 -2
View File
@@ -5,7 +5,6 @@ from datetime import timedelta
from pathlib import Path
from unittest import mock
from django.db import connection
from django.test import override_settings
from django.utils import timezone
from rest_framework import status
@@ -69,7 +68,7 @@ class TestSystemStatus(APITestCase):
self.assertEqual(response.data["install_type"], "bare-metal")
self.assertIsNotNone(response.data["storage"]["total"])
self.assertIsNotNone(response.data["storage"]["available"])
self.assertEqual(response.data["database"]["type"], connection.vendor)
self.assertEqual(response.data["database"]["type"], "sqlite")
self.assertIsNotNone(response.data["database"]["url"])
self.assertEqual(response.data["database"]["status"], "OK")
self.assertIsNone(response.data["database"]["error"])
@@ -540,3 +539,50 @@ class TestSystemStatus(APITestCase):
self.assertEqual(response.data["tasks"]["summary"]["success_count"], 1)
self.assertEqual(response.data["tasks"]["summary"]["failure_count"], 1)
self.assertEqual(response.data["tasks"]["summary"]["pending_count"], 1)
def test_system_status_reports_database_error(self) -> None:
"""
GIVEN:
- Reading the migration state fails
WHEN:
- The system status is requested
THEN:
- The response reports the database error rather than a server error
"""
self.client.force_login(self.user)
with mock.patch(
"documents.views.MigrationLoader",
side_effect=Exception("database is down"),
):
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
database = response.data["database"]
self.assertEqual(database["status"], "ERROR")
self.assertIsNotNone(database["error"])
self.assertEqual(
database["migration_status"],
{"latest_migration": None, "unapplied_migrations": []},
)
def test_system_status_migration_query_error(self) -> None:
"""
GIVEN:
- The migration graph loads but reading the applied migrations fails
WHEN:
- The system status is requested
THEN:
- No migrations are reported as unapplied, since the state is unknown
"""
self.client.force_login(self.user)
with mock.patch("documents.views.MigrationRecorder") as recorder:
recorder.Migration.objects.all.side_effect = Exception("query failed")
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
database = response.data["database"]
self.assertEqual(database["status"], "ERROR")
self.assertEqual(
database["migration_status"],
{"latest_migration": None, "unapplied_migrations": []},
)
+2 -2
View File
@@ -169,7 +169,7 @@ class TestBarcode(
- No barcodes detected
- No pages to split on
"""
test_file = self.SIMPLE_PDF
test_file = self.SAMPLE_DIR / "simple.pdf"
with self.get_reader(test_file) as reader:
reader.detect()
separator_page_numbers = reader.get_separation_pages()
@@ -461,7 +461,7 @@ class TestBarcode(
THEN:
- Nothing happens
"""
test_file = self.SIMPLE_PDF
test_file = self.SAMPLE_DIR / "simple.pdf"
with self.get_reader(test_file) as reader:
try:
+34 -11
View File
@@ -29,9 +29,6 @@ from documents.permissions import set_permissions_for_objects
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.permissions import grant_object
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
from paperless_testing.samples import WITH_FORM_PDF
class TestBulkEdit(DirectoriesMixin, TestCase):
@@ -709,27 +706,47 @@ class TestPDFActions(DirectoriesMixin, TestCase):
super().setUp()
sample1 = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1,
)
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1_archive,
)
sample2 = self.dirs.scratch_dir / "sample2.pdf"
shutil.copy(
MULTI_PAGE_DIGITAL_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000002.pdf",
sample2,
)
sample2_archive = self.dirs.archive_dir / "sample2_archive.pdf"
shutil.copy(
MULTI_PAGE_DIGITAL_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000002.pdf",
sample2_archive,
)
sample3 = self.dirs.scratch_dir / "sample3.pdf"
shutil.copy(
WITH_FORM_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000003.pdf",
sample3,
)
self.doc1 = Document.objects.create(
@@ -763,7 +780,11 @@ class TestPDFActions(DirectoriesMixin, TestCase):
)
img_doc_archive = self.dirs.archive_dir / "sample_image.pdf"
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
img_doc_archive,
)
self.img_doc = Document.objects.create(
@@ -1031,7 +1052,9 @@ class TestPDFActions(DirectoriesMixin, TestCase):
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
bulk_edit.merge(doc_ids)
error_str = cm.output[0]
expected_str = f"Error merging document {self.doc2.id}, it will not be included in the merge"
expected_str = (
"Error merging document 2, it will not be included in the merge"
)
self.assertIn(expected_str, error_str)
mock_consume_file.assert_not_called()
@@ -1191,7 +1214,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
bulk_edit.split(doc_ids, pages)
error_str = cm.output[0]
expected_str = f"Error splitting document {self.doc2.id}"
expected_str = "Error splitting document 2"
self.assertIn(expected_str, error_str)
mock_consume_file.assert_not_called()
+33 -14
View File
@@ -39,9 +39,6 @@ from paperless_testing.factories import TagFactory
from paperless_testing.factories import UserFactory
from paperless_testing.fakes.progress import FakeProgressManager
from paperless_testing.permissions import grant_object
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
from paperless_testing.samples import MULTI_PAGE_IMAGES_PDF
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
class _BaseNewStyleParser:
@@ -118,7 +115,9 @@ class _BaseNewStyleParser:
class DummyParser(_BaseNewStyleParser):
_ARCHIVE_SRC = MULTI_PAGE_IMAGES_PDF
_ARCHIVE_SRC = (
Path(__file__).parent / "samples" / "documents" / "archive" / "0000001.pdf"
)
def parse(self, document_path, mime_type, *, produce_archive: bool = True) -> None:
self._text = "The Text"
@@ -204,19 +203,33 @@ class TestConsumer(
self.addCleanup(patcher.stop)
def get_test_file(self):
src = SIMPLE_DIGITAL_PDF
src = (
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf"
)
dst = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(src, dst)
return dst
def get_test_file2(self):
src = MULTI_PAGE_DIGITAL_PDF
src = (
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000002.pdf"
)
dst = self.dirs.scratch_dir / "sample2.pdf"
shutil.copy(src, dst)
return dst
def get_test_archive_file(self):
src = MULTI_PAGE_IMAGES_PDF
src = (
Path(__file__).parent / "samples" / "documents" / "archive" / "0000001.pdf"
)
dst = self.dirs.scratch_dir / "sample_archive.pdf"
shutil.copy(src, dst)
return dst
@@ -248,8 +261,8 @@ class TestConsumer(
)
self.assertIsNone(document.correspondent)
self.assertIsNone(document.document_type)
self.assertEqual(document.filename, f"{document.pk:07}.pdf")
self.assertEqual(document.archive_filename, f"{document.pk:07}.pdf")
self.assertEqual(document.filename, "0000001.pdf")
self.assertEqual(document.archive_filename, "0000001.pdf")
self.assertIsFile(document.source_path)
@@ -1111,7 +1124,7 @@ class TestConsumer(
@mock.patch("documents.consumer.get_parser_registry")
def test_similar_filenames(self, m) -> None:
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent / "samples" / "simple.pdf",
settings.CONSUMPTION_DIR / "simple.pdf",
)
shutil.copy(
@@ -1127,17 +1140,17 @@ class TestConsumer(
with self.get_consumer(settings.CONSUMPTION_DIR / "simple.png") as consumer:
consumer.run()
doc1 = Document.objects.latest("pk")
doc1 = Document.objects.filter(pk=1).first()
with self.get_consumer(settings.CONSUMPTION_DIR / "simple.pdf") as consumer:
consumer.run()
doc2 = Document.objects.latest("pk")
doc2 = Document.objects.filter(pk=2).first()
with self.get_consumer(settings.CONSUMPTION_DIR / "simple.png.pdf") as consumer:
consumer.run()
doc3 = Document.objects.latest("pk")
doc3 = Document.objects.filter(pk=3).first()
self.assertEqual(doc1.filename, "simple.png")
self.assertEqual(doc1.archive_filename, "simple.pdf")
@@ -1652,7 +1665,13 @@ class TestConsumerRemoteOCR(
self.addCleanup(patcher.stop)
def _consume(self, *, overrides: DocumentMetadataOverrides | None = None) -> bool:
src = SIMPLE_DIGITAL_PDF
src = (
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf"
)
dst = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(src, dst)
@@ -242,7 +242,9 @@ class TestDocumentListEffectiveContentAnnotation:
# Only the list query itself reads a content column: no extra query
# for the skipped prefetch, none for a per-instance fallback
content_queries = [
query for query in ctx.captured_queries if _content_column() in query["sql"]
query
for query in ctx.captured_queries
if '"documents_document"."content"' in query["sql"]
]
assert len(content_queries) == 1
@@ -363,11 +365,6 @@ def _get_document_queries(
return [q for q in ctx.captured_queries if '"django_content_type"' not in q["sql"]]
def _content_column() -> str:
quote = connection.ops.quote_name
return f"{quote('documents_document')}.{quote('content')}"
def _get_effective_content_fallback_queries(
ctx: CaptureQueriesContext,
) -> list[dict[str, str]]:
@@ -382,7 +379,7 @@ def _get_effective_content_fallback_queries(
return [
q
for q in ctx.captured_queries
if q["sql"].startswith(f"SELECT {_content_column()} FROM")
if q["sql"].startswith('SELECT "documents_document"."content" FROM')
]
+2 -5
View File
@@ -37,15 +37,12 @@ class TestDoubleSided(
self.double_sided_dir.mkdir()
self.staging_file = self.dirs.scratch_dir / STAGING_FILE_NAME
def _sample(self, name: str) -> Path:
return self.SIMPLE_PDF if name == "simple.pdf" else self.SAMPLE_DIR / name
def consume_file(self, srcname, dstname: str | Path = "foo.pdf"):
"""
Starts the consume process and also ensures the
destination file does not exist afterwards
"""
src = self._sample(srcname)
src = self.SAMPLE_DIR / srcname
dst = self.double_sided_dir / dstname
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst)
@@ -60,7 +57,7 @@ class TestDoubleSided(
return msg
def create_staging_file(self, src="double-sided-odd.pdf", datetime=None) -> None:
shutil.copy(self._sample(src), self.staging_file)
shutil.copy(self.SAMPLE_DIR / src, self.staging_file)
if datetime is None:
datetime = dt.datetime.now()
os.utime(str(self.staging_file), (datetime.timestamp(),) * 2)
+26 -6
View File
@@ -31,6 +31,7 @@ from documents.serialisers import DocumentSerializer
from documents.tasks import empty_trash
from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.dirs import PaperlessDirs
from paperless_testing.factories import DocumentFactory
from paperless_testing.factories import UserFactory
@@ -760,9 +761,6 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
archive_checksum="B",
)
# Without a format the files are renamed to match the primary key
original = doc.source_path
archive = doc.archive_path
self.assertIsFile(original)
self.assertIsFile(archive)
self.assertIsFile(doc.source_path)
@@ -981,9 +979,6 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
archive_filename="0000001.pdf",
)
# Without a format the files are renamed to match the primary key
original = doc.source_path
archive = doc.archive_path
self.assertIsFile(original)
self.assertIsFile(archive)
self.assertIsFile(doc.source_path)
@@ -2140,3 +2135,28 @@ class TestPathDateLocalization:
with override_settings(FILENAME_FORMAT=filename_format):
filename = generate_filename(document)
assert filename == Path(expected_filename)
@pytest.mark.django_db
def test_refresh_failure_surfaces_database_error(
paperless_dirs: PaperlessDirs,
) -> None:
"""
GIVEN:
- A document with a stored filename
WHEN:
- Saving it triggers a filename update and refreshing it from the database fails
THEN:
- The database error is raised, not an unbound variable error from the rollback
"""
document = DocumentFactory(filename="some.pdf")
with (
mock.patch.object(
Document,
"refresh_from_db",
side_effect=DatabaseError("database is locked"),
),
pytest.raises(DatabaseError, match="database is locked"),
):
document.save()
+21
View File
@@ -4,6 +4,7 @@ from typing import Literal
import pytest
from documents.templating.filters import format_datetime
from documents.templating.filters import localize_date
@@ -294,3 +295,23 @@ class TestDateLocalization:
Tests `localize_date` with `date` string across different locales and formats.
"""
assert localize_date(value, format_style, locale_str) == expected_output
class TestFormatDatetime:
@pytest.mark.parametrize(
("value", "expected"),
[
pytest.param("2026-09-21", "2026-09-21 00:00", id="date"),
pytest.param(
"2026-09-21T13:45:00+00:00",
"2026-09-21 13:45",
id="datetime",
),
],
)
def test_parses_iso_strings(self, value: str, expected: str) -> None:
assert format_datetime(value, "%Y-%m-%d %H:%M") == expected
def test_rejects_unparseable_string(self) -> None:
with pytest.raises(ValueError, match="not a date"):
format_datetime("not a date", "%Y")
+1 -2
View File
@@ -23,9 +23,8 @@ from documents.tasks import update_document_content_maybe_archive_file
from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
sample_file: Path = SIMPLE_DIGITAL_PDF
sample_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
@pytest.mark.management
+99 -35
View File
@@ -59,7 +59,6 @@ from paperless_testing.dirs import paperless_environment
from paperless_testing.factories import DocumentBarcodeFactory
from paperless_testing.factories import DocumentFactory
from paperless_testing.permissions import grant_object
from paperless_testing.samples import install_document_samples
@pytest.mark.management
@@ -151,20 +150,6 @@ class TestExportImport(
super().setUp()
def _install_sample_files(self) -> None:
documents_dir = Path(self.dirs.media_dir) / "documents"
shutil.rmtree(documents_dir)
install_document_samples(documents_dir)
# Thumbnails are located by primary key, which the database sequence
# does not reset between tests on PostgreSQL
for number, document in enumerate(
(self.d1, self.d2, self.d3, self.d4),
start=1,
):
source = documents_dir / "thumbnails" / f"{number:07}.webp"
if source != document.thumbnail_path:
shutil.copy(source, document.thumbnail_path)
def _get_document_from_manifest(self, manifest, id):
f = list(
filter(
@@ -218,7 +203,11 @@ class TestExportImport(
return manifest
def test_exporter(self, *, use_filename_format=False) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
num_permission_objects = Permission.objects.count()
@@ -321,7 +310,11 @@ class TestExportImport(
self.assertEqual(messages.total_issue_count, 0)
def test_exporter_with_filename_format(self) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
with override_settings(
FILENAME_FORMAT="{created_year}/{correspondent}/{title}",
@@ -329,7 +322,11 @@ class TestExportImport(
self.test_exporter(use_filename_format=True)
def test_exporter_includes_share_links_and_bundles(self) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
share_link = ShareLink.objects.create(
slug="share-link-slug",
@@ -427,7 +424,11 @@ class TestExportImport(
)
def test_update_export_changed_time(self) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export()
self.assertIsFile(self.target / "manifest.json")
@@ -462,7 +463,11 @@ class TestExportImport(
self.assertEqual(st_mtime_3, st_mtime_4)
def test_update_export_changed_checksum(self) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export()
@@ -488,7 +493,11 @@ class TestExportImport(
self.assertIsFile(self.target / "manifest.json")
def test_update_export_deleted_document(self) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
manifest = self._do_export()
@@ -519,7 +528,11 @@ class TestExportImport(
@override_settings(FILENAME_FORMAT="{title}/{correspondent}")
def test_update_export_changed_location(self) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export(use_filename_format=True)
self.assertIsFile(self.target / "wow1" / "c.pdf")
@@ -560,7 +573,11 @@ class TestExportImport(
- Zipfile is created
- Zipfile contains exported files
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
args = ["document_exporter", self.target, "--zip"]
@@ -588,7 +605,11 @@ class TestExportImport(
- Zipfile is created
- Zipfile contains exported files
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
args = ["document_exporter", self.target, "--zip", "--use-filename-format"]
@@ -623,7 +644,11 @@ class TestExportImport(
- Zipfile contains exported files
- The existing file and directory in target are removed
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
# Create stuff in target directory
existing_file = self.target / "test.txt"
@@ -718,7 +743,11 @@ class TestExportImport(
- Manifest.json doesn't contain information about archive files
- Documents can be imported again
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
manifest = self._do_export()
has_archive = False
@@ -760,7 +789,11 @@ class TestExportImport(
- Manifest.json doesn't contain information about thumbnails
- Documents can be imported again
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
manifest = self._do_export()
has_thumbnail = False
@@ -804,7 +837,11 @@ class TestExportImport(
- Main manifest.json file doesn't contain information about documents
- Documents can be imported again
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
manifest = self._do_export(split_manifest=True)
has_document = False
@@ -828,7 +865,11 @@ class TestExportImport(
self.assertEqual(CustomFieldInstance.objects.count(), 1)
def _export_import_barcodes(self, *, split_manifest: bool) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
DocumentBarcodeFactory(document=self.d1, value="https://example.com")
DocumentBarcodeFactory(document=self.d2, page=2, value="DE8937")
@@ -863,7 +904,11 @@ class TestExportImport(
THEN:
- Documents can be imported again
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export(use_folder_prefix=True)
@@ -889,7 +934,11 @@ class TestExportImport(
THEN:
- Documents can be imported again
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
self._do_export(use_folder_prefix=True, split_manifest=True)
@@ -915,7 +964,11 @@ class TestExportImport(
- ContentType & Permission objects are not deleted, db transaction rolled back
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
num_content_type_objects = ContentType.objects.count()
num_permission_objects = Permission.objects.count()
@@ -951,7 +1004,11 @@ class TestExportImport(
self.assertEqual(Permission.objects.count(), num_permission_objects + 1)
def test_exporter_with_auditlog_disabled(self) -> None:
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
with override_settings(
AUDIT_LOG_ENABLED=False,
@@ -970,7 +1027,11 @@ class TestExportImport(
- The soft-deleted document, note, and custom field instance
survive the round-trip with deleted_at preserved
"""
self._install_sample_files()
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree(
Path(__file__).parent / "samples" / "documents",
Path(self.dirs.media_dir) / "documents",
)
# d1 has self.note and self.cfi1 attached via setUp
self.d1.delete()
@@ -1014,7 +1075,10 @@ class TestExportImport(
"""
shutil.rmtree(self.dirs.media_dir / "documents")
install_document_samples(self.dirs.media_dir / "documents")
shutil.copytree(
self.SAMPLE_DIR / "documents",
self.dirs.media_dir / "documents",
)
_ = self._do_export(data_only=True)
@@ -19,10 +19,6 @@ class TestFuzzyMatchCommand(TestCase):
call_command(
"document_fuzzy_match",
"--no-progress-bar",
# The default scales with the CPU count and forks, which closes
# the test's database connection on PostgreSQL
"--processes",
"1",
*args,
stdout=stdout,
stderr=stderr,
@@ -425,29 +425,26 @@ class TestRetaggerStoragePath(DirectoriesMixin):
@pytest.mark.management
@pytest.mark.django_db
class TestRetaggerIdRange(DirectoriesMixin):
@pytest.mark.usefixtures("documents")
@pytest.mark.parametrize(
("range_end", "expected_count"),
("id_range_args", "expected_count"),
[
pytest.param("second", 1, id="narrow_range_limits_scope"),
pytest.param("extra", 2, id="wide_range_tags_all_matches"),
pytest.param(["1", "2"], 1, id="narrow_range_limits_scope"),
pytest.param(["1", "9999"], 2, id="wide_range_tags_all_matches"),
],
)
def test_id_range_limits_scope(
self,
tags: TagTuple,
documents: DocumentTuple,
range_end: str,
id_range_args: list[str],
expected_count: int,
) -> None:
first, second, *_ = documents
extra = DocumentFactory(content="NOT the first document")
end = {"second": second, "extra": extra}[range_end]
DocumentFactory(content="NOT the first document")
call_command(
"document_retagger",
"--tags",
"--id-range",
str(first.pk),
str(end.pk),
*id_range_args,
skip_checks=True,
)
tag_first, *_ = tags
@@ -11,7 +11,6 @@ from documents.models import Document
from documents.parsers import get_default_thumbnail
from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
@pytest.mark.management
@@ -25,7 +24,7 @@ class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
filename="test.pdf",
)
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent / "samples" / "simple.pdf",
self.d1.source_path,
)
@@ -37,7 +36,7 @@ class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
filename="test2.pdf",
)
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent / "samples" / "simple.pdf",
self.d2.source_path,
)
@@ -1,68 +0,0 @@
import re
from fnmatch import fnmatch
import pytest
from documents.matching import glob_to_database_regex
from documents.models import Document
from paperless_testing.factories import DocumentFactory
NAMES = [
"invoice.pdf",
"Invoice.PDF",
"invoice_2024.pdf",
"a.b.c",
"a-b",
"sample [1].pdf",
"sample 1.pdf",
"x^y",
"back\\slash",
"dir/file.pdf",
"",
]
PATTERNS = [
"*",
"invoice.pdf",
"*.pdf",
"*invoice*pdf",
"*a*b*c*",
"invoice_????.pdf",
"sample [[]1].pdf",
"sample [1-2].pdf",
"sample [!2].pdf",
"sample [^2].pdf",
"sample [",
"a.b.c",
"x^y",
"back\\slash",
"[]]",
]
@pytest.mark.parametrize("pattern", PATTERNS)
def test_regex_agrees_with_fnmatch(pattern: str) -> None:
regex = glob_to_database_regex(pattern)
for name in NAMES:
expected = fnmatch(name.lower(), pattern.lower())
actual = re.search(regex, name, re.IGNORECASE) is not None
assert actual is expected, f"{pattern!r} vs {name!r}"
@pytest.mark.parametrize("pattern", ["*", "*a*b*c*", "a*b*c*d*e"])
def test_regex_avoids_syntax_postgresql_rejects(pattern: str) -> None:
regex = glob_to_database_regex(pattern)
assert not any(marker in regex for marker in ("(?", "\\Z"))
@pytest.mark.django_db
def test_database_filter_matches_glob() -> None:
matching = DocumentFactory(original_filename="Invoice_2024.PDF")
DocumentFactory(original_filename="invoice_2024.txt")
DocumentFactory(original_filename="receipt.pdf")
found = Document.objects.filter(
original_filename__iregex=glob_to_database_regex("*invoice*.pdf"),
)
assert list(found) == [matching]
+1 -2
View File
@@ -15,11 +15,10 @@ import pytest
from documents import pdf_ops
from documents.pdf_ops import PageSpec
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
SRC_ROOT = Path(__file__).parents[2]
SAMPLES = Path(__file__).parent / "samples"
THREE_PAGES = MULTI_PAGE_DIGITAL_PDF
THREE_PAGES = SAMPLES / "documents" / "originals" / "0000002.pdf"
TWELVE_PAGES = SAMPLES / "barcodes" / "split-by-asn-2.pdf"
ENCRYPTED = SAMPLES / "password-is-test.pdf"
SIGNED = SRC_ROOT / "paperless" / "tests" / "samples" / "tesseract" / "signed.pdf"
+23 -5
View File
@@ -21,7 +21,6 @@ from documents.tests.helpers import dummy_preprocess
from paperless_testing.assertions import FileSystemAssertsMixin
from paperless_testing.dirs import DirectoriesMixin
from paperless_testing.factories import DocumentFactory
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
@pytest.mark.django_db
@@ -230,12 +229,20 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
"""
sample1 = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1,
)
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1_archive,
)
doc = Document.objects.create(
@@ -263,7 +270,11 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
"""
sample1 = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(
SIMPLE_DIGITAL_PDF,
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1,
)
doc = Document.objects.create(
@@ -279,7 +290,14 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
def _create_root_with_version(self) -> tuple[Document, Document]:
sample1 = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(SIMPLE_DIGITAL_PDF, sample1)
shutil.copy(
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf",
sample1,
)
root = DocumentFactory(content="root content", mime_type="application/pdf")
version = DocumentFactory(
content="my document",
+30 -30
View File
@@ -174,11 +174,11 @@ class TestWorkflows(
w.save()
self.assertEqual(w.__str__(), "Workflow: Workflow 1")
self.assertEqual(trigger.__str__(), f"WorkflowTrigger {trigger.pk}")
self.assertEqual(action.__str__(), f"WorkflowAction {action.pk}")
self.assertEqual(trigger.__str__(), "WorkflowTrigger 1")
self.assertEqual(action.__str__(), "WorkflowAction 1")
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -290,7 +290,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -409,7 +409,7 @@ class TestWorkflows(
w2.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -433,7 +433,7 @@ class TestWorkflows(
list(document.tags.all()),
[self.t1, self.t2, self.t3],
)
self.assertCountEqual(
self.assertEqual(
list(
get_users_with_perms(
document,
@@ -478,7 +478,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -531,7 +531,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -608,7 +608,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -687,7 +687,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -765,7 +765,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -876,7 +876,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -928,7 +928,7 @@ class TestWorkflows(
generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination)
shutil.copy(self.SIMPLE_PDF, destination)
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
doc.refresh_from_db()
@@ -2067,7 +2067,7 @@ class TestWorkflows(
superuser = UserFactory(username="superuser", superuser=True)
self.client.force_authenticate(user=superuser)
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
tasks.consume_file(
@@ -3053,7 +3053,7 @@ class TestWorkflows(
generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination)
shutil.copy(self.SIMPLE_PDF, destination)
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
doc.refresh_from_db()
doc.tags.set([self.t1, self.t2])
@@ -3117,7 +3117,7 @@ class TestWorkflows(
generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination)
shutil.copy(self.SIMPLE_PDF, destination)
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
doc.refresh_from_db()
doc.tags.set([self.t1])
@@ -3266,7 +3266,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -3390,7 +3390,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -3540,7 +3540,7 @@ class TestWorkflows(
workflow.actions.set([assignment_action, email_action])
temp_working_copy = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "working-copy.pdf",
)
@@ -3640,7 +3640,7 @@ class TestWorkflows(
# move the file
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -3748,7 +3748,7 @@ class TestWorkflows(
generated = generate_unique_filename(doc)
destination = (settings.ORIGINALS_DIR / generated).resolve()
create_source_path_directory(destination)
shutil.copy(self.SIMPLE_PDF, destination)
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
@@ -3959,7 +3959,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -4080,7 +4080,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -4431,7 +4431,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -4879,7 +4879,7 @@ class TestWorkflows(
w.save()
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
@@ -5071,7 +5071,7 @@ class TestWorkflows(
# Create a test file to be consumed
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
test_file_path = Path(test_file)
@@ -5135,7 +5135,7 @@ class TestWorkflows(
# Create a test file to be consumed
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple2.pdf",
)
test_file_path = Path(test_file)
@@ -5639,7 +5639,7 @@ class TestDateWorkflowLocalization(
)
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
tmp_path / "simple.pdf",
)
@@ -5706,7 +5706,7 @@ class TestRemoteOCRWorkflowAction(DirectoriesMixin, SampleDirMixin, APITestCase)
self._make_workflow(WorkflowTrigger.WorkflowTriggerType.CONSUMPTION)
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
overrides = DocumentMetadataOverrides()
@@ -5924,7 +5924,7 @@ class TestApplyAISuggestionsWorkflowAction(
)
test_file = shutil.copy(
self.SIMPLE_PDF,
self.SAMPLE_DIR / "simple.pdf",
self.dirs.scratch_dir / "simple.pdf",
)
-3
View File
@@ -11,7 +11,6 @@ from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource
from paperless_testing.fakes.progress import FakeProgressManager
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
class ConsumeTaskMixin:
@@ -54,8 +53,6 @@ class SampleDirMixin:
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
SIMPLE_PDF = SIMPLE_DIGITAL_PDF
class GetConsumerMixin:
@contextmanager
+10 -3
View File
@@ -5267,7 +5267,9 @@ class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Custom
"migration_status": inline_serializer(
name="MigrationStatus",
fields={
"latest_migration": serializers.CharField(),
"latest_migration": serializers.CharField(
allow_null=True,
),
"unapplied_migrations": serializers.ListSerializer(
child=serializers.CharField(),
),
@@ -5342,6 +5344,8 @@ class SystemStatusView(PassUserMixin):
db_conn = connections["default"]
db_url = str(db_conn.settings_dict["NAME"])
db_error = None
all_migrations: list[str] = []
applied_migrations: list[str] = []
try:
db_conn.ensure_connection()
@@ -5352,7 +5356,8 @@ class SystemStatusView(PassUserMixin):
f"{m.app}.{m.name}"
for m in MigrationRecorder.Migration.objects.all().order_by("id")
]
except Exception as e: # pragma: no cover
except Exception as e:
all_migrations = []
applied_migrations = []
db_status = "ERROR"
logger.exception(
@@ -5548,7 +5553,9 @@ class SystemStatusView(PassUserMixin):
"status": db_status,
"error": db_error,
"migration_status": {
"latest_migration": applied_migrations[-1],
"latest_migration": (
applied_migrations[-1] if applied_migrations else None
),
"unapplied_migrations": [
m for m in all_migrations if m not in applied_migrations
],
+68 -72
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-29 17:25+0000\n"
"PO-Revision-Date: 2026-10-29 17:27\n"
"POT-Creation-Date: 2026-10-19 15:40+0000\n"
"PO-Revision-Date: 2026-10-19 15:42\n"
"Last-Translator: \n"
"Language-Team: Afrikaans\n"
"Language: af_ZA\n"
@@ -21,43 +21,39 @@ msgstr ""
msgid "Documents"
msgstr "Dokumente"
#: documents/filters.py:337
msgid "Expected a date or a relative date such as \"today\" or \"in 30 days\"."
msgstr ""
#: documents/filters.py:513
#: documents/filters.py:464
msgid "Value must be valid JSON."
msgstr "Waarde moet geldige JSON wees."
#: documents/filters.py:534
#: documents/filters.py:483
msgid "Invalid custom field query expression"
msgstr "Ongeldige gepasmaakte veldnavraaguitdrukking"
#: documents/filters.py:544
#: documents/filters.py:493
msgid "Invalid expression list. Must be nonempty."
msgstr "Ongeldige uitdrukking lys. Moet nie leeg wees nie."
#: documents/filters.py:565
#: documents/filters.py:514
msgid "Invalid logical operator {op!r}"
msgstr "Ongeldige logiese uitdrukking {op!r}"
#: documents/filters.py:579
#: documents/filters.py:528
msgid "Maximum number of query conditions exceeded."
msgstr ""
#: documents/filters.py:643
#: documents/filters.py:592
msgid "{name!r} is not a valid custom field."
msgstr ""
#: documents/filters.py:680
#: documents/filters.py:629
msgid "{data_type} does not support query expr {expr!r}."
msgstr ""
#: documents/filters.py:801 documents/models.py:137
#: documents/filters.py:748 documents/models.py:137
msgid "Maximum nesting depth exceeded."
msgstr ""
#: documents/filters.py:1178
#: documents/filters.py:1125
msgid "Custom field not found"
msgstr ""
@@ -1639,48 +1635,48 @@ msgstr ""
msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:360 documents/views.py:2730
#: documents/serialisers.py:515 documents/serialisers.py:872
#: documents/serialisers.py:2902 documents/views.py:363 documents/views.py:2768
#: paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
#: documents/serialisers.py:676
#: documents/serialisers.py:708
msgid "Invalid color."
msgstr "Ongeldige kleur."
#: documents/serialisers.py:2339
#: documents/serialisers.py:2369
#, python-format
msgid "File type %(type)s not supported"
msgstr "Lêertipe %(type)s word nie ondersteun nie"
#: documents/serialisers.py:2383
#: documents/serialisers.py:2413
#, python-format
msgid "Custom field id must be an integer: %(id)s"
msgstr ""
#: documents/serialisers.py:2390
#: documents/serialisers.py:2420
#, python-format
msgid "Custom field with id %(id)s does not exist"
msgstr ""
#: documents/serialisers.py:2407 documents/serialisers.py:2417
#: documents/serialisers.py:2437 documents/serialisers.py:2447
msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr ""
#: documents/serialisers.py:2412
#: documents/serialisers.py:2442
msgid "Some custom fields don't exist or were specified twice."
msgstr ""
#: documents/serialisers.py:2559
#: documents/serialisers.py:2589
msgid "Invalid variable detected."
msgstr "Ongeldige veranderlike bespeur."
#: documents/serialisers.py:2928
#: documents/serialisers.py:2958
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4780
#: documents/serialisers.py:2988 documents/views.py:4816
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1920,40 +1916,40 @@ msgstr ""
msgid ", "
msgstr ""
#: documents/views.py:315
#: documents/views.py:318
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:353 documents/views.py:2727
#: documents/views.py:356 documents/views.py:2765
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1665
#: documents/views.py:1703
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1676
#: documents/views.py:1714
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1688
#: documents/views.py:1726
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4796
#: documents/views.py:4832
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4843
#: documents/views.py:4879
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4909
#: documents/views.py:4945
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4923
#: documents/views.py:4959
msgid "The share link bundle is unavailable."
msgstr ""
@@ -2245,151 +2241,151 @@ msgstr ""
msgid "paperless application settings"
msgstr ""
#: paperless/settings/__init__.py:566
#: paperless/settings/__init__.py:562
msgid "English (US)"
msgstr "Engels (VS)"
#: paperless/settings/__init__.py:567
#: paperless/settings/__init__.py:563
msgid "Arabic"
msgstr "Arabies"
#: paperless/settings/__init__.py:568
#: paperless/settings/__init__.py:564
msgid "Afrikaans"
msgstr ""
#: paperless/settings/__init__.py:569
#: paperless/settings/__init__.py:565
msgid "Belarusian"
msgstr "Belorussies"
#: paperless/settings/__init__.py:570
#: paperless/settings/__init__.py:566
msgid "Bulgarian"
msgstr ""
#: paperless/settings/__init__.py:571
#: paperless/settings/__init__.py:567
msgid "Catalan"
msgstr "Katalaans"
#: paperless/settings/__init__.py:572
#: paperless/settings/__init__.py:568
msgid "Czech"
msgstr "Tsjeggies"
#: paperless/settings/__init__.py:573
#: paperless/settings/__init__.py:569
msgid "Danish"
msgstr "Deens"
#: paperless/settings/__init__.py:574
#: paperless/settings/__init__.py:570
msgid "German"
msgstr "Duits"
#: paperless/settings/__init__.py:575
#: paperless/settings/__init__.py:571
msgid "Greek"
msgstr ""
#: paperless/settings/__init__.py:576
#: paperless/settings/__init__.py:572
msgid "English (GB)"
msgstr "Engels (GB)"
#: paperless/settings/__init__.py:577
#: paperless/settings/__init__.py:573
msgid "Spanish"
msgstr "Spaans"
#: paperless/settings/__init__.py:578
#: paperless/settings/__init__.py:574
msgid "Persian"
msgstr ""
#: paperless/settings/__init__.py:579
#: paperless/settings/__init__.py:575
msgid "Finnish"
msgstr "Fins"
#: paperless/settings/__init__.py:580
#: paperless/settings/__init__.py:576
msgid "French"
msgstr "Frans"
#: paperless/settings/__init__.py:581
#: paperless/settings/__init__.py:577
msgid "Hungarian"
msgstr ""
#: paperless/settings/__init__.py:582
#: paperless/settings/__init__.py:578
msgid "Indonesian"
msgstr ""
#: paperless/settings/__init__.py:583
#: paperless/settings/__init__.py:579
msgid "Italian"
msgstr "Italiaans"
#: paperless/settings/__init__.py:584
#: paperless/settings/__init__.py:580
msgid "Japanese"
msgstr ""
#: paperless/settings/__init__.py:585
#: paperless/settings/__init__.py:581
msgid "Korean"
msgstr ""
#: paperless/settings/__init__.py:586
#: paperless/settings/__init__.py:582
msgid "Luxembourgish"
msgstr "Luxemburgs"
#: paperless/settings/__init__.py:587
#: paperless/settings/__init__.py:583
msgid "Norwegian"
msgstr ""
#: paperless/settings/__init__.py:588
#: paperless/settings/__init__.py:584
msgid "Dutch"
msgstr "Nederlands"
#: paperless/settings/__init__.py:589
#: paperless/settings/__init__.py:585
msgid "Polish"
msgstr "Pools"
#: paperless/settings/__init__.py:590
#: paperless/settings/__init__.py:586
msgid "Portuguese (Brazil)"
msgstr "Portugees (Brasilië)"
#: paperless/settings/__init__.py:591
#: paperless/settings/__init__.py:587
msgid "Portuguese"
msgstr "Portugees"
#: paperless/settings/__init__.py:592
#: paperless/settings/__init__.py:588
msgid "Romanian"
msgstr "Roemeens"
#: paperless/settings/__init__.py:593
#: paperless/settings/__init__.py:589
msgid "Russian"
msgstr "Russies"
#: paperless/settings/__init__.py:594
#: paperless/settings/__init__.py:590
msgid "Slovak"
msgstr ""
#: paperless/settings/__init__.py:595
#: paperless/settings/__init__.py:591
msgid "Slovenian"
msgstr "Sloweens"
#: paperless/settings/__init__.py:596
#: paperless/settings/__init__.py:592
msgid "Serbian"
msgstr "Serwies"
#: paperless/settings/__init__.py:597
#: paperless/settings/__init__.py:593
msgid "Swedish"
msgstr "Sweeds"
#: paperless/settings/__init__.py:598
#: paperless/settings/__init__.py:594
msgid "Turkish"
msgstr "Turks"
#: paperless/settings/__init__.py:599
#: paperless/settings/__init__.py:595
msgid "Ukrainian"
msgstr ""
#: paperless/settings/__init__.py:600
#: paperless/settings/__init__.py:596
msgid "Vietnamese"
msgstr ""
#: paperless/settings/__init__.py:601
#: paperless/settings/__init__.py:597
msgid "Chinese Simplified"
msgstr "Vereenvoudigde Sjinees"
#: paperless/settings/__init__.py:602
#: paperless/settings/__init__.py:598
msgid "Chinese Traditional"
msgstr ""
+68 -72
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-29 17:25+0000\n"
"PO-Revision-Date: 2026-10-29 17:27\n"
"POT-Creation-Date: 2026-10-19 15:40+0000\n"
"PO-Revision-Date: 2026-10-19 15:42\n"
"Last-Translator: \n"
"Language-Team: Amharic\n"
"Language: am_ET\n"
@@ -21,43 +21,39 @@ msgstr ""
msgid "Documents"
msgstr "መዝገባት"
#: documents/filters.py:337
msgid "Expected a date or a relative date such as \"today\" or \"in 30 days\"."
msgstr ""
#: documents/filters.py:513
#: documents/filters.py:464
msgid "Value must be valid JSON."
msgstr "የሚሰራው እሴት \"JSON\" መሆን አለበት"
#: documents/filters.py:534
#: documents/filters.py:483
msgid "Invalid custom field query expression"
msgstr "ልክ ያልሆነ የተወሰነ የቦታ መጠይቅ አገላለጽ"
#: documents/filters.py:544
#: documents/filters.py:493
msgid "Invalid expression list. Must be nonempty."
msgstr "ልክ ያልሆነ የመግለጫ ዝርዝር። ባዶ መሆን የለበትም።"
#: documents/filters.py:565
#: documents/filters.py:514
msgid "Invalid logical operator {op!r}"
msgstr "ልክ ያልሆነ የሎጂክ ኦፕሬተር {op!r}"
#: documents/filters.py:579
#: documents/filters.py:528
msgid "Maximum number of query conditions exceeded."
msgstr "ከፍተኛው የጥያቄ ሁኔታዎች/መጠን ብዛት አልፏል።"
#: documents/filters.py:643
#: documents/filters.py:592
msgid "{name!r} is not a valid custom field."
msgstr "{name!r} ይሄ ታዐማኒነት ያለው ልማድ አይደለም።"
#: documents/filters.py:680
#: documents/filters.py:629
msgid "{data_type} does not support query expr {expr!r}."
msgstr "ጥያቄን አይደግፍም expr {expr!r}."
#: documents/filters.py:801 documents/models.py:137
#: documents/filters.py:748 documents/models.py:137
msgid "Maximum nesting depth exceeded."
msgstr "ከፍተኛው የጥገኝነት ጥልቀት አልፏል።"
#: documents/filters.py:1178
#: documents/filters.py:1125
msgid "Custom field not found"
msgstr "ይህ ልማድ አልተገኘም"
@@ -1639,48 +1635,48 @@ msgstr ""
msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:483 documents/serialisers.py:840
#: documents/serialisers.py:2872 documents/views.py:360 documents/views.py:2730
#: documents/serialisers.py:515 documents/serialisers.py:872
#: documents/serialisers.py:2902 documents/views.py:363 documents/views.py:2768
#: paperless_mail/serialisers.py:156
msgid "Insufficient permissions."
msgstr ""
#: documents/serialisers.py:676
#: documents/serialisers.py:708
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:2339
#: documents/serialisers.py:2369
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/serialisers.py:2383
#: documents/serialisers.py:2413
#, python-format
msgid "Custom field id must be an integer: %(id)s"
msgstr ""
#: documents/serialisers.py:2390
#: documents/serialisers.py:2420
#, python-format
msgid "Custom field with id %(id)s does not exist"
msgstr ""
#: documents/serialisers.py:2407 documents/serialisers.py:2417
#: documents/serialisers.py:2437 documents/serialisers.py:2447
msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr ""
#: documents/serialisers.py:2412
#: documents/serialisers.py:2442
msgid "Some custom fields don't exist or were specified twice."
msgstr ""
#: documents/serialisers.py:2559
#: documents/serialisers.py:2589
msgid "Invalid variable detected."
msgstr ""
#: documents/serialisers.py:2928
#: documents/serialisers.py:2958
msgid "Duplicate document identifiers are not allowed."
msgstr ""
#: documents/serialisers.py:2958 documents/views.py:4780
#: documents/serialisers.py:2988 documents/views.py:4816
#, python-format
msgid "Documents not found: %(ids)s"
msgstr ""
@@ -1920,40 +1916,40 @@ msgstr ""
msgid ", "
msgstr ""
#: documents/views.py:315
#: documents/views.py:318
msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr ""
#: documents/views.py:353 documents/views.py:2727
#: documents/views.py:356 documents/views.py:2765
msgid "Invalid more_like_id"
msgstr ""
#: documents/views.py:1665
#: documents/views.py:1703
msgid "Invalid AI configuration."
msgstr ""
#: documents/views.py:1676
#: documents/views.py:1714
msgid "AI backend request timed out."
msgstr ""
#: documents/views.py:1688
#: documents/views.py:1726
msgid "AI backend rejected the request. Check logs for details."
msgstr ""
#: documents/views.py:4796
#: documents/views.py:4832
#, python-format
msgid "Insufficient permissions to share document %(id)s."
msgstr ""
#: documents/views.py:4843
#: documents/views.py:4879
msgid "Bundle is already being processed."
msgstr ""
#: documents/views.py:4909
#: documents/views.py:4945
msgid "The share link bundle is still being prepared. Please try again later."
msgstr ""
#: documents/views.py:4923
#: documents/views.py:4959
msgid "The share link bundle is unavailable."
msgstr ""
@@ -2245,151 +2241,151 @@ msgstr ""
msgid "paperless application settings"
msgstr ""
#: paperless/settings/__init__.py:566
#: paperless/settings/__init__.py:562
msgid "English (US)"
msgstr ""
#: paperless/settings/__init__.py:567
#: paperless/settings/__init__.py:563
msgid "Arabic"
msgstr ""
#: paperless/settings/__init__.py:568
#: paperless/settings/__init__.py:564
msgid "Afrikaans"
msgstr ""
#: paperless/settings/__init__.py:569
#: paperless/settings/__init__.py:565
msgid "Belarusian"
msgstr ""
#: paperless/settings/__init__.py:570
#: paperless/settings/__init__.py:566
msgid "Bulgarian"
msgstr ""
#: paperless/settings/__init__.py:571
#: paperless/settings/__init__.py:567
msgid "Catalan"
msgstr ""
#: paperless/settings/__init__.py:572
#: paperless/settings/__init__.py:568
msgid "Czech"
msgstr ""
#: paperless/settings/__init__.py:573
#: paperless/settings/__init__.py:569
msgid "Danish"
msgstr ""
#: paperless/settings/__init__.py:574
#: paperless/settings/__init__.py:570
msgid "German"
msgstr ""
#: paperless/settings/__init__.py:575
#: paperless/settings/__init__.py:571
msgid "Greek"
msgstr ""
#: paperless/settings/__init__.py:576
#: paperless/settings/__init__.py:572
msgid "English (GB)"
msgstr ""
#: paperless/settings/__init__.py:577
#: paperless/settings/__init__.py:573
msgid "Spanish"
msgstr ""
#: paperless/settings/__init__.py:578
#: paperless/settings/__init__.py:574
msgid "Persian"
msgstr ""
#: paperless/settings/__init__.py:579
#: paperless/settings/__init__.py:575
msgid "Finnish"
msgstr ""
#: paperless/settings/__init__.py:580
#: paperless/settings/__init__.py:576
msgid "French"
msgstr ""
#: paperless/settings/__init__.py:581
#: paperless/settings/__init__.py:577
msgid "Hungarian"
msgstr ""
#: paperless/settings/__init__.py:582
#: paperless/settings/__init__.py:578
msgid "Indonesian"
msgstr ""
#: paperless/settings/__init__.py:583
#: paperless/settings/__init__.py:579
msgid "Italian"
msgstr ""
#: paperless/settings/__init__.py:584
#: paperless/settings/__init__.py:580
msgid "Japanese"
msgstr ""
#: paperless/settings/__init__.py:585
#: paperless/settings/__init__.py:581
msgid "Korean"
msgstr ""
#: paperless/settings/__init__.py:586
#: paperless/settings/__init__.py:582
msgid "Luxembourgish"
msgstr ""
#: paperless/settings/__init__.py:587
#: paperless/settings/__init__.py:583
msgid "Norwegian"
msgstr ""
#: paperless/settings/__init__.py:588
#: paperless/settings/__init__.py:584
msgid "Dutch"
msgstr ""
#: paperless/settings/__init__.py:589
#: paperless/settings/__init__.py:585
msgid "Polish"
msgstr ""
#: paperless/settings/__init__.py:590
#: paperless/settings/__init__.py:586
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings/__init__.py:591
#: paperless/settings/__init__.py:587
msgid "Portuguese"
msgstr ""
#: paperless/settings/__init__.py:592
#: paperless/settings/__init__.py:588
msgid "Romanian"
msgstr ""
#: paperless/settings/__init__.py:593
#: paperless/settings/__init__.py:589
msgid "Russian"
msgstr ""
#: paperless/settings/__init__.py:594
#: paperless/settings/__init__.py:590
msgid "Slovak"
msgstr ""
#: paperless/settings/__init__.py:595
#: paperless/settings/__init__.py:591
msgid "Slovenian"
msgstr ""
#: paperless/settings/__init__.py:596
#: paperless/settings/__init__.py:592
msgid "Serbian"
msgstr ""
#: paperless/settings/__init__.py:597
#: paperless/settings/__init__.py:593
msgid "Swedish"
msgstr ""
#: paperless/settings/__init__.py:598
#: paperless/settings/__init__.py:594
msgid "Turkish"
msgstr ""
#: paperless/settings/__init__.py:599
#: paperless/settings/__init__.py:595
msgid "Ukrainian"
msgstr ""
#: paperless/settings/__init__.py:600
#: paperless/settings/__init__.py:596
msgid "Vietnamese"
msgstr ""
#: paperless/settings/__init__.py:601
#: paperless/settings/__init__.py:597
msgid "Chinese Simplified"
msgstr ""
#: paperless/settings/__init__.py:602
#: paperless/settings/__init__.py:598
msgid "Chinese Traditional"
msgstr ""
Loaded 100 of 168 files, more files were not shown because too many files have changed in this diff. Show more