Compare commits

..

98 Commits

Author SHA1 Message Date
Jonas Winkler
8da85d3609 Update ci.yml 2021-02-22 13:25:21 +01:00
jonaswinkler
127d30918d lets hope this works! 2021-02-22 12:03:07 +01:00
jonaswinkler
3b553f6455 changelog 2021-02-22 11:53:13 +01:00
jonaswinkler
6d934da5dd Revert "associate error messages with documents"
This reverts commit aa3d91a3
2021-02-22 11:52:54 +01:00
jonaswinkler
aa3d91a338 associate error messages with documents 2021-02-22 11:38:16 +01:00
jonaswinkler
d64818b46c fixes #591 2021-02-22 11:11:04 +01:00
jonaswinkler
99a18516b2 tests 2021-02-22 00:17:16 +01:00
jonaswinkler
30b0a30146 dropdown menu shadows 2021-02-22 00:04:44 +01:00
jonaswinkler
cb10617979 enable deskewing and rotation by default 2021-02-21 23:40:26 +01:00
jonaswinkler
265432f2a5 fix up the ocrmypdf parameter construction for clean-final and redo 2021-02-21 23:39:19 +01:00
jonaswinkler
a13e9f23b1 use archived file for thumbnail, if available 2021-02-21 23:30:14 +01:00
jonaswinkler
65b37f61ca update thumbnail in archiver, since page rotation might have changed 2021-02-21 23:29:52 +01:00
jonaswinkler
7751755399 Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2021-02-21 22:27:58 +01:00
jonaswinkler
14e2ad7bc4 more parameter checking 2021-02-21 22:19:24 +01:00
jonaswinkler
dfc23a2b38 bugfix for tika parser 2021-02-21 21:36:43 +01:00
Jonas Winkler
d2fc840293 Merge pull request #587 from jonaswinkler/translations_src-ui-messages-xlf--dev_fr
Translate '/src-ui/messages.xlf' in 'fr'
2021-02-21 16:01:53 +01:00
Jonas Winkler
37fe6fb9c3 Merge pull request #586 from jonaswinkler/translations_src-locale-en-us-lc-messages-django-po--dev_fr
Translate '/src/locale/en_US/LC_MESSAGES/django.po' in 'fr'
2021-02-21 16:01:41 +01:00
transifex-integration[bot]
a21ec76997 Translate /src-ui/messages.xlf in fr
translation completed for the source file '/src-ui/messages.xlf'
on the 'fr' language.
2021-02-21 14:16:49 +00:00
transifex-integration[bot]
501d8d9683 Apply translations in fr
translation completed for the source file '/src/locale/en_US/LC_MESSAGES/django.po'
on the 'fr' language.
2021-02-21 14:15:43 +00:00
jonaswinkler
8562ca9a77 Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2021-02-21 13:44:15 +01:00
jonaswinkler
29641e5d66 Merge branch 'master' into dev 2021-02-21 13:44:07 +01:00
jonaswinkler
ee7308be2d documentation on how to build the documentation 2021-02-21 13:43:54 +01:00
jonaswinkler
ef4009e94f documentation 2021-02-21 13:35:47 +01:00
Jonas Winkler
27d2ae6976 Merge pull request #585 from jonaswinkler/translations_src-ui-messages-xlf--dev_en_GB
Translate '/src-ui/messages.xlf' in 'en_GB'
2021-02-21 13:12:31 +01:00
transifex-integration[bot]
0f9675f9d6 Translate /src-ui/messages.xlf in en_GB
translation completed for the source file '/src-ui/messages.xlf'
on the 'en_GB' language.
2021-02-21 12:12:19 +00:00
jonaswinkler
bac4a63cc8 run the polling file change checks on individual threads to speed up queueing of new files 2021-02-21 12:43:55 +01:00
jonaswinkler
0453787d38 increased default delay when waiting for file changes with polling 2021-02-21 12:14:54 +01:00
jonaswinkler
afc3e41f13 changelog 2021-02-21 01:48:14 +01:00
jonaswinkler
86d6316cc9 version bump 2021-02-21 01:30:03 +01:00
jonaswinkler
7b2c1f82f5 documentation 2021-02-21 01:29:55 +01:00
jonaswinkler
e2a932d744 update dependencies 2021-02-21 00:24:33 +01:00
jonaswinkler
b978994525 documentation for the new configuration options 2021-02-21 00:23:01 +01:00
jonaswinkler
6da237dd9e pycodestyle 2021-02-21 00:21:43 +01:00
jonaswinkler
50c1978d36 tests 2021-02-21 00:18:34 +01:00
jonaswinkler
fdb310c497 changelog 2021-02-21 00:17:12 +01:00
jonaswinkler
ce121a261d completely reworked the OCRmyPDF parser. 2021-02-21 00:16:57 +01:00
jonaswinkler
ebdfd4241a Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2021-02-21 00:13:17 +01:00
jonaswinkler
9cbb1c5726 add some test files 2021-02-21 00:13:08 +01:00
Jonas Winkler
85dabccbe7 Merge pull request #579 from jonaswinkler/translations_src-ui-messages-xlf--dev_nl_NL
Translate '/src-ui/messages.xlf' in 'nl_NL'
2021-02-20 19:01:08 +01:00
transifex-integration[bot]
a9a8189d4b Translate /src-ui/messages.xlf in nl_NL
translation completed for the source file '/src-ui/messages.xlf'
on the 'nl_NL' language.
2021-02-20 17:45:27 +00:00
Jonas Winkler
30579112d2 Merge pull request #578 from jonaswinkler/translations_src-ui-messages-xlf--dev_de
Translate '/src-ui/messages.xlf' in 'de'
2021-02-20 16:45:20 +01:00
transifex-integration[bot]
ccfd009c1a Translate /src-ui/messages.xlf in de
translation completed for the source file '/src-ui/messages.xlf'
on the 'de' language.
2021-02-20 15:44:19 +00:00
jonaswinkler
044a939623 Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2021-02-20 16:11:25 +01:00
jonaswinkler
203bc162cd front end support for downloading multiple documents 2021-02-20 16:10:50 +01:00
jonaswinkler
31f03ef1d3 API support for downloading compressed archives of multiple documents 2021-02-20 16:09:29 +01:00
Jonas Winkler
4d3552dc64 Merge pull request #570 from jonaswinkler/translations_src-ui-messages-xlf--dev_en_GB
Translate '/src-ui/messages.xlf' in 'en_GB'
2021-02-19 12:18:46 +01:00
Jonas Winkler
ea8a52404f Merge pull request #569 from jonaswinkler/translations_src-ui-messages-xlf--dev_de
Translate '/src-ui/messages.xlf' in 'de'
2021-02-19 12:18:35 +01:00
Jonas Winkler
0ae9aecdef Update README.md 2021-02-19 11:51:59 +01:00
jonaswinkler
4de4789605 this took way too much time 2021-02-19 11:34:51 +01:00
jonaswinkler
950bb46827 version bump 2021-02-19 11:31:14 +01:00
Jonas Winkler
44936dc5f0 Update README.md 2021-02-19 11:24:21 +01:00
Jonas Winkler
1140a878b4 Update README.md 2021-02-19 11:22:43 +01:00
transifex-integration[bot]
efb49af7ac Translate /src-ui/messages.xlf in en_GB
translation completed for the source file '/src-ui/messages.xlf'
on the 'en_GB' language.
2021-02-18 16:44:55 +00:00
transifex-integration[bot]
b5a8106a6a Translate /src-ui/messages.xlf in de
translation completed for the source file '/src-ui/messages.xlf'
on the 'de' language.
2021-02-18 16:43:41 +00:00
jonaswinkler
0f80eee54e refactored most of the list view; fixes #147, much snappier UX when switching between views 2021-02-18 17:29:21 +01:00
jonaswinkler
0e237fa459 messages 2021-02-18 17:11:47 +01:00
Jonas Winkler
702b985ceb Merge pull request #558 from jonaswinkler/translations_src-locale-en-us-lc-messages-django-po--dev_de
Translate '/src/locale/en_US/LC_MESSAGES/django.po' in 'de'
2021-02-17 14:46:25 +01:00
Jonas Winkler
7d87bcbb98 Merge pull request #560 from jonaswinkler/translations_src-locale-en-us-lc-messages-django-po--dev_en_GB
Translate '/src/locale/en_US/LC_MESSAGES/django.po' in 'en_GB'
2021-02-17 14:46:14 +01:00
Jonas Winkler
340521aa0d Merge pull request #559 from jonaswinkler/translations_src-locale-en-us-lc-messages-django-po--dev_nl_NL
Translate '/src/locale/en_US/LC_MESSAGES/django.po' in 'nl_NL'
2021-02-17 14:46:03 +01:00
transifex-integration[bot]
7bc557a999 Apply translations in en_GB
translation completed for the source file '/src/locale/en_US/LC_MESSAGES/django.po'
on the 'en_GB' language.
2021-02-17 13:36:15 +00:00
jonaswinkler
dfa7cdf47e Merge branch 'dev' of github.com:jonaswinkler/paperless-ng into dev 2021-02-17 14:26:26 +01:00
jonaswinkler
0d78e58d77 fixed paperless not properly selecting en-gb 2021-02-17 14:26:06 +01:00
transifex-integration[bot]
58df3d5767 Apply translations in nl_NL
translation completed for the source file '/src/locale/en_US/LC_MESSAGES/django.po'
on the 'nl_NL' language.
2021-02-17 12:57:48 +00:00
Jonas Winkler
4e4d6e806c Update Crowdin configuration file 2021-02-17 13:22:45 +01:00
Jonas Winkler
6ff99945f3 Update Crowdin configuration file 2021-02-17 13:18:19 +01:00
transifex-integration[bot]
b7f1b9f8ad Apply translations in de
translation completed for the source file '/src/locale/en_US/LC_MESSAGES/django.po'
on the 'de' language.
2021-02-17 11:50:01 +00:00
jonaswinkler
08a44cf468 changelog and version 2021-02-17 12:31:19 +01:00
jonaswinkler
a1162d6d5a update requirements 2021-02-17 12:25:34 +01:00
jonaswinkler
1c81d88013 add support for iso 8601 date display 2021-02-17 12:15:22 +01:00
jonaswinkler
1e4ec7e29e added en-GB language 2021-02-16 14:54:18 +01:00
jonaswinkler
2c4e34dd0c changelog 2021-02-15 23:44:48 +01:00
jonaswinkler
cb308fae7b only show inbox statistics if inbox tags are defined 2021-02-15 23:14:54 +01:00
jonaswinkler
3f03d51b24 version bump 2021-02-15 16:52:45 +01:00
jonaswinkler
831db6ab87 note regarding Python 3.6 2021-02-15 16:46:06 +01:00
jonaswinkler
43fdf634f2 added a note regarding python 3.6 2021-02-15 16:37:44 +01:00
jonaswinkler
f07a6b4586 PAPERLESS_WEBSERVER_WORKERS option 2021-02-15 16:27:35 +01:00
jonaswinkler
2fcf484229 bugfix dismissing wrong status messages 2021-02-15 14:52:47 +01:00
jonaswinkler
8bf4241b16 some search index optimizations 2021-02-15 13:26:36 +01:00
jonaswinkler
56bd966c02 local import of ocrmypdf so that the webserver does not load that 2021-02-15 12:18:10 +01:00
jonaswinkler
416101d557 only import dateparser when required 2021-02-15 11:52:46 +01:00
jonaswinkler
c330cca2c9 remove unused imports 2021-02-15 11:26:13 +01:00
jonaswinkler
7e88085377 load sklearn modules only when training data has changed 2021-02-15 11:25:25 +01:00
jonaswinkler
5e669534f2 reorganized test case 2021-02-14 17:24:31 +01:00
jonaswinkler
98b147b622 better sanity checker that logs messages in the log files and does not fail on warnings. 2021-02-14 17:08:29 +01:00
jonaswinkler
df6c59bc4f update dependencies 2021-02-14 15:38:47 +01:00
jonaswinkler
6e48da41e5 changelog 2021-02-14 14:05:42 +01:00
Jonas Winkler
5c8a01a6e8 Merge pull request #538 from jonaswinkler/translations_src-locale-en-us-lc-messages-django-po--dev_cs
Translate '/src/locale/en-us/LC_MESSAGES/django.po' in 'cs'
2021-02-14 13:41:33 +01:00
jonaswinkler
3d0a52c25f only load channels app if DEBUG is enabled; its only purpose is to monkey-patch the runserver command. 2021-02-14 12:50:30 +01:00
jonaswinkler
43c729568b release worker memory after tasks are done. 2021-02-14 12:29:55 +01:00
transifex-integration[bot]
62caeed283 Apply translations in cs
translation completed for the source file '/src/locale/en-us/LC_MESSAGES/django.po'
on the 'cs' language.
2021-02-14 07:05:05 +00:00
jonaswinkler
12836d4c68 revert django-q configuration 2021-02-13 20:25:52 +01:00
jonaswinkler
b48e67d714 revert a faulty change that caused memory usage to explode #537 2021-02-13 19:51:04 +01:00
jonaswinkler
f91f4d71bb Merge branch 'master' into dev 2021-02-13 18:09:14 +01:00
jonaswinkler
0a1f264c71 Gotenberg troubleshooting 2021-02-13 18:09:00 +01:00
jonaswinkler
64d61ae2fa version bump 2021-02-13 18:01:19 +01:00
jonaswinkler
5f0e800f6e metadata tab not showing anything if files are missing #534 2021-02-13 16:41:03 +01:00
jonaswinkler
8b2965d55b added sanity checker management command for manual execution #534 2021-02-13 16:39:29 +01:00
jonaswinkler
ed478a1d73 change thumbnail display for extra wide images #433 2021-02-12 18:20:17 +01:00
80 changed files with 3157 additions and 1330 deletions

View File

@@ -35,8 +35,6 @@ jobs:
-
name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends libpoppler-cpp-dev
pip install --upgrade pipenv
pipenv install --system --dev --ignore-pipfile
-
@@ -81,7 +79,7 @@ jobs:
name: Prepare tests
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends libpoppler-cpp-dev unpaper tesseract-ocr imagemagick ghostscript optipng
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng
pip install --upgrade pipenv
pipenv install --system --dev --ignore-pipfile
-
@@ -114,6 +112,13 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: '15'
-
name: Configure version on dev branches
if: startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev'
run: |
git_hash=$(git rev-parse --short "$GITHUB_SHA")
git_branch=${GITHUB_REF#refs/heads/}
sed -i -E "s/version: \"(.*)\"/version: \"${git_branch} ${git_hash}\"/g" src-ui/src/environments/environment.prod.ts
-
name: Build frontend
run: ./compile-frontend.sh
@@ -140,7 +145,7 @@ jobs:
name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends libpoppler-cpp-dev gettext liblept5
sudo apt-get install -qq --no-install-recommends gettext liblept5
pip3 install -r requirements.txt
-
name: Download frontend artifact

View File

@@ -67,7 +67,6 @@ COPY requirements.txt ../
RUN apt-get update \
&& apt-get -y --no-install-recommends install \
build-essential \
libpoppler-cpp-dev \
libpq-dev \
libqpdf-dev \
&& python3 -m pip install --upgrade --no-cache-dir supervisor \

View File

@@ -23,7 +23,6 @@ imap-tools = "*"
langdetect = "*"
# numpy 1.20.0 drops python 3.6 support
numpy = "~=1.19.5"
pdftotext = "*"
pathvalidate = "*"
# pinned to 8.1.0, since aarch64 wheels might not be available beyond that https://github.com/python-pillow/Pillow/issues/5202
pillow = "==8.1.0"
@@ -39,7 +38,7 @@ scikit-learn="==0.24.0"
# Prevent scipy updates because 1.6 is incompatible with python 3.6
scipy="~=1.5.4"
whitenoise = "~=5.2.0"
watchdog = "*"
watchdog = "~=1.0.0"
whoosh="~=2.7.4"
inotifyrecursive = "~=0.3.4"
ocrmypdf = "~=11.6"
@@ -51,11 +50,11 @@ channels = "~=3.0"
channels-redis = "*"
uvicorn = {extras = ["standard"], version = "*"}
concurrent-log-handler = "*"
django-redis = "*"
# uvloop 0.15+ incompatible with python 3.6
uvloop = "~=0.14.0"
# TODO: keep an eye on piwheel builds and update this once available (https://www.piwheels.org/project/cryptography/)
cryptography = "~=3.3.2"
"pdfminer.six" = "*"
[dev-packages]
coveralls = "*"

167
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "b3bed0a6b8981e8fffc1b6aa3bc35a0b1472f28e6f745c62469eb8045740e57b"
"sha256": "71959eb287fc97969263be5e3a1b1f4f369b7a5ace85bd1947a25b9b92e17e8a"
},
"pipfile-spec": 6,
"requires": {},
@@ -60,11 +60,11 @@
},
"autobahn": {
"hashes": [
"sha256:93df8fc9d1821c9dabff9fed52181a9ad6eea5e9989d53102c391607d7c1666e",
"sha256:cceed2121b7a93024daa93c91fae33007f8346f0e522796421f36a6183abea99"
"sha256:41a3a3f89cde48643baf4e105d9491c566295f9abee951379e59121784044b8b",
"sha256:7e6b1bf95196b733978bab2d54a7ab8899c16ce11be369dc58422c07b7eea726"
],
"markers": "python_version >= '3.6'",
"version": "==21.1.1"
"version": "==21.2.1"
},
"automat": {
"hashes": [
@@ -90,47 +90,47 @@
},
"cffi": {
"hashes": [
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
"sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d",
"sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a",
"sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec",
"sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362",
"sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668",
"sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c",
"sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b",
"sha256:23f318bf74b170c6e9adb390e8bd282457f6de46c19d03b52f3fd042b5e19654",
"sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06",
"sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698",
"sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2",
"sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c",
"sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7",
"sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009",
"sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03",
"sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b",
"sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e",
"sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909",
"sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53",
"sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35",
"sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26",
"sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b",
"sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01",
"sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb",
"sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293",
"sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd",
"sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d",
"sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3",
"sha256:be8661bcee1bc2fc4b033a6ab65bd1f87ce5008492601695d0b9a4e820c3bde5",
"sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d",
"sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e",
"sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca",
"sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d",
"sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775",
"sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375",
"sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b",
"sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b",
"sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"
"sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813",
"sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06",
"sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea",
"sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee",
"sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396",
"sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73",
"sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315",
"sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1",
"sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49",
"sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892",
"sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482",
"sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058",
"sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5",
"sha256:5560dbf8deedbffb638d8a2da31da91094db361cc07f8a501a339b2daae2cbcc",
"sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53",
"sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045",
"sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3",
"sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5",
"sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e",
"sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c",
"sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369",
"sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827",
"sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053",
"sha256:9338beed13d880320450d95c9e07ccf839faa3ea7b75d788f4ed46d845044a71",
"sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa",
"sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4",
"sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322",
"sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132",
"sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62",
"sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa",
"sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0",
"sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396",
"sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e",
"sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991",
"sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6",
"sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1",
"sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406",
"sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d",
"sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"
],
"version": "==1.14.4"
"version": "==1.14.5"
},
"channels": {
"hashes": [
@@ -227,11 +227,11 @@
},
"django": {
"hashes": [
"sha256:169e2e7b4839a7910b393eec127fd7cbae62e80fa55f89c6510426abf673fe5f",
"sha256:c6c0462b8b361f8691171af1fb87eceb4442da28477e12200c40420176206ba7"
"sha256:32ce792ee9b6a0cbbec340123e229ac9f765dff8c2a4ae9247a14b2ba3a365a7",
"sha256:baf099db36ad31f970775d0be5587cc58a6256a6771a44eb795b554d45f211b8"
],
"index": "pypi",
"version": "==3.1.6"
"version": "==3.1.7"
},
"django-cors-headers": {
"hashes": [
@@ -273,15 +273,6 @@
"index": "pypi",
"version": "==1.3.4"
},
"django-redis": {
"hashes": [
"sha256:1133b26b75baa3664164c3f44b9d5d133d1b8de45d94d79f38d1adc5b1d502e5",
"sha256:306589c7021e6468b2656edc89f62b8ba67e8d5a1c8877e2688042263daa7a63",
"sha256:f2b25b62cc95b63b7059aaf8e81710e7eea94678e545d31c46e47a6f4af99e56"
],
"index": "pypi",
"version": "==4.12.1"
},
"djangorestframework": {
"hashes": [
"sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7",
@@ -600,11 +591,11 @@
},
"ocrmypdf": {
"hashes": [
"sha256:a54634d017a2f44aa2115b0b6ae5aa41a7cec018f5c53d16ad3abec1e70b3db7",
"sha256:d0e2da48d4abd90f48f0937b2cd4ba57503b56c603f5e3aa91e20e3b21a036cd"
"sha256:0f624456a50be0b0bc8c0b59704d159f637616c093a1cabe8bb383706561bcf7",
"sha256:b829ad640a6160423162012e094ee2f7cd074ec99efadd7f7486954ec9182985"
],
"index": "pypi",
"version": "==11.6.0"
"version": "==11.6.2"
},
"pathvalidate": {
"hashes": [
@@ -619,15 +610,8 @@
"sha256:b9aac0ebeafb21c08bf65f2039f4b2c5f78a3449d0a41df711d72445649e952a",
"sha256:d78877ba8d8bf957f3bb636c4f73f4f6f30f56c461993877ac22c39c20837509"
],
"markers": "python_version >= '3.4'",
"version": "==20201018"
},
"pdftotext": {
"hashes": [
"sha256:98aeb8b07a4127e1a30223bd933ef080bbd29aa88f801717ca6c5618380b8aa6"
],
"index": "pypi",
"version": "==2.1.5"
"version": "==20201018"
},
"pikepdf": {
"hashes": [
@@ -850,11 +834,11 @@
},
"python-magic": {
"hashes": [
"sha256:356efa93c8899047d1eb7d3eb91e871ba2f5b1376edbaf4cc305e3c872207355",
"sha256:b757db2a5289ea3f1ced9e60f072965243ea43a2221430048fd8cacab17be0ce"
"sha256:8551e804c09a3398790bd9e392acb26554ae2609f29c72abb0b9dee9a5571eae",
"sha256:ca884349f2c92ce830e3f498c5b7c7051fe2942c3ee4332f65213b8ebff15a62"
],
"index": "pypi",
"version": "==0.4.18"
"version": "==0.4.22"
},
"pytz": {
"hashes": [
@@ -1113,11 +1097,11 @@
},
"tqdm": {
"hashes": [
"sha256:2874fa525c051177583ec59c0fb4583e91f28ccd3f217ffad2acdb32d2c789ac",
"sha256:ab9b659241d82b8b51b2269ee243ec95286046bf06015c4e15a947cc15914211"
"sha256:65185676e9fdf20d154cffd1c5de8e39ef9696ff7e59fe0156b1b08e468736af",
"sha256:70657337ec104eb4f3fb229285358f23f045433f6aea26846cdd55f0fd68945c"
],
"index": "pypi",
"version": "==4.56.1"
"version": "==4.57.0"
},
"twisted": {
"extras": [
@@ -1155,11 +1139,11 @@
},
"txaio": {
"hashes": [
"sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549",
"sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f"
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
],
"markers": "python_version >= '3.6'",
"version": "==20.12.1"
"version": "==21.2.1"
},
"tzlocal": {
"hashes": [
@@ -1181,11 +1165,11 @@
"standard"
],
"hashes": [
"sha256:1079c50a06f6338095b4f203e7861dbff318dde5f22f3a324fc6e94c7654164c",
"sha256:ef1e0bb5f7941c6fe324e06443ddac0331e1632a776175f87891c7bd02694355"
"sha256:3292251b3c7978e8e4a7868f4baf7f7f7bb7e40c759ecc125c37e99cdea34202",
"sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"
],
"index": "pypi",
"version": "==0.13.3"
"version": "==0.13.4"
},
"uvloop": {
"hashes": [
@@ -1229,11 +1213,10 @@
},
"watchgod": {
"hashes": [
"sha256:59700dab7445aa8e6067a5b94f37bae90fc367554549b1ed2e9d0f4f38a90d2a",
"sha256:5fb60afa9558b79736395db1cb60ad3ed59df5c2f507a3ff729220cf1251ffdc",
"sha256:e9cca0ab9c63f17fc85df9fd8bd18156ff00aff04ebe5976cee473f4968c6858"
"sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29",
"sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"
],
"version": "==0.6"
"version": "==0.7"
},
"wcwidth": {
"hashes": [
@@ -1507,11 +1490,11 @@
},
"faker": {
"hashes": [
"sha256:bf2a9b3f8d00a5dada61fc4a3f80fe0d6795c7f02a138a7d2ef2db5817c7d017",
"sha256:d4aecdb877519d06c2fdc01ffc5ecf70658981acf5e13cd07ded9892994ef5c6"
"sha256:31a58ec5a8f4672f24da3b5ddea02c82a712de1de3179b432948e5c34d787aca",
"sha256:aadfe0efe11ecbbbc5b3b0b0fab050c2acbd2d8e5201769546d43d236bfff663"
],
"markers": "python_version >= '3.6'",
"version": "==6.1.1"
"version": "==6.4.1"
},
"filelock": {
"hashes": [
@@ -1649,11 +1632,11 @@
},
"pygments": {
"hashes": [
"sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435",
"sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"
"sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0",
"sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"
],
"markers": "python_version >= '3.5'",
"version": "==2.7.4"
"version": "==2.8.0"
},
"pyparsing": {
"hashes": [
@@ -1846,11 +1829,11 @@
},
"tox": {
"hashes": [
"sha256:65d0e90ceb816638a50d64f4b47b11da767b284c0addda2294cb3cd69bd72425",
"sha256:cf7fef81a3a2434df4d7af2a6d1bf606d2970220addfbe7dea2615bd4bb2c252"
"sha256:89afa9c59c04beb55eda789c7a65feb1a70fde117f85f1bd1c27c66758456e60",
"sha256:ed1e650cf6368bcbc4a071eeeba363c480920e0ed8a9ad1793c7caaa5ad33d49"
],
"index": "pypi",
"version": "==3.21.4"
"version": "==3.22.0"
},
"urllib3": {
"hashes": [

View File

@@ -7,9 +7,16 @@
# Paperless-ng
[Paperless](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and contributors that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.
[Paperless (click me)](https://github.com/the-paperless-project/paperless) is an application by Daniel Quinn and contributors that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents.
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. For a detailed list of changes, have a look at the [change log](https://paperless-ng.readthedocs.io/en/latest/changelog.html) in the documentation.
Paperless-ng is a fork of the original project, adding a new interface and many other changes under the hood. These key points should help you decide whether Paperless-ng is something you would prefer over Paperless:
* Interface: The new front end is the main interface for paperless-ng, the old interface still exists but most customizations (such as thumbnails for the document list) have been removed.
* Encryption: Paperless-ng does not support GnuPG anymore, since storing your data on encrypted file systems (that you optionally mount on demand) achieves about the same result.
* Resource usage: Paperless-ng does use a bit more resources than Paperless. Running the web server requires about 300MB of RAM or more, depending on the configuration. While adding documents, it requires about 300MB additional RAM, depending on the document. It still runs on Pi (many users do that), but it has been generally geared to better use the resources of more powerful systems.
* API changes: If you rely on the REST API of paperless, some of its functionality has been changed.
For a detailed list of changes, have a look at the [change log](https://paperless-ng.readthedocs.io/en/latest/changelog.html) in the documentation.
# How it Works
@@ -32,8 +39,8 @@ Here's what you get:
* Performs OCR on your documents, adds selectable text to image only documents and adds tags, correspondents and document types to your documents.
* Supports PDF documents, images, plain text files, and Office documents (Word, Excel, Powerpoint, and LibreOffice equivalents).
* Office document support is optional and provided by Apache Tika (see [configuration](https://paperless-ng.readthedocs.io/en/latest/configuration.html#tika-settings))
* Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and can be configured freely.
* Single page application front end. Should be pretty snappy. Will be mobile friendly in the future.
* Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely.
* Single page application front end.
* Includes a dashboard that shows basic statistics and has document upload.
* Filtering by tags, correspondents, types, and more.
* Customizable views can be saved and displayed on the dashboard.
@@ -44,14 +51,13 @@ Here's what you get:
* Searching for similar documents ("More like this")
* Email processing: Paperless adds documents from your email accounts.
* Configure multiple accounts and filters for each account.
* When adding documents from mails, paperless can move these mails to a new folder, mark them as read, flag them or delete them.
* When adding documents from mails, paperless can move these mails to a new folder, mark them as read, flag them as important or delete them.
* Machine learning powered document matching.
* Paperless learns from your documents and will be able to automatically assign tags, correspondents and types to documents once you've stored a few documents in paperless.
* A task processor that processes documents in parallel and also tells you when something goes wrong. On modern multi core systems, consumption is blazing fast.
* Optimized for multi core systems: Paperless-ng consumes multiple documents in parallel.
* The integrated sanity checker makes sure that your document archive is in good health.
If you want to see some screenshots of paperless-ng in action, [some are available in the documentation](https://paperless-ng.readthedocs.io/en/latest/screenshots.html). However, some parts of the UI have changed since I took these.
For a complete list of changes from paperless, check out the [changelog](https://paperless-ng.readthedocs.io/en/latest/changelog.html)
If you want to see some screenshots of paperless-ng in action, [some are available in the documentation](https://paperless-ng.readthedocs.io/en/latest/screenshots.html).
# Getting started
@@ -103,4 +109,4 @@ These projects also exist, but their status and compatibility with paperless-ng
# Important Note
Document scanners are typically used to scan sensitive documents. Things like your social insurance number, tax records, invoices, etc. Everything is stored in the clear without encryption by default (it needs to be searchable, so if someone has ideas on how to do that on encrypted data, I'm all ears). This means that Paperless should never be run on an untrusted host. Instead, I recommend that if you do want to use it, run it locally on a server in your own home.
Document scanners are typically used to scan sensitive documents. Things like your social insurance number, tax records, invoices, etc. Everything is stored in the clear without encryption. This means that Paperless should never be run on an untrusted host. Instead, I recommend that if you do want to use it, run it locally on a server in your own home.

View File

@@ -15,7 +15,6 @@
- imagemagick
- optipng
- gnupg
- libpoppler-cpp-dev
- libpq-dev
- libmagic-dev
- mime-support

View File

@@ -1,5 +1,5 @@
files:
- source: /src/locale/en-us/LC_MESSAGES/django.po
translation: /src/locale/%two_letters_code%/LC_MESSAGES/django.po
- source: /src/locale/en_US/LC_MESSAGES/django.po
translation: /src/locale/%locale_with_underscore%/LC_MESSAGES/django.po
- source: /src-ui/messages.xlf
translation: /src-ui/src/locale/messages.%two_letters_code%.xlf
translation: /src-ui/src/locale/messages.%locale_with_underscore%.xlf

View File

@@ -1,4 +1,4 @@
for command in document_archiver document_exporter document_importer mail_fetcher document_create_classifier document_index document_renamer document_retagger document_thumbnails;
for command in document_archiver document_exporter document_importer mail_fetcher document_create_classifier document_index document_renamer document_retagger document_thumbnails document_sanity_checker;
do
echo "installing $command..."
sed "s/management_command/$command/g" management_script.sh > /usr/local/bin/$command

View File

@@ -410,6 +410,34 @@ the naming scheme.
The command takes no arguments and processes all your documents at once.
.. _utilities-sanity-checker:
Sanity checker
==============
Paperless has a built-in sanity checker that inspects your document collection for issues.
The issues detected by the sanity checker are as follows:
* Missing original files.
* Missing archive files.
* Inaccessible original files due to improper permissions.
* Inaccessible archive files due to improper permissions.
* Corrupted original documents by comparing their checksum against what is stored in the database.
* Corrupted archive documents by comparing their checksum against what is stored in the database.
* Missing thumbnails.
* Inaccessible thumbnails due to improper permissions.
* Documents without any content (warning).
* Orphaned files in the media directory (warning). These are files that are not referenced by any document im paperless.
.. code::
document_sanity_checker
The command takes no arguments. Depending on the size of your document archive, this may take some time.
Fetching e-mail
===============

View File

@@ -5,6 +5,74 @@
Changelog
*********
paperless-ng 1.2.0
##################
* Changes to the OCRmyPDF integration
* Added support for deskewing and automatic rotation of incorrectly rotated pages. This is enabled by default, see :ref:`configuration-ocr`.
* Better support for encrypted files.
* Better support for various other PDF files: Paperless will now attempt to force OCR with safe options when OCR fails with the configured options.
* Added an explicit option to skip cleaning with ``unpaper``.
* Download multiple selected documents as a zip archive.
* The document list now remembers the current page.
* Improved responsiveness when switching between saved views and the document list.
* Increased the default wait time when observing files in the consumption folder
with polling from 1 to 5 seconds. This will decrease the likelihood of paperless
consuming partially written files.
* Fixed a crash of the document archiver management command when trying to process documents with unknown mime types.
* Paperless no longer depends on ``libpoppler-cpp-dev``.
.. note::
Some packages that paperless depends on are slowly dropping Python 3.6
support one after another, including the web server. Supporting Python
3.6 means that I cannot update these packages anymore.
At some point, paperless will drop Python 3.6 support. If using a bare
metal installation and you're still on Python 3.6, upgrade to 3.7 or newer.
If using docker, this does not affect you.
paperless-ng 1.1.4
##################
* Added English (GB) locale.
* Added ISO-8601 date display option.
paperless-ng 1.1.3
##################
* Added a docker-specific configuration option to adjust the number of
worker processes of the web server. See :ref:`configuration-docker`.
* Some more memory usage optimizations.
* Don't show inbox statistics if no inbox tag is defined.
paperless-ng 1.1.2
##################
* Always show top left corner of thumbnails, even for extra wide documents.
* Added a management command for executing the sanity checker directly.
See :ref:`utilities-sanity-checker`.
* The weekly sanity check now reports messages in the log files.
* Fixed an issue with the metadata tab not reporting anything in case of missing files.
* Reverted a change from 1.1.0 that caused huge memory usage due to redis caching.
* Some memory usage optimizations.
paperless-ng 1.1.1
##################

View File

@@ -202,7 +202,6 @@ Paperless uses `OCRmyPDF <https://ocrmypdf.readthedocs.io/en/latest/>`_ for
performing OCR on documents and images. Paperless uses sensible defaults for
most settings, but all of them can be configured to your needs.
PAPERLESS_OCR_LANGUAGE=<lang>
Customize the language that paperless will attempt to use when
parsing documents.
@@ -245,6 +244,54 @@ PAPERLESS_OCR_MODE=<mode>
The default is ``skip``, which only performs OCR when necessary and always
creates archived documents.
Read more about this in the `OCRmyPDF documentation <https://ocrmypdf.readthedocs.io/en/latest/advanced.html#when-ocr-is-skipped>`_.
PAPERLESS_OCR_CLEAN=<mode>
Tells paperless to use ``unpaper`` to clean any input document before
sending it to tesseract. This uses more resources, but generally results
in better OCR results. The following modes are available:
* ``clean``: Apply unpaper.
* ``clean-final``: Apply unpaper, and use the cleaned images to build the
output file instead of the original images.
* ``none``: Do not apply unpaper.
Defaults to ``clean``.
.. note::
``clean-final`` is incompatible with ocr mode ``redo``. When both
``clean-final`` and the ocr mode ``redo`` is configured, ``clean``
is used instead.
PAPERLESS_OCR_DESKEW=<bool>
Tells paperless to correct skewing (slight rotation of input images mainly
due to improper scanning)
Defaults to ``true``, which enables this feature.
.. note::
Deskewing is incompatible with ocr mode ``redo``. Deskewing will get
disabled automatically if ``redo`` is used as the ocr mode.
PAPERLESS_OCR_ROTATE_PAGES=<bool>
Tells paperless to correct page rotation (90°, 180° and 270° rotation).
If you notice that paperless is not rotating pages incorrectly rotated
pages (or vice versa), try adjusting the threshold up or down (see below).
Defaults to ``true``, which enables this feature.
PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD=<num>
Adjust the threshold for automatic page rotation by ``PAPERLESS_OCR_ROTATE_PAGES``.
This is an arbitrary value reported by tesseract. "15" is a very conservative value,
whereas "2" is a very aggressive option and will often result correctly rotated pages
being rotated as well.
Defaults to "12".
PAPERLESS_OCR_OUTPUT_TYPE=<type>
Specify the the type of PDF documents that paperless should produce.
@@ -271,7 +318,6 @@ PAPERLESS_OCR_PAGES=<num>
Defaults to 0, which disables this feature and always uses all pages.
PAPERLESS_OCR_IMAGE_DPI=<num>
Paperless will OCR any images you put into the system and convert them
into PDF documents. This is useful if your scanner produces images.
@@ -282,8 +328,8 @@ PAPERLESS_OCR_IMAGE_DPI=<num>
Set this to the DPI your scanner produces images at.
Default is none, which causes paperless to fail if no DPI information is
present in an image.
Default is none, which will automatically calculate image DPI so that
the produced PDF documents are A4 sized.
PAPERLESS_OCR_USER_ARGS=<json>
@@ -352,7 +398,7 @@ requires are as follows:
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
# ...
gotenberg:
@@ -555,3 +601,65 @@ PAPERLESS_GS_BINARY=<path>
PAPERLESS_OPTIPNG_BINARY=<path>
Defaults to "/usr/bin/optipng".
.. _configuration-docker:
Docker-specific options
#######################
These options don't have any effect in ``paperless.conf``. These options adjust
the behavior of the docker container. Configure these in `docker-compose.env`.
PAPERLESS_WEBSERVER_WORKERS=<num>
The number of worker processes the webserver should spawn. More worker processes
usually result in the front end to load data much quicker. However, each worker process
also loads the entire application into memory separately, so increasing this value
will increase RAM usage.
Consider configuring this to 1 on low power devices with limited amount of RAM.
Defaults to 2.
USERMAP_UID=<uid>
The ID of the paperless user in the container. Set this to your actual user ID on the
host system, which you can get by executing
.. code:: shell-session
$ id -u
Paperless will change ownership on its folders to this user, so you need to get this right
in order to be able to write to the consumption directory.
Defaults to 1000.
USERMAP_GID=<gid>
The ID of the paperless Group in the container. Set this to your actual group ID on the
host system, which you can get by executing
.. code:: shell-session
$ id -g
Paperless will change ownership on its folders to this group, so you need to get this right
in order to be able to write to the consumption directory.
Defaults to 1000.
PAPERLESS_OCR_LANGUAGES=<list>
Additional OCR languages to install. By default, paperless comes with
English, German, Italian, Spanish and French. If your language is not in this list, install
additional languages with this configuration option:
.. code:: bash
PAPERLESS_OCR_LANGUAGES=tur ces
To actually use these languages, also set the default OCR language of paperless:
.. code:: bash
PAPERLESS_OCR_LANGUAGE=tur
Defaults to none, which does not install any additional languages.

View File

@@ -109,6 +109,30 @@ This will build the front end and put it in a location from which the Django ser
it as static content. This way, you can verify that authentication is working.
Building the documentation
==========================
The documentation is built using sphinx. I've configured ReadTheDocs to automatically build
the documentation when changes are pushed. If you want to build the documentation locally,
this is how you do it:
1. Install python dependencies.
.. code:: shell-session
$ cd /path/to/paperless
$ pipenv install --dev
2. Build the documentation
.. code:: shell-session
$ cd /path/to/paperless/docs
$ pipenv run make clean html
This will build the HTML documentation, and put the resulting files in the ``_build/html``
directory.
Extending Paperless
===================

View File

@@ -280,7 +280,6 @@ writing. Windows is not and will never be supported.
* ``imagemagick`` >= 6 for PDF conversion
* ``optipng`` for optimizing thumbnails
* ``gnupg`` for handling encrypted documents
* ``libpoppler-cpp-dev`` for PDF to text conversion
* ``libpq-dev`` for PostgreSQL
* ``libmagic-dev`` for mime type detection
* ``mime-support`` for mime type detection
@@ -354,7 +353,7 @@ writing. Windows is not and will never be supported.
.. code:: shell-session
sudo -Hu paperless pip3 install -r requirements.txt
This will install all python dependencies in the home directory of
the new paperless user.
@@ -763,7 +762,8 @@ configuring some options in paperless can help improve performance immensely:
* Stick with SQLite to save some resources.
* Consider setting ``PAPERLESS_OCR_PAGES`` to 1, so that paperless will only OCR
the first page of your documents.
the first page of your documents. In most cases, this page contains enough
information to be able to find it.
* ``PAPERLESS_TASK_WORKERS`` and ``PAPERLESS_THREADS_PER_WORKER`` are configured
to use all cores. The Raspberry Pi models 3 and up have 4 cores, meaning that
paperless will use 2 workers and 2 threads per worker. This may result in
@@ -774,8 +774,13 @@ configuring some options in paperless can help improve performance immensely:
your documents before feeding them into paperless. Some scanners are able to
do this! You might want to even specify ``skip_noarchive`` to skip archive
file generation for already ocr'ed documents entirely.
* If you want to perform OCR on the the device, consider using ``PAPERLESS_OCR_CLEAN=none``.
This will speed up OCR times and use less memory at the expense of slightly worse
OCR results.
* Set ``PAPERLESS_OPTIMIZE_THUMBNAILS`` to 'false' if you want faster consumption
times. Thumbnails will be about 20% larger.
* If using docker, consider setting ``PAPERLESS_WEBSERVER_WORKERS`` to
1. This will save some memory.
For details, refer to :ref:`configuration`.
@@ -800,7 +805,7 @@ Using nginx as a reverse proxy
##############################
If you want to expose paperless to the internet, you should hide it behind a
reverse proxy with SSL enabled.
reverse proxy with SSL enabled.
In addition to the usual configuration for SSL,
the following configuration is required for paperless to operate:

View File

@@ -94,6 +94,30 @@ If you want to get rid of the warning or actually experience issues with automat
the file ``classification_model.pickle`` in the data directory and let paperless recreate it.
504 Server Error: Gateway Timeout when adding Office documents
##############################################################
You may experience these errors when using the optional TIKA integration:
.. code::
requests.exceptions.HTTPError: 504 Server Error: Gateway Timeout for url: http://gotenberg:3000/convert/office
Gotenberg is a server that converts Office documents into PDF documents and has a default timeout of 10 seconds.
When conversion takes longer, Gotenberg raises this error.
You can increase the timeout by configuring an environment variable for gotenberg (see also `here <https://thecodingmachine.github.io/gotenberg/#environment_variables.default_wait_timeout>`__).
If using docker-compose, this is achieved by the following configuration change in the ``docker-compose.yml`` file:
.. code:: yaml
gotenberg:
image: thecodingmachine/gotenberg
restart: unless-stopped
environment:
DISABLE_GOOGLE_CHROME: 1
DEFAULT_WAIT_TIMEOUT: 30
Permission denied errors in the consumption directory
#####################################################

View File

@@ -1,5 +1,7 @@
import os
bind = '0.0.0.0:8000'
workers = 2
workers = int(os.getenv("PAPERLESS_WEBSERVER_WORKERS", 2))
worker_class = 'uvicorn.workers.UvicornWorker'
timeout = 120

View File

@@ -41,6 +41,10 @@
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa
#PAPERLESS_OCR_PAGES=1
#PAPERLESS_OCR_IMAGE_DPI=300
#PAPERLESS_OCR_CLEAN=clean
#PAPERLESS_OCR_DESKEW=true
#PAPERLESS_OCR_ROTATE_PAGES=true
#PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD=12.0
#PAPERLESS_OCR_USER_ARGS={}
#PAPERLESS_CONVERT_MEMORY_LIMIT=0
#PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless

View File

@@ -12,11 +12,11 @@ arrow==0.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2,
asgiref==3.3.1; python_version >= '3.5'
async-timeout==3.0.1; python_full_version >= '3.5.3'
attrs==20.3.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
autobahn==21.1.1; python_version >= '3.6'
autobahn==21.2.1; python_version >= '3.6'
automat==20.2.0
blessed==1.17.12
certifi==2020.12.5
cffi==1.14.4
cffi==1.14.5
channels-redis==3.2.0
channels==3.0.3
chardet==4.0.0; python_version >= '3.1'
@@ -32,8 +32,7 @@ django-extensions==3.1.1
django-filter==2.4.0
django-picklefield==3.0.1; python_version >= '3'
django-q==1.3.4
django-redis==4.12.1
django==3.1.6
django==3.1.7
djangorestframework==3.12.2
filelock==3.0.12
fuzzywuzzy[speedup]==0.18.0
@@ -54,10 +53,9 @@ langdetect==1.0.8
lxml==4.6.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
msgpack==1.0.2
numpy==1.19.5
ocrmypdf==11.6.0
ocrmypdf==11.6.2
pathvalidate==2.3.2
pdfminer.six==20201018; python_version >= '3.4'
pdftotext==2.1.5
pdfminer.six==20201018
pikepdf==2.5.2
pillow==8.1.0
pluggy==0.13.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
@@ -72,7 +70,7 @@ python-dateutil==2.8.1
python-dotenv==0.15.0
python-gnupg==0.4.6
python-levenshtein==0.12.2
python-magic==0.4.18
python-magic==0.4.22
pytz==2021.1
pyyaml==5.4.1
redis==3.5.3
@@ -87,15 +85,15 @@ sortedcontainers==2.3.0
sqlparse==0.4.1; python_version >= '3.5'
threadpoolctl==2.1.0; python_version >= '3.5'
tika==1.24
tqdm==4.56.1
tqdm==4.57.0
twisted[tls]==20.3.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
txaio==20.12.1; python_version >= '3.6'
txaio==21.2.1; python_version >= '3.6'
tzlocal==2.1
urllib3==1.26.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
uvicorn[standard]==0.13.3
uvicorn[standard]==0.13.4
uvloop==0.14.0
watchdog==1.0.2
watchgod==0.6
watchgod==0.7
wcwidth==0.2.5
websockets==8.1
whitenoise==5.2.0

16
resources/logo.txt Normal file
View File

@@ -0,0 +1,16 @@
9w
{@@N
Q@@@@H
G@@@@@@@\
SilN@@@@@@@
*Q *@@@@@@@@S /= = = = = = = = = = = = = = = = = =\
*@ B@@@@@@@@N || ||
N R$ A@@@@@@@@@@ || PAPERLESS-NG ||
x@@ $U B@@@@@@@@@R || ||
N@@N^ @ N@@@@@@@@@* \= = = = = = = = = = = = = = = = = =/
|@@@u @ E@@@@@@@@l
Q@@@ \ Px@@@@@@P
1@@S` @@@o'
z$ ;
v
/

View File

@@ -18,7 +18,8 @@
"locales": {
"de": "src/locale/messages.de.xlf",
"nl-NL": "src/locale/messages.nl_NL.xlf",
"fr": "src/locale/messages.fr.xlf"
"fr": "src/locale/messages.fr.xlf",
"en-GB": "src/locale/messages.en_GB.xlf"
}
},
"architect": {

View File

@@ -52,17 +52,17 @@
</context-group>
</trans-unit>
<trans-unit id="2155249406916744630" datatype="html">
<source>View &quot;<x id="PH" equiv-text="this.list.savedView.name"/>&quot; saved successfully.</source>
<source>View &quot;<x id="PH" equiv-text="this.list.activeSavedViewTitle"/>&quot; saved successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">115</context>
</context-group>
</trans-unit>
<trans-unit id="6837554170707123455" datatype="html">
<source>View &quot;<x id="PH" equiv-text="savedView.name"/>&quot; created successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">130</context>
<context context-type="linenumber">136</context>
</context-group>
</trans-unit>
<trans-unit id="9ca82952a6bc860b5391d5975322d8af8ceddfa4" datatype="html">
@@ -114,8 +114,8 @@
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="72e7d343f9165602cce1ca7faffbc565fd31ef92" datatype="html">
<source>Save &quot;<x id="INTERPOLATION" equiv-text="{{list.savedViewTitle}}"/>&quot;</source>
<trans-unit id="5f5ce787c428d917c30c9bd70789a618e09743a7" datatype="html">
<source>Save &quot;<x id="INTERPOLATION" equiv-text="{{list.activeSavedViewTitle}}"/>&quot;</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">71</context>
@@ -513,13 +513,6 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="5ca707824ab93066c7d9b44e1b8bf216725c2c22" datatype="html">
<source>Filter</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/logs/logs.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit id="5610279464668232148" datatype="html">
<source>Saved view &quot;<x id="PH" equiv-text="savedView.name"/>&quot; deleted.</source>
<context-group purpose="location">
@@ -545,14 +538,21 @@
<source>Use date format of display language</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">95</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="4912706592792948707" datatype="html">
<source>ISO 8601</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">97</context>
</context-group>
</trans-unit>
<trans-unit id="8488620293789898901" datatype="html">
<source>Error while storing settings on server: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">111</context>
<context context-type="linenumber">114</context>
</context-group>
</trans-unit>
<trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
@@ -1081,6 +1081,13 @@
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit id="6523384805359286307" datatype="html">
<source>Title: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit id="02d184c288f567825a1fcbf83bcd3099a10853d5" datatype="html">
<source>Filter tags</source>
<context-group purpose="location">
@@ -1219,21 +1226,21 @@
<source>Error executing bulk operation: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">73</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="7894972847287473517" datatype="html">
<source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot;</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">112</context>
<context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit id="8639884465898458690" datatype="html">
<source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">114</context>
<context context-type="linenumber">115</context>
</context-group>
<note priority="1" from="description">This is for messages like &apos;modify &quot;tag1&quot; and &quot;tag2&quot;&apos;</note>
</trans-unit>
@@ -1241,7 +1248,7 @@
<source>, </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">117</context>
</context-group>
<note priority="1" from="description">this is used to separate enumerations and should probably be a comma and a whitespace in most languages</note>
</trans-unit>
@@ -1249,7 +1256,7 @@
<source><x id="PH" equiv-text="list"/> and &quot;<x id="PH_1" equiv-text="items[items.length - 1].name"/>&quot;</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">117</context>
<context context-type="linenumber">118</context>
</context-group>
<note priority="1" from="description">this is for messages like &apos;modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;&apos;</note>
</trans-unit>
@@ -1257,112 +1264,112 @@
<source>Confirm tags assignment</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">127</context>
</context-group>
</trans-unit>
<trans-unit id="6619516195038467207" datatype="html">
<source>This operation will add the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">129</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit id="1894412783609570695" datatype="html">
<source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit id="7181166515756808573" datatype="html">
<source>This operation will remove the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit id="3819792277998068944" datatype="html">
<source>This operation will remove the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">136</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit id="2739066218579571288" datatype="html">
<source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> and remove the tags <x id="PH_1" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">138</context>
<context context-type="linenumber">139</context>
</context-group>
</trans-unit>
<trans-unit id="2996713129519325161" datatype="html">
<source>Confirm correspondent assignment</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">158</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="6900893559485781849" datatype="html">
<source>This operation will assign the correspondent &quot;<x id="PH" equiv-text="correspondent.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">160</context>
<context context-type="linenumber">161</context>
</context-group>
</trans-unit>
<trans-unit id="1257522660364398440" datatype="html">
<source>This operation will remove the correspondent from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">162</context>
<context context-type="linenumber">163</context>
</context-group>
</trans-unit>
<trans-unit id="5393409374423140648" datatype="html">
<source>Confirm document type assignment</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">181</context>
<context context-type="linenumber">182</context>
</context-group>
</trans-unit>
<trans-unit id="332180123895325027" datatype="html">
<source>This operation will assign the document type &quot;<x id="PH" equiv-text="documentType.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit id="2236642492594872779" datatype="html">
<source>This operation will remove the document type from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">185</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit id="749430623564850405" datatype="html">
<source>Delete confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit id="4303174930844518780" datatype="html">
<source>This operation will permanently delete <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">201</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="5641451190833696892" datatype="html">
<source>This operation cannot be undone.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">202</context>
<context context-type="linenumber">203</context>
</context-group>
</trans-unit>
<trans-unit id="6734339521247847366" datatype="html">
<source>Delete document(s)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">204</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit id="8b0609df23817024b3bed12beb9b64fc1009f588" datatype="html">
@@ -1386,6 +1393,13 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit id="fc2de37422d7c4af6686842283cc2afd781b6848" datatype="html">
<source>Download originals</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="a1e6c11f20d4bf6e8e6b43e3c6d2561b2080645e" datatype="html">
<source>Suggestions:</source>
<context-group purpose="location">
@@ -1414,20 +1428,20 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="46c8fe557cf52c9389783627d4f85453f4ddb459" datatype="html">
<source>Documents in inbox: <x id="INTERPOLATION" equiv-text="{{statistics.documents_inbox}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="c327c0e67bcac7494dcbaa9afb3b42d5008c6438" datatype="html">
<source>Total documents: <x id="INTERPOLATION" equiv-text="{{statistics.documents_total}}"/></source>
<trans-unit id="c0d907c2687c09612395aee6ef7c04ca8e5e5e0a" datatype="html">
<source>Total documents: <x id="INTERPOLATION" equiv-text="{{statistics?.documents_total}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
</trans-unit>
<trans-unit id="13e8d49dbcad9f9d71e66a9a56d6f328cff430c9" datatype="html">
<source>Documents in inbox: <x id="INTERPOLATION" equiv-text="{{statistics?.documents_inbox}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit id="6443586946875325554" datatype="html">
<source>Processing: <x id="PH" equiv-text="countUploadingAndProcessing"/></source>
<context-group purpose="location">
@@ -1619,25 +1633,32 @@
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="6987083569809053351" datatype="html">
<source>English (GB)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit id="1858110241312746425" datatype="html">
<source>German</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
<context context-type="linenumber">84</context>
</context-group>
</trans-unit>
<trans-unit id="3071065188816255493" datatype="html">
<source>Dutch</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">84</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit id="7633754075223722162" datatype="html">
<source>French</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">85</context>
<context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit id="2119857572761283468" datatype="html">

View File

@@ -5545,6 +5545,11 @@
"schema-utils": "^2.6.5"
}
},
"file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",

View File

@@ -23,6 +23,7 @@
"@ng-bootstrap/ng-bootstrap": "^8.0.0",
"@ng-select/ng-select": "^5.0.9",
"bootstrap": "^4.5.0",
"file-saver": "^2.0.5",
"ng-bootstrap": "^1.6.3",
"ng2-pdf-viewer": "^6.3.2",
"ngx-cookie-service": "^10.1.1",

View File

@@ -64,10 +64,12 @@ import { CustomDatePipe } from './pipes/custom-date.pipe';
import localeFr from '@angular/common/locales/fr';
import localeNl from '@angular/common/locales/nl';
import localeDe from '@angular/common/locales/de';
import localeEnGb from '@angular/common/locales/en-GB';
registerLocaleData(localeFr)
registerLocaleData(localeNl)
registerLocaleData(localeDe)
registerLocaleData(localeEnGb)
@NgModule({
declarations: [

View File

@@ -48,7 +48,7 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
if (this.savedView.show_in_sidebar) {
this.router.navigate(['view', this.savedView.id])
} else {
this.list.load(this.savedView)
this.list.loadSavedView(this.savedView, true)
this.router.navigate(["documents"])
}
}

View File

@@ -1,6 +1,6 @@
<app-widget-frame title="Statistics" i18n-title>
<ng-container content>
<p class="card-text" i18n>Documents in inbox: {{statistics.documents_inbox}}</p>
<p class="card-text" i18n>Total documents: {{statistics.documents_total}}</p>
<p class="card-text" i18n *ngIf="statistics?.documents_inbox != null">Documents in inbox: {{statistics?.documents_inbox}}</p>
<p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p>
</ng-container>
</app-widget-frame>

View File

@@ -191,8 +191,8 @@ export class DocumentDetailComponent implements OnInit {
close() {
this.openDocumentService.closeDocument(this.document)
if (this.documentListViewService.savedViewId) {
this.router.navigate(['view', this.documentListViewService.savedViewId])
if (this.documentListViewService.activeSavedViewId) {
this.router.navigate(['view', this.documentListViewService.activeSavedViewId])
} else {
this.router.navigate(['documents'])
}

View File

@@ -56,6 +56,20 @@
</div>
</div>
<div class="col-auto ml-auto mb-2 mb-xl-0 d-flex">
<div class="btn-group btn-group-sm mr-2">
<button type="button" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download" />
</svg>&nbsp;<ng-container i18n>Download</ng-container>
</button>
<div class="btn-group" ngbDropdown role="group" aria-label="Button group with nested dropdown">
<button class="btn btn-outline-primary btn-sm dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<button ngbDropdownItem i18n (click)="downloadSelected('originals')">Download originals</button>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />

View File

@@ -15,6 +15,7 @@ import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable
import { MatchingModel } from 'src/app/data/matching-model';
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
import { ToastService } from 'src/app/services/toast.service';
import { saveAs } from 'file-saver';
@Component({
selector: 'app-bulk-editor',
@@ -137,7 +138,7 @@ export class BulkEditorComponent {
} else {
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on ${this.list.selected.size} selected document(s).`
}
modal.componentInstance.btnClass = "btn-warning"
modal.componentInstance.btnCaption = $localize`Confirm`
modal.componentInstance.confirmClicked.subscribe(() => {
@@ -207,4 +208,10 @@ export class BulkEditorComponent {
this.executeBulkOperation(modal, "delete", {})
})
}
downloadSelected(content = "archive") {
this.documentService.bulkDownload(Array.from(this.list.selected), content).subscribe((result: any) => {
saveAs(result, 'documents.zip');
})
}
}

View File

@@ -6,7 +6,7 @@
.doc-img {
object-fit: cover;
object-position: top;
object-position: top left;
height: 100%;
position: absolute;
mix-blend-mode: multiply;

View File

@@ -2,7 +2,7 @@
.doc-img {
object-fit: cover;
object-position: top;
object-position: top left;
height: 200px;
mix-blend-mode: multiply;
}

View File

@@ -63,12 +63,12 @@
<div class="btn-group ml-2 flex-fill" ngbDropdown role="group">
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" ngbDropdownToggle i18n>Views</button>
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
<ng-container *ngIf="!list.savedViewId">
<ng-container *ngIf="!list.activeSavedViewId">
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
</ng-container>
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId" i18n>Save "{{list.savedViewTitle}}"</button>
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" i18n>Save "{{list.activeSavedViewTitle}}"</button>
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
</div>
</div>
@@ -86,7 +86,7 @@
<span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>&nbsp;<span i18n *ngIf="isFiltered">(filtered)</span>
</p>
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
[rotate]="true" aria-label="Default pagination"></ngb-pagination>
</div>
<div *ngIf="displayMode == 'largeCards'">

View File

@@ -1,4 +1,4 @@
import { AfterViewInit, Component, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { Component, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Subscription } from 'rxjs';
@@ -9,7 +9,7 @@ import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { ToastService } from 'src/app/services/toast.service';
import { FilterEditorComponent } from './filter-editor/filter-editor.component';
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
@@ -46,7 +46,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
}
getTitle() {
return this.list.savedViewTitle || $localize`Documents`
return this.list.activeSavedViewTitle || $localize`Documents`
}
getSortFields() {
@@ -73,19 +73,18 @@ export class DocumentListComponent implements OnInit, OnDestroy {
this.list.reload()
})
this.route.paramMap.subscribe(params => {
this.list.clear()
if (params.has('id')) {
this.savedViewService.getCached(+params.get('id')).subscribe(view => {
if (!view) {
this.router.navigate(["404"])
return
}
this.list.savedView = view
this.list.activateSavedView(view)
this.list.reload()
this.rulesChanged()
})
} else {
this.list.savedView = null
this.list.activateSavedView(null)
this.list.reload()
this.rulesChanged()
}
@@ -99,16 +98,23 @@ export class DocumentListComponent implements OnInit, OnDestroy {
}
loadViewConfig(view: PaperlessSavedView) {
this.list.load(view)
this.list.loadSavedView(view)
this.list.reload()
this.rulesChanged()
}
saveViewConfig() {
this.savedViewService.update(this.list.savedView).subscribe(result => {
this.toastService.showInfo($localize`View "${this.list.savedView.name}" saved successfully.`)
})
if (this.list.activeSavedViewId != null) {
let savedView: PaperlessSavedView = {
id: this.list.activeSavedViewId,
filter_rules: this.list.filterRules,
sort_field: this.list.sortField,
sort_reverse: this.list.sortReverse
}
this.savedViewService.patch(savedView).subscribe(result => {
this.toastService.showInfo($localize`View "${this.list.activeSavedViewTitle}" saved successfully.`)
})
}
}
saveViewConfigAs() {
@@ -116,7 +122,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
modal.componentInstance.defaultName = this.filterEditor.generateFilterName()
modal.componentInstance.saveClicked.subscribe(formValue => {
modal.componentInstance.buttonsEnabled = false
let savedView = {
let savedView: PaperlessSavedView = {
name: formValue.name,
show_on_dashboard: formValue.showOnDashboard,
show_in_sidebar: formValue.showInSideBar,
@@ -137,8 +143,8 @@ export class DocumentListComponent implements OnInit, OnDestroy {
resetFilters(): void {
this.filterRulesModified = false
if (this.list.savedViewId) {
this.savedViewService.getCached(this.list.savedViewId).subscribe(viewUntouched => {
if (this.list.activeSavedViewId) {
this.savedViewService.getCached(this.list.activeSavedViewId).subscribe(viewUntouched => {
this.list.filterRules = viewUntouched.filter_rules
this.list.reload()
})
@@ -150,11 +156,11 @@ export class DocumentListComponent implements OnInit, OnDestroy {
rulesChanged() {
let modified = false
if (this.list.savedView == null) {
if (this.list.activeSavedViewId == null) {
modified = this.list.filterRules.length > 0 // documents list is modified if it has any filters
} else {
// compare savedView current filters vs original
this.savedViewService.getCached(this.list.savedViewId).subscribe(view => {
this.savedViewService.getCached(this.list.activeSavedViewId).subscribe(view => {
let filterRulesInitial = view.filter_rules
if (this.list.filterRules.length !== filterRulesInitial.length) modified = true

View File

@@ -46,6 +46,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
return $localize`Without any tag`
}
case FILTER_TITLE:
return $localize`Title: ${rule.value}`
}
}
@@ -117,7 +119,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
})
}
get filterRules() {
get filterRules(): FilterRule[] {
let filterRules: FilterRule[] = []
if (this._titleFilter) {
filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter})

View File

@@ -34,7 +34,7 @@
<div class="col">
<select class="form-control" formControlName="dateLocale">
<option *ngFor="let lang of dateLocaleOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code"> - {{today | date:'shortDate':null:lang.code}}</span></option>
<option *ngFor="let lang of dateLocaleOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code"> - {{today | customDate:'shortDate':null:lang.code}}</span></option>
</select>
</div>
@@ -167,7 +167,7 @@
</li>
</ul>
<div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3 shadow"></div>
<div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3 shadow-sm"></div>
<button type="submit" class="btn btn-primary" i18n>Save</button>
</form>

View File

@@ -35,7 +35,7 @@ export class SettingsComponent implements OnInit {
savedViews: PaperlessSavedView[]
get computedDateLocale(): string {
return this.settingsForm.value.dateLocale || this.settingsForm.value.displayLanguage
return this.settingsForm.value.dateLocale || this.settingsForm.value.displayLanguage || this.currentLocale
}
constructor(
@@ -92,7 +92,10 @@ export class SettingsComponent implements OnInit {
}
get dateLocaleOptions(): LanguageOption[] {
return [{code: "", name: $localize`Use date format of display language`}].concat(this.settings.getLanguageOptions())
return [
{code: "", name: $localize`Use date format of display language`},
{code: "iso-8601", name: $localize`ISO 8601`}
].concat(this.settings.getLanguageOptions())
}
get today() {

View File

@@ -2,18 +2,29 @@ import { DatePipe } from '@angular/common';
import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core';
import { SettingsService, SETTINGS_KEYS } from '../services/settings.service';
const FORMAT_TO_ISO_FORMAT = {
"longDate": "y-MM-dd",
"mediumDate": "yy-MM-dd",
"shortDate": "yy-MM-dd"
}
@Pipe({
name: 'customDate'
})
export class CustomDatePipe extends DatePipe implements PipeTransform {
constructor(@Inject(LOCALE_ID) locale: string, private settings: SettingsService) {
super(settings.get(SETTINGS_KEYS.DATE_LOCALE) || locale)
super(locale)
}
transform(value: any, format?: string, timezone?: string, locale?: string): string | null {
return super.transform(value, format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT), timezone, locale)
let l = locale || this.settings.get(SETTINGS_KEYS.DATE_LOCALE)
let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT)
if (l == "iso-8601") {
return super.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone)
} else {
return super.transform(value, format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT), timezone, locale)
}
}
}

View File

@@ -169,7 +169,12 @@ export class ConsumerStatusService {
}
dismiss(status: FileStatus) {
let index = this.consumerStatus.findIndex(s => s.filename == status.filename)
let index
if (status.taskId != null) {
index = this.consumerStatus.findIndex(s => s.taskId == status.taskId)
} else {
index = this.consumerStatus.findIndex(s => s.filename == status.filename)
}
if (index > -1) {
this.consumerStatus.splice(index, 1)

View File

@@ -8,6 +8,23 @@ import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys';
import { DocumentService } from './rest/document.service';
import { SettingsService, SETTINGS_KEYS } from './settings.service';
interface ListViewState {
title?: string
documents?: PaperlessDocument[]
currentPage: number
collectionSize: number
sortField: string
sortReverse: boolean
filterRules: FilterRule[]
selected?: Set<number>
}
/**
* This service manages the document list which is displayed using the document list view.
@@ -20,156 +37,174 @@ import { SettingsService, SETTINGS_KEYS } from './settings.service';
})
export class DocumentListViewService {
static DEFAULT_SORT_FIELD = 'created'
isReloading: boolean = false
documents: PaperlessDocument[] = []
currentPage = 1
currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
collectionSize: number
rangeSelectionAnchorIndex: number
lastRangeSelectionToIndex: number
/**
* This is the current config for the document list. The service will always remember the last settings used for the document list.
*/
private _documentListViewConfig: PaperlessSavedView
/**
* Optionally, this is the currently selected saved view, which might be null.
*/
private _savedViewConfig: PaperlessSavedView
currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
get savedView(): PaperlessSavedView {
return this._savedViewConfig
private listViewStates: Map<number, ListViewState> = new Map()
private _activeSavedViewId: number = null
get activeSavedViewId() {
return this._activeSavedViewId
}
set savedView(value: PaperlessSavedView) {
if (value && !this._savedViewConfig || value && value.id != this._savedViewConfig.id) {
//saved view inactive and should be active now, or saved view active, but a different view is requested
//this is here so that we don't modify value, which might be the actual instance of the saved view.
this.selectNone()
this._savedViewConfig = Object.assign({}, value)
} else if (this._savedViewConfig && !value) {
//saved view active, but document list requested
this.selectNone()
this._savedViewConfig = null
get activeSavedViewTitle() {
return this.activeListViewState.title
}
private defaultListViewState(): ListViewState {
return {
title: null,
documents: [],
currentPage: 1,
collectionSize: null,
sortField: "created",
sortReverse: true,
filterRules: [],
selected: new Set<number>()
}
}
get savedViewId() {
return this.savedView?.id
private get activeListViewState() {
if (!this.listViewStates.has(this._activeSavedViewId)) {
this.listViewStates.set(this._activeSavedViewId, this.defaultListViewState())
}
return this.listViewStates.get(this._activeSavedViewId)
}
get savedViewTitle() {
return this.savedView?.name
}
get documentListView() {
return this._documentListViewConfig
}
set documentListView(value) {
if (value) {
this._documentListViewConfig = Object.assign({}, value)
this.saveDocumentListView()
activateSavedView(view: PaperlessSavedView) {
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
if (view) {
this._activeSavedViewId = view.id
this.loadSavedView(view)
} else {
this._activeSavedViewId = null
}
}
/**
* This is what switches between the saved views and the document list view. Everything on the document list uses
* this property to determine the settings for the currently displayed document list.
*/
get view() {
return this.savedView || this.documentListView
}
load(view: PaperlessSavedView) {
this.documentListView.filter_rules = cloneFilterRules(view.filter_rules)
this.documentListView.sort_reverse = view.sort_reverse
this.documentListView.sort_field = view.sort_field
this.saveDocumentListView()
}
clear() {
this.collectionSize = null
this.documents = []
this.currentPage = 1
loadSavedView(view: PaperlessSavedView, closeCurrentView: boolean = false) {
if (closeCurrentView) {
this._activeSavedViewId = null
}
this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules)
this.activeListViewState.sortField = view.sort_field
this.activeListViewState.sortReverse = view.sort_reverse
if (this._activeSavedViewId) {
this.activeListViewState.title = view.name
}
this.reduceSelectionToFilter()
}
reload(onFinish?) {
this.isReloading = true
let activeListViewState = this.activeListViewState
this.documentService.listFiltered(
this.currentPage,
activeListViewState.currentPage,
this.currentPageSize,
this.view.sort_field,
this.view.sort_reverse,
this.view.filter_rules).subscribe(
activeListViewState.sortField,
activeListViewState.sortReverse,
activeListViewState.filterRules).subscribe(
result => {
this.collectionSize = result.count
this.documents = result.results
this.isReloading = false
activeListViewState.collectionSize = result.count
activeListViewState.documents = result.results
if (onFinish) {
onFinish()
}
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
this.isReloading = false
},
error => {
if (this.currentPage != 1 && error.status == 404) {
this.isReloading = false
if (activeListViewState.currentPage != 1 && error.status == 404) {
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
this.currentPage = 1
activeListViewState.currentPage = 1
this.reload()
}
this.isReloading = false
})
}
set filterRules(filterRules: FilterRule[]) {
//we're going to clone the filterRules object, since we don't
//want changes in the filter editor to propagate into here right away.
this.view.filter_rules = filterRules
this.activeListViewState.filterRules = filterRules
this.reload()
this.reduceSelectionToFilter()
this.saveDocumentListView()
}
get filterRules(): FilterRule[] {
return this.view.filter_rules
return this.activeListViewState.filterRules
}
set sortField(field: string) {
this.view.sort_field = field
this.saveDocumentListView()
this.activeListViewState.sortField = field
this.reload()
this.saveDocumentListView()
}
get sortField(): string {
return this.view.sort_field
return this.activeListViewState.sortField
}
set sortReverse(reverse: boolean) {
this.view.sort_reverse = reverse
this.saveDocumentListView()
this.activeListViewState.sortReverse = reverse
this.reload()
this.saveDocumentListView()
}
get sortReverse(): boolean {
return this.view.sort_reverse
return this.activeListViewState.sortReverse
}
get collectionSize(): number {
return this.activeListViewState.collectionSize
}
get currentPage(): number {
return this.activeListViewState.currentPage
}
set currentPage(page: number) {
this.activeListViewState.currentPage = page
this.reload()
this.saveDocumentListView()
}
get documents(): PaperlessDocument[] {
return this.activeListViewState.documents
}
get selected(): Set<number> {
return this.activeListViewState.selected
}
setSort(field: string, reverse: boolean) {
this.view.sort_field = field
this.view.sort_reverse = reverse
this.saveDocumentListView()
this.activeListViewState.sortField = field
this.activeListViewState.sortReverse = reverse
this.reload()
this.saveDocumentListView()
}
private saveDocumentListView() {
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
if (this._activeSavedViewId == null) {
let savedState: ListViewState = {
collectionSize: this.activeListViewState.collectionSize,
currentPage: this.activeListViewState.currentPage,
filterRules: this.activeListViewState.filterRules,
sortField: this.activeListViewState.sortField,
sortReverse: this.activeListViewState.sortReverse
}
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(savedState))
}
}
quickFilter(filterRules: FilterRule[]) {
this.savedView = null
this.view.filter_rules = filterRules
this._activeSavedViewId = null
this.activeListViewState.filterRules = filterRules
this.activeListViewState.currentPage = 1
this.reduceSelectionToFilter()
this.saveDocumentListView()
this.router.navigate(["documents"])
@@ -217,8 +252,6 @@ export class DocumentListViewService {
}
}
selected = new Set<number>()
selectNone() {
this.selected.clear()
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
@@ -227,13 +260,11 @@ export class DocumentListViewService {
reduceSelectionToFilter() {
if (this.selected.size > 0) {
this.documentService.listAllFilteredIds(this.filterRules).subscribe(ids => {
let subset = new Set<number>()
for (let id of ids) {
if (this.selected.has(id)) {
subset.add(id)
for (let id of this.selected) {
if (!ids.includes(id)) {
this.selected.delete(id)
}
}
this.selected = subset
})
}
}
@@ -287,20 +318,21 @@ export class DocumentListViewService {
}
constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router) {
let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
if (documentListViewConfigJson) {
try {
this.documentListView = JSON.parse(documentListViewConfigJson)
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
// Remove null elements from the restored state
Object.keys(savedState).forEach(k => {
if (savedState[k] == null) {
delete savedState[k]
}
})
//only use restored state attributes instead of defaults if they are not null
let newState = Object.assign(this.defaultListViewState(), savedState)
this.listViewStates.set(null, newState)
} catch (e) {
sessionStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
this.documentListView = null
}
}
if (!this.documentListView || this.documentListView.filter_rules == null || this.documentListView.sort_reverse == null || this.documentListView.sort_field == null) {
this.documentListView = {
filter_rules: [],
sort_reverse: true,
sort_field: 'created'
}
}
}

View File

@@ -134,4 +134,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
return this.http.get<PaperlessDocumentSuggestions>(this.getResourceUrl(id, 'suggestions'))
}
bulkDownload(ids: number[], content="both") {
return this.http.post(this.getResourceUrl(null, 'bulk_download'), {"documents": ids, "content": content}, { responseType: 'blob' })
}
}

View File

@@ -79,7 +79,8 @@ export class SettingsService {
getLanguageOptions(): LanguageOption[] {
return [
{code: "en-US", name: $localize`English (US)`, englishName: "English (US)"},
{code: "en-us", name: $localize`English (US)`, englishName: "English (US)"},
{code: "en-gb", name: $localize`English (GB)`, englishName: "English (GB)"},
{code: "de", name: $localize`German`, englishName: "German"},
{code: "nl", name: $localize`Dutch`, englishName: "Dutch"},
{code: "fr", name: $localize`French`, englishName: "French"}

View File

@@ -2,7 +2,7 @@ export const environment = {
production: true,
apiBaseUrl: "/api/",
appTitle: "Paperless-ng",
version: "1.1.1",
version: "1.2.0",
webSocketHost: window.location.host,
webSocketProtocol: (window.location.protocol == "https:" ? "wss:" : "ws:")
};

View File

@@ -58,11 +58,11 @@
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2155249406916744630">
<source>View &quot;<x equiv-text="this.list.savedView.name" id="PH"/>&quot; saved successfully.</source>
<target>Ansicht &quot;<x equiv-text="this.list.savedView.name" id="PH"/>&quot; erfolgreich gespeichert.</target>
<source>View &quot;<x equiv-text="this.list.activeSavedViewTitle" id="PH"/>&quot; saved successfully.</source>
<target>Ansicht &quot;<x equiv-text="this.list.activeSavedViewTitle" id="PH"/>&quot; erfolgreich gespeichert.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">115</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6837554170707123455">
@@ -70,7 +70,7 @@
<target>Ansicht &quot;<x equiv-text="savedView.name" id="PH"/>&quot; erfolgreich erstellt.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">130</context>
<context context-type="linenumber">136</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="9ca82952a6bc860b5391d5975322d8af8ceddfa4">
@@ -129,9 +129,9 @@
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="72e7d343f9165602cce1ca7faffbc565fd31ef92">
<source>Save &quot;<x equiv-text="{{list.savedViewTitle}}" id="INTERPOLATION"/>&quot;</source>
<target>&quot;<x equiv-text="{{list.savedViewTitle}}" id="INTERPOLATION"/>&quot; speichern</target>
<trans-unit datatype="html" id="5f5ce787c428d917c30c9bd70789a618e09743a7">
<source>Save &quot;<x equiv-text="{{list.activeSavedViewTitle}}" id="INTERPOLATION"/>&quot;</source>
<target>&quot;<x equiv-text="{{list.activeSavedViewTitle}}" id="INTERPOLATION"/>&quot; speichern</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">71</context>
@@ -585,14 +585,6 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5ca707824ab93066c7d9b44e1b8bf216725c2c22">
<source>Filter</source>
<target>Filtern</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/logs/logs.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5610279464668232148">
<source>Saved view &quot;<x equiv-text="savedView.name" id="PH"/>&quot; deleted.</source>
<target>Gespeicherte Ansicht &quot;<x equiv-text="savedView.name" id="PH"/>&quot; gelöscht.</target>
@@ -622,7 +614,15 @@
<target>Benutze Datumsformat der Anzeigesprache</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">95</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="4912706592792948707">
<source>ISO 8601</source>
<target>ISO 8601</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">97</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8488620293789898901">
@@ -630,7 +630,7 @@
<target>Fehler beim Speichern der Einstellungen auf dem Server: <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">111</context>
<context context-type="linenumber">114</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="121cc5391cd2a5115bc2b3160379ee5b36cd7716">
@@ -1234,6 +1234,14 @@
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6523384805359286307">
<source>Title: <x equiv-text="rule.value" id="PH"/></source>
<target>Titel: <x equiv-text="rule.value" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="02d184c288f567825a1fcbf83bcd3099a10853d5">
<source>Filter tags</source>
<target>Tags filtern</target>
@@ -1392,7 +1400,7 @@
<target>Fehler beim Ausführung der Massenverarbeitung: <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">73</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7894972847287473517">
@@ -1400,7 +1408,7 @@
<target>&quot;<x equiv-text="items[0].name" id="PH"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">112</context>
<context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8639884465898458690">
@@ -1408,7 +1416,7 @@
<target>&quot;<x equiv-text="items[0].name" id="PH"/>&quot; und &quot;<x equiv-text="items[1].name" id="PH_1"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">114</context>
<context context-type="linenumber">115</context>
</context-group>
<note from="description" priority="1">This is for messages like 'modify &quot;tag1&quot; and &quot;tag2&quot;'</note>
</trans-unit>
@@ -1417,7 +1425,7 @@
<target>, </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">117</context>
</context-group>
<note from="description" priority="1">this is used to separate enumerations and should probably be a comma and a whitespace in most languages</note>
</trans-unit>
@@ -1426,7 +1434,7 @@
<target><x equiv-text="list" id="PH"/> und &quot;<x equiv-text="items[items.length - 1].name" id="PH_1"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">117</context>
<context context-type="linenumber">118</context>
</context-group>
<note from="description" priority="1">this is for messages like 'modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;'</note>
</trans-unit>
@@ -1435,7 +1443,7 @@
<target>Tag-Zuweisung bestätigen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">127</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6619516195038467207">
@@ -1443,7 +1451,7 @@
<target>Diese Aktion wird <x equiv-text="this.list.selected.size" id="PH_1"/> ausgewählten Dokumenten das Tag &quot;<x equiv-text="tag.name" id="PH"/>&quot; hinzufügen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">129</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1894412783609570695">
@@ -1451,7 +1459,7 @@
<target>Diese Aktion wird <x equiv-text="this.list.selected.size" id="PH_1"/> ausgewählten Dokumenten die Tags <x equiv-text="this._localizeList(changedTags.itemsToAdd)" id="PH"/> hinzufügen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7181166515756808573">
@@ -1459,7 +1467,7 @@
<target>Diese Aktion wird das Tag &quot;<x equiv-text="tag.name" id="PH"/>&quot; von <x equiv-text="this.list.selected.size" id="PH_1"/> ausgewählten Dokumenten entfernen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="3819792277998068944">
@@ -1467,7 +1475,7 @@
<target>Diese Aktion wird die Tags <x equiv-text="this._localizeList(changedTags.itemsToRemove)" id="PH"/> von <x equiv-text="this.list.selected.size" id="PH_1"/> ausgewählten Dokumenten entfernen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">136</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2739066218579571288">
@@ -1475,7 +1483,7 @@
<target>Diese Aktion wird die Tags <x equiv-text="this._localizeList(changedTags.itemsToAdd)" id="PH"/> den <x equiv-text="this.list.selected.size" id="PH_2"/> ausgewählten Dokumenten hinzufügen und die Tags <x equiv-text="this._localizeList(changedTags.itemsToRemove)" id="PH_1"/> entfernen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">138</context>
<context context-type="linenumber">139</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2996713129519325161">
@@ -1483,7 +1491,7 @@
<target>Korrespondent-Zuweisung bestätigen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">158</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6900893559485781849">
@@ -1491,7 +1499,7 @@
<target>Diese Aktion wird <x equiv-text="this.list.selected.size" id="PH_1"/> ausgewählten Dokumenten den Korrespondent &quot;<x equiv-text="correspondent.name" id="PH"/>&quot; zuweisen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">160</context>
<context context-type="linenumber">161</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1257522660364398440">
@@ -1499,7 +1507,7 @@
<target>Diese Aktion wird bei <x equiv-text="this.list.selected.size" id="PH"/> ausgewählten Dokumenten den Korrespondent entfernen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">162</context>
<context context-type="linenumber">163</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5393409374423140648">
@@ -1507,7 +1515,7 @@
<target>Dokumenttyp-Zuweisung bestätigen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">181</context>
<context context-type="linenumber">182</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="332180123895325027">
@@ -1515,7 +1523,7 @@
<target>Diese Aktion wird <x equiv-text="this.list.selected.size" id="PH_1"/> ausgewählten Dokumenten den Dokumenttyp &quot;<x equiv-text="correspondent.name" id="PH"/>&quot; zuweisen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2236642492594872779">
@@ -1523,7 +1531,7 @@
<target>Diese Aktion wird bei <x equiv-text="this.list.selected.size" id="PH"/> ausgewählten Dokumenten den Dokumenttyp entfernen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">185</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="749430623564850405">
@@ -1531,7 +1539,7 @@
<target>Löschen bestätigen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="4303174930844518780">
@@ -1539,7 +1547,7 @@
<target>Diese Aktion wird <x equiv-text="this.list.selected.size" id="PH"/> ausgewählte Dokumente unwiderruflich löschen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">201</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5641451190833696892">
@@ -1547,7 +1555,7 @@
<target>Diese Aktion kann nicht rückgängig gemacht werden.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">202</context>
<context context-type="linenumber">203</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6734339521247847366">
@@ -1555,7 +1563,7 @@
<target>Dokument(e) löschen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">204</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8b0609df23817024b3bed12beb9b64fc1009f588">
@@ -1582,6 +1590,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="fc2de37422d7c4af6686842283cc2afd781b6848">
<source>Download originals</source>
<target>Originale herunterladen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="a1e6c11f20d4bf6e8e6b43e3c6d2561b2080645e">
<source>Suggestions:</source>
<target>Vorschläge:</target>
@@ -1614,22 +1630,22 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="46c8fe557cf52c9389783627d4f85453f4ddb459">
<source>Documents in inbox: <x equiv-text="{{statistics.documents_inbox}}" id="INTERPOLATION"/></source>
<target>Dokumente im Posteingang: <x equiv-text="{{statistics.documents_inbox}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="c327c0e67bcac7494dcbaa9afb3b42d5008c6438">
<source>Total documents: <x equiv-text="{{statistics.documents_total}}" id="INTERPOLATION"/></source>
<target>Anzahl Dokumente gesamt: <x equiv-text="{{statistics.documents_total}}" id="INTERPOLATION"/></target>
<trans-unit datatype="html" id="c0d907c2687c09612395aee6ef7c04ca8e5e5e0a">
<source>Total documents: <x equiv-text="{{statistics?.documents_total}}" id="INTERPOLATION"/></source>
<target>Anzahl Dokumente gesamt: <x equiv-text="{{statistics?.documents_total}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="13e8d49dbcad9f9d71e66a9a56d6f328cff430c9">
<source>Documents in inbox: <x equiv-text="{{statistics?.documents_inbox}}" id="INTERPOLATION"/></source>
<target>Dokumente im Posteingang: <x equiv-text="{{statistics?.documents_inbox}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6443586946875325554">
<source>Processing: <x equiv-text="countUploadingAndProcessing" id="PH"/></source>
<target>Verarbeite: <x equiv-text="countUploadingAndProcessing" id="PH"/></target>
@@ -1848,12 +1864,20 @@
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6987083569809053351">
<source>English (GB)</source>
<target>Englisch (UK)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1858110241312746425">
<source>German</source>
<target>Deutsch</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
<context context-type="linenumber">84</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="3071065188816255493">
@@ -1861,7 +1885,7 @@
<target>Niederländisch</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">84</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7633754075223722162">
@@ -1869,7 +1893,7 @@
<target>Französisch</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">85</context>
<context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2119857572761283468">

View File

@@ -58,19 +58,19 @@
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2155249406916744630">
<source>View &quot;<x equiv-text="this.list.savedView.name" id="PH"/>&quot; saved successfully.</source>
<target>View &amp;quot;<x equiv-text="this.list.savedView.name" id="PH"/>&amp;quot; saved successfully.</target>
<source>View &quot;<x equiv-text="this.list.activeSavedViewTitle" id="PH"/>&quot; saved successfully.</source>
<target>View &quot;<x equiv-text="this.list.activeSavedViewTitle" id="PH"/>&quot; saved successfully.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">115</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6837554170707123455">
<source>View &quot;<x equiv-text="savedView.name" id="PH"/>&quot; created successfully.</source>
<target>View &amp;quot;<x equiv-text="savedView.name" id="PH"/>&amp;quot; created successfully.</target>
<target>View &quot;<x equiv-text="savedView.name" id="PH"/>&quot; created successfully.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">130</context>
<context context-type="linenumber">136</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="9ca82952a6bc860b5391d5975322d8af8ceddfa4">
@@ -129,9 +129,9 @@
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="72e7d343f9165602cce1ca7faffbc565fd31ef92">
<source>Save &quot;<x equiv-text="{{list.savedViewTitle}}" id="INTERPOLATION"/>&quot;</source>
<target>Save &amp;quot;<x equiv-text="{{list.savedViewTitle}}" id="INTERPOLATION"/>&amp;quot;</target>
<trans-unit datatype="html" id="5f5ce787c428d917c30c9bd70789a618e09743a7">
<source>Save &quot;<x equiv-text="{{list.activeSavedViewTitle}}" id="INTERPOLATION"/>&quot;</source>
<target>Save &quot;<x equiv-text="{{list.activeSavedViewTitle}}" id="INTERPOLATION"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">71</context>
@@ -219,7 +219,7 @@
</trans-unit>
<trans-unit datatype="html" id="5382975254277698192">
<source>Do you really want to delete document &quot;<x equiv-text="this.document.title" id="PH"/>&quot;?</source>
<target>Do you really want to delete document &amp;quot;<x equiv-text="this.document.title" id="PH"/>&amp;quot;?</target>
<target>Do you really want to delete document &quot;<x equiv-text="this.document.title" id="PH"/>&quot;?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">204</context>
@@ -475,7 +475,7 @@
</trans-unit>
<trans-unit datatype="html" id="93754014749412887">
<source>Do you really want to delete the tag &quot;<x equiv-text="object.name" id="PH"/>&quot;?</source>
<target>Do you really want to delete the tag &amp;quot;<x equiv-text="object.name" id="PH"/>&amp;quot;?</target>
<target>Do you really want to delete the tag &quot;<x equiv-text="object.name" id="PH"/>&quot;?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context>
<context context-type="linenumber">30</context>
@@ -515,7 +515,7 @@
</trans-unit>
<trans-unit datatype="html" id="8fa4d523f7b91df4390120b85ed0406138273e1a">
<source>Color</source>
<target>Color</target>
<target>Colour</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.html</context>
<context context-type="linenumber">20</context>
@@ -563,7 +563,7 @@
</trans-unit>
<trans-unit datatype="html" id="4990731724078522539">
<source>Do you really want to delete the document type &quot;<x equiv-text="object.name" id="PH"/>&quot;?</source>
<target>Do you really want to delete the document type &amp;quot;<x equiv-text="object.name" id="PH"/>&amp;quot;?</target>
<target>Do you really want to delete the document type &quot;<x equiv-text="object.name" id="PH"/>&quot;?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/document-type-list/document-type-list.component.ts</context>
<context context-type="linenumber">26</context>
@@ -585,17 +585,9 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5ca707824ab93066c7d9b44e1b8bf216725c2c22">
<source>Filter</source>
<target>Filter</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/logs/logs.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5610279464668232148">
<source>Saved view &quot;<x equiv-text="savedView.name" id="PH"/>&quot; deleted.</source>
<target>Saved view &amp;quot;<x equiv-text="savedView.name" id="PH"/>&amp;quot; deleted.</target>
<target>Saved view &quot;<x equiv-text="savedView.name" id="PH"/>&quot; deleted.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">67</context>
@@ -622,7 +614,15 @@
<target>Use date format of display language</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">95</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="4912706592792948707">
<source>ISO 8601</source>
<target>ISO 8601</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">97</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8488620293789898901">
@@ -630,7 +630,7 @@
<target>Error while storing settings on server: <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">111</context>
<context context-type="linenumber">114</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="121cc5391cd2a5115bc2b3160379ee5b36cd7716">
@@ -907,7 +907,7 @@
</trans-unit>
<trans-unit datatype="html" id="7427874343955308724">
<source>Do you really want to delete the correspondent &quot;<x equiv-text="object.name" id="PH"/>&quot;?</source>
<target>Do you really want to delete the correspondent &amp;quot;<x equiv-text="object.name" id="PH"/>&amp;quot;?</target>
<target>Do you really want to delete the correspondent &quot;<x equiv-text="object.name" id="PH"/>&quot;?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.ts</context>
<context context-type="linenumber">26</context>
@@ -1075,7 +1075,7 @@
</trans-unit>
<trans-unit datatype="html" id="afa760e48c97d64d19c1455d18b7834a2256e23f">
<source>Did you mean &quot;<x equiv-text="&lt;a [routerLink]=&quot;&quot; (click)=&quot;searchCorrectedQuery()&quot;&gt;{{correctedQuery}}" id="START_LINK"/><x equiv-text="{{correctedQuery}}&lt;/a&gt;" id="INTERPOLATION"/><x equiv-text="&lt;/a&gt;" id="CLOSE_LINK"/>&quot;?</source>
<target>Did you mean &amp;quot;<x equiv-text="&lt;a [routerLink]=&quot;&quot; (click)=&quot;searchCorrectedQuery()&quot;&gt;{{correctedQuery}}" id="START_LINK"/><x equiv-text="{{correctedQuery}}&lt;/a&gt;" id="INTERPOLATION"/><x equiv-text="&lt;/a&gt;" id="CLOSE_LINK"/>&amp;quot;?</target>
<target>Did you mean &quot;<x equiv-text="&lt;a [routerLink]=&quot;&quot; (click)=&quot;searchCorrectedQuery()&quot;&gt;{{correctedQuery}}" id="START_LINK"/><x equiv-text="{{correctedQuery}}&lt;/a&gt;" id="INTERPOLATION"/><x equiv-text="&lt;/a&gt;" id="CLOSE_LINK"/>&quot;?</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/search/search.component.html</context>
<context context-type="linenumber">13</context>
@@ -1234,6 +1234,14 @@
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6523384805359286307">
<source>Title: <x equiv-text="rule.value" id="PH"/></source>
<target>Title: <x equiv-text="rule.value" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="02d184c288f567825a1fcbf83bcd3099a10853d5">
<source>Filter tags</source>
<target>Filter tags</target>
@@ -1392,23 +1400,23 @@
<target>Error executing bulk operation: <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">73</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7894972847287473517">
<source>&quot;<x equiv-text="items[0].name" id="PH"/>&quot;</source>
<target>&amp;quot;<x equiv-text="items[0].name" id="PH"/>&amp;quot;</target>
<target>&quot;<x equiv-text="items[0].name" id="PH"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">112</context>
<context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8639884465898458690">
<source>&quot;<x equiv-text="items[0].name" id="PH"/>&quot; and &quot;<x equiv-text="items[1].name" id="PH_1"/>&quot;</source>
<target>&amp;quot;<x equiv-text="items[0].name" id="PH"/>&amp;quot; and &amp;quot;<x equiv-text="items[1].name" id="PH_1"/>&amp;quot;</target>
<target>&quot;<x equiv-text="items[0].name" id="PH"/>&quot; and &quot;<x equiv-text="items[1].name" id="PH_1"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">114</context>
<context context-type="linenumber">115</context>
</context-group>
<note from="description" priority="1">This is for messages like 'modify &quot;tag1&quot; and &quot;tag2&quot;'</note>
</trans-unit>
@@ -1417,16 +1425,16 @@
<target>, </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">117</context>
</context-group>
<note from="description" priority="1">this is used to separate enumerations and should probably be a comma and a whitespace in most languages</note>
</trans-unit>
<trans-unit datatype="html" id="1822679894391095557">
<source><x equiv-text="list" id="PH"/> and &quot;<x equiv-text="items[items.length - 1].name" id="PH_1"/>&quot;</source>
<target><x equiv-text="list" id="PH"/> and &amp;quot;<x equiv-text="items[items.length - 1].name" id="PH_1"/>&amp;quot;</target>
<target><x equiv-text="list" id="PH"/> and &quot;<x equiv-text="items[items.length - 1].name" id="PH_1"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">117</context>
<context context-type="linenumber">118</context>
</context-group>
<note from="description" priority="1">this is for messages like 'modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;'</note>
</trans-unit>
@@ -1435,15 +1443,15 @@
<target>Confirm tags assignment</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">127</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6619516195038467207">
<source>This operation will add the tag &quot;<x equiv-text="tag.name" id="PH"/>&quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</source>
<target>This operation will add the tag &amp;quot;<x equiv-text="tag.name" id="PH"/>&amp;quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<target>This operation will add the tag &quot;<x equiv-text="tag.name" id="PH"/>&quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">129</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1894412783609570695">
@@ -1451,15 +1459,15 @@
<target>This operation will add the tags <x equiv-text="this._localizeList(changedTags.itemsToAdd)" id="PH"/> to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7181166515756808573">
<source>This operation will remove the tag &quot;<x equiv-text="tag.name" id="PH"/>&quot; from <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</source>
<target>This operation will remove the tag &amp;quot;<x equiv-text="tag.name" id="PH"/>&amp;quot; from <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<target>This operation will remove the tag &quot;<x equiv-text="tag.name" id="PH"/>&quot; from <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="3819792277998068944">
@@ -1467,7 +1475,7 @@
<target>This operation will remove the tags <x equiv-text="this._localizeList(changedTags.itemsToRemove)" id="PH"/> from <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">136</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2739066218579571288">
@@ -1475,7 +1483,7 @@
<target>This operation will add the tags <x equiv-text="this._localizeList(changedTags.itemsToAdd)" id="PH"/> and remove the tags <x equiv-text="this._localizeList(changedTags.itemsToRemove)" id="PH_1"/> on <x equiv-text="this.list.selected.size" id="PH_2"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">138</context>
<context context-type="linenumber">139</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2996713129519325161">
@@ -1483,15 +1491,15 @@
<target>Confirm correspondent assignment</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">158</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6900893559485781849">
<source>This operation will assign the correspondent &quot;<x equiv-text="correspondent.name" id="PH"/>&quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</source>
<target>This operation will assign the correspondent &amp;quot;<x equiv-text="correspondent.name" id="PH"/>&amp;quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<target>This operation will assign the correspondent &quot;<x equiv-text="correspondent.name" id="PH"/>&quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">160</context>
<context context-type="linenumber">161</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1257522660364398440">
@@ -1499,7 +1507,7 @@
<target>This operation will remove the correspondent from <x equiv-text="this.list.selected.size" id="PH"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">162</context>
<context context-type="linenumber">163</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5393409374423140648">
@@ -1507,15 +1515,15 @@
<target>Confirm document type assignment</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">181</context>
<context context-type="linenumber">182</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="332180123895325027">
<source>This operation will assign the document type &quot;<x equiv-text="documentType.name" id="PH"/>&quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</source>
<target>This operation will assign the document type &amp;quot;<x equiv-text="documentType.name" id="PH"/>&amp;quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<target>This operation will assign the document type &quot;<x equiv-text="documentType.name" id="PH"/>&quot; to <x equiv-text="this.list.selected.size" id="PH_1"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2236642492594872779">
@@ -1523,7 +1531,7 @@
<target>This operation will remove the document type from <x equiv-text="this.list.selected.size" id="PH"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">185</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="749430623564850405">
@@ -1531,7 +1539,7 @@
<target>Delete confirm</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="4303174930844518780">
@@ -1539,7 +1547,7 @@
<target>This operation will permanently delete <x equiv-text="this.list.selected.size" id="PH"/> selected document(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">201</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5641451190833696892">
@@ -1547,7 +1555,7 @@
<target>This operation cannot be undone.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">202</context>
<context context-type="linenumber">203</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6734339521247847366">
@@ -1555,7 +1563,7 @@
<target>Delete document(s)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">204</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8b0609df23817024b3bed12beb9b64fc1009f588">
@@ -1582,6 +1590,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="fc2de37422d7c4af6686842283cc2afd781b6848">
<source>Download originals</source>
<target>Download originals</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="a1e6c11f20d4bf6e8e6b43e3c6d2561b2080645e">
<source>Suggestions:</source>
<target>Suggestions:</target>
@@ -1614,22 +1630,22 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="46c8fe557cf52c9389783627d4f85453f4ddb459">
<source>Documents in inbox: <x equiv-text="{{statistics.documents_inbox}}" id="INTERPOLATION"/></source>
<target>Documents in inbox: <x equiv-text="{{statistics.documents_inbox}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="c327c0e67bcac7494dcbaa9afb3b42d5008c6438">
<source>Total documents: <x equiv-text="{{statistics.documents_total}}" id="INTERPOLATION"/></source>
<target>Total documents: <x equiv-text="{{statistics.documents_total}}" id="INTERPOLATION"/></target>
<trans-unit datatype="html" id="c0d907c2687c09612395aee6ef7c04ca8e5e5e0a">
<source>Total documents: <x equiv-text="{{statistics?.documents_total}}" id="INTERPOLATION"/></source>
<target>Total documents: <x equiv-text="{{statistics?.documents_total}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="13e8d49dbcad9f9d71e66a9a56d6f328cff430c9">
<source>Documents in inbox: <x equiv-text="{{statistics?.documents_inbox}}" id="INTERPOLATION"/></source>
<target>Documents in inbox: <x equiv-text="{{statistics?.documents_inbox}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6443586946875325554">
<source>Processing: <x equiv-text="countUploadingAndProcessing" id="PH"/></source>
<target>Processing: <x equiv-text="countUploadingAndProcessing" id="PH"/></target>
@@ -1848,12 +1864,20 @@
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6987083569809053351">
<source>English (GB)</source>
<target>English (GB)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1858110241312746425">
<source>German</source>
<target>German</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
<context context-type="linenumber">84</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="3071065188816255493">
@@ -1861,7 +1885,7 @@
<target>Dutch</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">84</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7633754075223722162">
@@ -1869,7 +1893,7 @@
<target>French</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">85</context>
<context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2119857572761283468">

View File

@@ -58,11 +58,11 @@
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2155249406916744630">
<source>View &quot;<x equiv-text="this.list.savedView.name" id="PH"/>&quot; saved successfully.</source>
<target>Vue &quot;<x equiv-text="this.list.savedView.name" id="PH"/>&quot; enregistrée avec succès.</target>
<source>View &quot;<x equiv-text="this.list.activeSavedViewTitle" id="PH"/>&quot; saved successfully.</source>
<target>Vue &quot;<x equiv-text="this.list.activeSavedViewTitle" id="PH"/>&quot; enregistrée avec succès.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">115</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6837554170707123455">
@@ -70,7 +70,7 @@
<target>Vue &quot;<x equiv-text="savedView.name" id="PH"/>&quot; créée avec succès.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">130</context>
<context context-type="linenumber">136</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="9ca82952a6bc860b5391d5975322d8af8ceddfa4">
@@ -129,9 +129,9 @@
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="72e7d343f9165602cce1ca7faffbc565fd31ef92">
<source>Save &quot;<x equiv-text="{{list.savedViewTitle}}" id="INTERPOLATION"/>&quot;</source>
<target>Enregistrer &quot;<x equiv-text="{{list.savedViewTitle}}" id="INTERPOLATION"/>&quot;</target>
<trans-unit datatype="html" id="5f5ce787c428d917c30c9bd70789a618e09743a7">
<source>Save &quot;<x equiv-text="{{list.activeSavedViewTitle}}" id="INTERPOLATION"/>&quot;</source>
<target>Enregistrer &quot;<x equiv-text="{{list.activeSavedViewTitle}}" id="INTERPOLATION"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">71</context>
@@ -585,14 +585,6 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5ca707824ab93066c7d9b44e1b8bf216725c2c22">
<source>Filter</source>
<target>Filtrer</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/logs/logs.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5610279464668232148">
<source>Saved view &quot;<x equiv-text="savedView.name" id="PH"/>&quot; deleted.</source>
<target>Vue &quot;<x equiv-text="savedView.name" id="PH"/>&quot; supprimée.</target>
@@ -622,7 +614,15 @@
<target>Utiliser le format de date de la langue d'affichage</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">95</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="4912706592792948707">
<source>ISO 8601</source>
<target>ISO 8601</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">97</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8488620293789898901">
@@ -630,7 +630,7 @@
<target>Une erreur s'est produite lors de l'enregistrement des paramètres sur le serveur : <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">111</context>
<context context-type="linenumber">114</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="121cc5391cd2a5115bc2b3160379ee5b36cd7716">
@@ -1234,6 +1234,14 @@
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6523384805359286307">
<source>Title: <x equiv-text="rule.value" id="PH"/></source>
<target>Titre : <x equiv-text="rule.value" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="02d184c288f567825a1fcbf83bcd3099a10853d5">
<source>Filter tags</source>
<target>Filtrer les étiquettes</target>
@@ -1392,7 +1400,7 @@
<target>Une erreur s'est produite lors de l'exécution de l'opération de masse : <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">73</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7894972847287473517">
@@ -1400,7 +1408,7 @@
<target>&quot;<x equiv-text="items[0].name" id="PH"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">112</context>
<context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8639884465898458690">
@@ -1408,7 +1416,7 @@
<target>&quot;<x equiv-text="items[0].name" id="PH"/>&quot; et &quot;<x equiv-text="items[1].name" id="PH_1"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">114</context>
<context context-type="linenumber">115</context>
</context-group>
<note from="description" priority="1">This is for messages like 'modify &quot;tag1&quot; and &quot;tag2&quot;'</note>
</trans-unit>
@@ -1417,7 +1425,7 @@
<target>, </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">117</context>
</context-group>
<note from="description" priority="1">this is used to separate enumerations and should probably be a comma and a whitespace in most languages</note>
</trans-unit>
@@ -1426,7 +1434,7 @@
<target><x equiv-text="list" id="PH"/> et &quot;<x equiv-text="items[items.length - 1].name" id="PH_1"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">117</context>
<context context-type="linenumber">118</context>
</context-group>
<note from="description" priority="1">this is for messages like 'modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;'</note>
</trans-unit>
@@ -1435,7 +1443,7 @@
<target>Confirmer l'affectation des étiquettes</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">127</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6619516195038467207">
@@ -1443,7 +1451,7 @@
<target>Cette action affectera l'étiquette &quot;<x equiv-text="tag.name" id="PH"/>&quot; au(x) <x equiv-text="this.list.selected.size" id="PH_1"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">129</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1894412783609570695">
@@ -1451,7 +1459,7 @@
<target>Cette action affectera les étiquettes <x equiv-text="this._localizeList(changedTags.itemsToAdd)" id="PH"/> au(x) <x equiv-text="this.list.selected.size" id="PH_1"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7181166515756808573">
@@ -1459,7 +1467,7 @@
<target>Cette action supprimera l'étiquette &quot;<x equiv-text="tag.name" id="PH"/>&quot; de(s) <x equiv-text="this.list.selected.size" id="PH_1"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="3819792277998068944">
@@ -1467,7 +1475,7 @@
<target>Cette action supprimera les étiquettes <x equiv-text="this._localizeList(changedTags.itemsToRemove)" id="PH"/> de(s) <x equiv-text="this.list.selected.size" id="PH_1"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">136</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2739066218579571288">
@@ -1475,7 +1483,7 @@
<target>Cette action affectera les étiquettes <x equiv-text="this._localizeList(changedTags.itemsToAdd)" id="PH"/> et supprimera les étiquettes <x equiv-text="this._localizeList(changedTags.itemsToRemove)" id="PH_1"/> de(s) <x equiv-text="this.list.selected.size" id="PH_2"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">138</context>
<context context-type="linenumber">139</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2996713129519325161">
@@ -1483,7 +1491,7 @@
<target>Confirmer l'affectation du correspondant</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">158</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6900893559485781849">
@@ -1491,7 +1499,7 @@
<target>Cette action affectera le correspondant &quot;<x equiv-text="correspondent.name" id="PH"/>&quot; au(x) <x equiv-text="this.list.selected.size" id="PH_1"/>document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">160</context>
<context context-type="linenumber">161</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1257522660364398440">
@@ -1499,7 +1507,7 @@
<target>Cette action supprimera le correspondant de(s) <x equiv-text="this.list.selected.size" id="PH"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">162</context>
<context context-type="linenumber">163</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5393409374423140648">
@@ -1507,7 +1515,7 @@
<target>Confirmer l'affectation du type de document</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">181</context>
<context context-type="linenumber">182</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="332180123895325027">
@@ -1515,7 +1523,7 @@
<target>Cette action affectera le type de document &quot;<x equiv-text="documentType.name" id="PH"/>&quot; au(x) <x equiv-text="this.list.selected.size" id="PH_1"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2236642492594872779">
@@ -1523,7 +1531,7 @@
<target>Cette action supprimera le type de document de(s) <x equiv-text="this.list.selected.size" id="PH"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">185</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="749430623564850405">
@@ -1531,7 +1539,7 @@
<target>Confirmer la suppression</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="4303174930844518780">
@@ -1539,7 +1547,7 @@
<target>Cette action supprimera définitivement <x equiv-text="this.list.selected.size" id="PH"/> document(s) sélectionné(s).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">201</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5641451190833696892">
@@ -1547,7 +1555,7 @@
<target>Cette action est irréversible.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">202</context>
<context context-type="linenumber">203</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6734339521247847366">
@@ -1555,7 +1563,7 @@
<target>Supprimer le(s) document(s)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">204</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8b0609df23817024b3bed12beb9b64fc1009f588">
@@ -1582,6 +1590,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="fc2de37422d7c4af6686842283cc2afd781b6848">
<source>Download originals</source>
<target>Télécharger les originaux</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="a1e6c11f20d4bf6e8e6b43e3c6d2561b2080645e">
<source>Suggestions:</source>
<target>Suggestions : </target>
@@ -1614,22 +1630,22 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="46c8fe557cf52c9389783627d4f85453f4ddb459">
<source>Documents in inbox: <x equiv-text="{{statistics.documents_inbox}}" id="INTERPOLATION"/></source>
<target>Documents dans la boîte de réception : <x equiv-text="{{statistics.documents_inbox}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="c327c0e67bcac7494dcbaa9afb3b42d5008c6438">
<source>Total documents: <x equiv-text="{{statistics.documents_total}}" id="INTERPOLATION"/></source>
<target>Nombre total de documents : <x equiv-text="{{statistics.documents_total}}" id="INTERPOLATION"/></target>
<trans-unit datatype="html" id="c0d907c2687c09612395aee6ef7c04ca8e5e5e0a">
<source>Total documents: <x equiv-text="{{statistics?.documents_total}}" id="INTERPOLATION"/></source>
<target>Nombre total de documents : <x equiv-text="{{statistics?.documents_total}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="13e8d49dbcad9f9d71e66a9a56d6f328cff430c9">
<source>Documents in inbox: <x equiv-text="{{statistics?.documents_inbox}}" id="INTERPOLATION"/></source>
<target>Documents dans la boîte de réception : <x equiv-text="{{statistics?.documents_inbox}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6443586946875325554">
<source>Processing: <x equiv-text="countUploadingAndProcessing" id="PH"/></source>
<target>Traitement : <x equiv-text="countUploadingAndProcessing" id="PH"/></target>
@@ -1848,12 +1864,20 @@
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6987083569809053351">
<source>English (GB)</source>
<target>Anglais (GB)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1858110241312746425">
<source>German</source>
<target>Allemand</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
<context context-type="linenumber">84</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="3071065188816255493">
@@ -1861,7 +1885,7 @@
<target>Néerlandais</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">84</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7633754075223722162">
@@ -1869,7 +1893,7 @@
<target>Français</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">85</context>
<context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2119857572761283468">

View File

@@ -58,11 +58,11 @@
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2155249406916744630">
<source>View &quot;<x equiv-text="this.list.savedView.name" id="PH"/>&quot; saved successfully.</source>
<target>View &quot;<x equiv-text="this.list.savedView.name" id="PH"/>&quot; met succes opgeslagen.</target>
<source>View &quot;<x equiv-text="this.list.activeSavedViewTitle" id="PH"/>&quot; saved successfully.</source>
<target>View &quot;<x equiv-text="this.list.activeSavedViewTitle" id="PH"/>&quot; met succes opgeslagen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">109</context>
<context context-type="linenumber">115</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6837554170707123455">
@@ -70,7 +70,7 @@
<target>View &quot;<x equiv-text="savedView.name" id="PH"/>&quot; met succes gemaakt.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">130</context>
<context context-type="linenumber">136</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="9ca82952a6bc860b5391d5975322d8af8ceddfa4">
@@ -129,9 +129,9 @@
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="72e7d343f9165602cce1ca7faffbc565fd31ef92">
<source>Save &quot;<x equiv-text="{{list.savedViewTitle}}" id="INTERPOLATION"/>&quot;</source>
<target>Opslaan &quot;<x equiv-text="{{list.savedViewTitle}}" id="INTERPOLATION"/>&quot;</target>
<trans-unit datatype="html" id="5f5ce787c428d917c30c9bd70789a618e09743a7">
<source>Save &quot;<x equiv-text="{{list.activeSavedViewTitle}}" id="INTERPOLATION"/>&quot;</source>
<target>Opslaan &quot;<x equiv-text="{{list.activeSavedViewTitle}}" id="INTERPOLATION"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">71</context>
@@ -585,14 +585,6 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5ca707824ab93066c7d9b44e1b8bf216725c2c22">
<source>Filter</source>
<target>Filter</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/logs/logs.component.html</context>
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5610279464668232148">
<source>Saved view &quot;<x equiv-text="savedView.name" id="PH"/>&quot; deleted.</source>
<target>Opgeslagen view &quot;<x equiv-text="savedView.name" id="PH"/>&quot; verwijderd.</target>
@@ -622,7 +614,15 @@
<target>Datumopmaak van weergavetaal gebruiken</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">95</context>
<context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="4912706592792948707">
<source>ISO 8601</source>
<target>ISO 8601</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">97</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8488620293789898901">
@@ -630,7 +630,7 @@
<target>Fout bij het opslaan van de instellingen: <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
<context context-type="linenumber">111</context>
<context context-type="linenumber">114</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="121cc5391cd2a5115bc2b3160379ee5b36cd7716">
@@ -1234,6 +1234,14 @@
<context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6523384805359286307">
<source>Title: <x equiv-text="rule.value" id="PH"/></source>
<target>Titel: <x equiv-text="rule.value" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="02d184c288f567825a1fcbf83bcd3099a10853d5">
<source>Filter tags</source>
<target>Etiketten filteren</target>
@@ -1392,7 +1400,7 @@
<target>Fout bij het uitvoeren van een massabewerking: <x equiv-text="JSON.stringify(error.error)" id="PH"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">73</context>
<context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7894972847287473517">
@@ -1400,7 +1408,7 @@
<target>&quot;<x equiv-text="items[0].name" id="PH"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">112</context>
<context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8639884465898458690">
@@ -1408,7 +1416,7 @@
<target>&quot;<x equiv-text="items[0].name" id="PH"/>&quot; en &quot;<x equiv-text="items[1].name" id="PH_1"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">114</context>
<context context-type="linenumber">115</context>
</context-group>
<note from="description" priority="1">This is for messages like 'modify &quot;tag1&quot; and &quot;tag2&quot;'</note>
</trans-unit>
@@ -1417,7 +1425,7 @@
<target>, </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">117</context>
</context-group>
<note from="description" priority="1">this is used to separate enumerations and should probably be a comma and a whitespace in most languages</note>
</trans-unit>
@@ -1426,7 +1434,7 @@
<target><x equiv-text="list" id="PH"/> en &quot;<x equiv-text="items[items.length - 1].name" id="PH_1"/>&quot;</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">117</context>
<context context-type="linenumber">118</context>
</context-group>
<note from="description" priority="1">this is for messages like 'modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;'</note>
</trans-unit>
@@ -1435,7 +1443,7 @@
<target>Bevestig toewijzen van etiketten</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">126</context>
<context context-type="linenumber">127</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6619516195038467207">
@@ -1443,7 +1451,7 @@
<target>Het etiket &quot;<x equiv-text="tag.name" id="PH"/>&quot; zal aan <x equiv-text="this.list.selected.size" id="PH_1"/> geselecteerd(e) document(en) worden toegewezen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">129</context>
<context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1894412783609570695">
@@ -1451,7 +1459,7 @@
<target>De etiketten <x equiv-text="this._localizeList(changedTags.itemsToAdd)" id="PH"/> zullen aan <x equiv-text="this.list.selected.size" id="PH_1"/> geselecteerd(e) document(en) worden toegewezen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">131</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7181166515756808573">
@@ -1459,7 +1467,7 @@
<target>Het etiket &quot;<x equiv-text="tag.name" id="PH"/>&quot; zal verwijderd worden van <x equiv-text="this.list.selected.size" id="PH_1"/> geselecteerd(e) document(en).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">134</context>
<context context-type="linenumber">135</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="3819792277998068944">
@@ -1467,7 +1475,7 @@
<target>De etiketten <x equiv-text="this._localizeList(changedTags.itemsToRemove)" id="PH"/> zullen verwijderd worden van <x equiv-text="this.list.selected.size" id="PH_1"/> geselecteerd(e) document(en).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">136</context>
<context context-type="linenumber">137</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2739066218579571288">
@@ -1475,7 +1483,7 @@
<target>De etiketten <x equiv-text="this._localizeList(changedTags.itemsToAdd)" id="PH"/> zullen toegevoegd worden aan, en de etiketten <x equiv-text="this._localizeList(changedTags.itemsToRemove)" id="PH_1"/> zullen verwijderd worden van <x equiv-text="this.list.selected.size" id="PH_2"/> geselecteerd(e) document(en).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">138</context>
<context context-type="linenumber">139</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2996713129519325161">
@@ -1483,7 +1491,7 @@
<target>Bevestig toewijzen van correspondent</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">158</context>
<context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6900893559485781849">
@@ -1491,7 +1499,7 @@
<target>De correspondent &quot;<x equiv-text="correspondent.name" id="PH"/>&quot; zal aan <x equiv-text="this.list.selected.size" id="PH_1"/> geselecteerd(e) document(en) worden toegewezen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">160</context>
<context context-type="linenumber">161</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1257522660364398440">
@@ -1499,7 +1507,7 @@
<target>De correspondent zal verwijderd worden van <x equiv-text="this.list.selected.size" id="PH"/> geselecteerd(e) document(en).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">162</context>
<context context-type="linenumber">163</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5393409374423140648">
@@ -1507,7 +1515,7 @@
<target>Bevestig toewijzen van documenttype</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">181</context>
<context context-type="linenumber">182</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="332180123895325027">
@@ -1515,7 +1523,7 @@
<target>Het documenttype &quot;<x equiv-text="documentType.name" id="PH"/>&quot; zal aan <x equiv-text="this.list.selected.size" id="PH_1"/> geselecteerd(e) document(en) worden toegewezen.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2236642492594872779">
@@ -1523,7 +1531,7 @@
<target>Het documenttype zal verwijderd worden van <x equiv-text="this.list.selected.size" id="PH"/> geselecteerd(e) document(en).</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">185</context>
<context context-type="linenumber">186</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="749430623564850405">
@@ -1531,7 +1539,7 @@
<target>Bevestig verwijderen</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">200</context>
<context context-type="linenumber">201</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="4303174930844518780">
@@ -1539,7 +1547,7 @@
<target><x equiv-text="this.list.selected.size" id="PH"/> geselecteerd(e) document(en) zullen permanent worden verwijderd.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">201</context>
<context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="5641451190833696892">
@@ -1547,7 +1555,7 @@
<target>Deze actie kan niet ongedaan worden gemaakt.</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">202</context>
<context context-type="linenumber">203</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6734339521247847366">
@@ -1555,7 +1563,7 @@
<target>Verwijder document(en)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">204</context>
<context context-type="linenumber">205</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="8b0609df23817024b3bed12beb9b64fc1009f588">
@@ -1582,6 +1590,14 @@
<context context-type="linenumber">27</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="fc2de37422d7c4af6686842283cc2afd781b6848">
<source>Download originals</source>
<target>Originelen downloaden</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="a1e6c11f20d4bf6e8e6b43e3c6d2561b2080645e">
<source>Suggestions:</source>
<target>Suggesties:</target>
@@ -1614,22 +1630,22 @@
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="46c8fe557cf52c9389783627d4f85453f4ddb459">
<source>Documents in inbox: <x equiv-text="{{statistics.documents_inbox}}" id="INTERPOLATION"/></source>
<target>Documenten in &quot;Postvak in&quot;: <x equiv-text="{{statistics.documents_inbox}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="c327c0e67bcac7494dcbaa9afb3b42d5008c6438">
<source>Total documents: <x equiv-text="{{statistics.documents_total}}" id="INTERPOLATION"/></source>
<target>Totaal aantal documenten: <x equiv-text="{{statistics.documents_total}}" id="INTERPOLATION"/></target>
<trans-unit datatype="html" id="c0d907c2687c09612395aee6ef7c04ca8e5e5e0a">
<source>Total documents: <x equiv-text="{{statistics?.documents_total}}" id="INTERPOLATION"/></source>
<target>Totaal aantal documenten: <x equiv-text="{{statistics?.documents_total}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="13e8d49dbcad9f9d71e66a9a56d6f328cff430c9">
<source>Documents in inbox: <x equiv-text="{{statistics?.documents_inbox}}" id="INTERPOLATION"/></source>
<target>Documenten in &quot;Postvak in&quot;: <x equiv-text="{{statistics?.documents_inbox}}" id="INTERPOLATION"/></target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
<context context-type="linenumber">3</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6443586946875325554">
<source>Processing: <x equiv-text="countUploadingAndProcessing" id="PH"/></source>
<target>Bezig met verwerken: <x equiv-text="countUploadingAndProcessing" id="PH"/></target>
@@ -1848,12 +1864,20 @@
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="6987083569809053351">
<source>English (GB)</source>
<target>Engels (Brits)</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="1858110241312746425">
<source>German</source>
<target>Duits</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">83</context>
<context context-type="linenumber">84</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="3071065188816255493">
@@ -1861,7 +1885,7 @@
<target>Nederlands</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">84</context>
<context context-type="linenumber">85</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="7633754075223722162">
@@ -1869,7 +1893,7 @@
<target>Frans</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
<context context-type="linenumber">85</context>
<context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit datatype="html" id="2119857572761283468">

View File

@@ -1,10 +1,6 @@
from django.contrib import admin
from django.utils.html import format_html, format_html_join
from django.utils.safestring import mark_safe
from whoosh.writing import AsyncWriter
from . import index
from .models import Correspondent, Document, DocumentType, Log, Tag, \
from .models import Correspondent, Document, DocumentType, Tag, \
SavedView, SavedViewFilterRule
@@ -86,17 +82,21 @@ class DocumentAdmin(admin.ModelAdmin):
created_.short_description = "Created"
def delete_queryset(self, request, queryset):
ix = index.open_index()
with AsyncWriter(ix) as writer:
from documents import index
with index.open_index_writer() as writer:
for o in queryset:
index.remove_document(writer, o)
super(DocumentAdmin, self).delete_queryset(request, queryset)
def delete_model(self, request, obj):
from documents import index
index.remove_document_from_index(obj)
super(DocumentAdmin, self).delete_model(request, obj)
def save_model(self, request, obj, form, change):
from documents import index
index.add_or_update_document(obj)
super(DocumentAdmin, self).save_model(request, obj, form, change)

View File

@@ -0,0 +1,60 @@
from zipfile import ZipFile
from documents.models import Document
class BulkArchiveStrategy:
def __init__(self, zipf: ZipFile):
self.zipf = zipf
def make_unique_filename(self,
doc: Document,
archive: bool = False,
folder: str = ""):
counter = 0
while True:
filename = folder + doc.get_public_filename(archive, counter)
if filename in self.zipf.namelist():
counter += 1
else:
return filename
def add_document(self, doc: Document):
raise NotImplementedError() # pragma: no cover
class OriginalsOnlyStrategy(BulkArchiveStrategy):
def add_document(self, doc: Document):
self.zipf.write(doc.source_path, self.make_unique_filename(doc))
class ArchiveOnlyStrategy(BulkArchiveStrategy):
def __init__(self, zipf):
super(ArchiveOnlyStrategy, self).__init__(zipf)
def add_document(self, doc: Document):
if doc.has_archive_version:
self.zipf.write(doc.archive_path,
self.make_unique_filename(doc, archive=True))
else:
self.zipf.write(doc.source_path,
self.make_unique_filename(doc))
class OriginalAndArchiveStrategy(BulkArchiveStrategy):
def add_document(self, doc: Document):
if doc.has_archive_version:
self.zipf.write(
doc.archive_path, self.make_unique_filename(
doc, archive=True, folder="archive/"
)
)
self.zipf.write(
doc.source_path,
self.make_unique_filename(doc, folder="originals/")
)

View File

@@ -2,9 +2,7 @@ import itertools
from django.db.models import Q
from django_q.tasks import async_task
from whoosh.writing import AsyncWriter
from documents import index
from documents.models import Document, Correspondent, DocumentType
@@ -99,8 +97,9 @@ def modify_tags(doc_ids, add_tags, remove_tags):
def delete(doc_ids):
Document.objects.filter(id__in=doc_ids).delete()
ix = index.open_index()
with AsyncWriter(ix) as writer:
from documents import index
with index.open_index_writer() as writer:
for id in doc_ids:
index.remove_document_by_id(writer, id)

View File

@@ -5,7 +5,6 @@ import pickle
import re
from django.conf import settings
from django.core.cache import cache
from documents.models import Document, MatchingModel
@@ -31,29 +30,23 @@ def load_classifier():
)
return None
version = os.stat(settings.MODEL_FILE).st_mtime
classifier = DocumentClassifier()
try:
classifier.load()
classifier = cache.get("paperless-classifier", version=version)
if not classifier:
classifier = DocumentClassifier()
try:
classifier.load()
cache.set("paperless-classifier", classifier,
version=version, timeout=86400)
except (EOFError, IncompatibleClassifierVersionError) as e:
# there's something wrong with the model file.
logger.exception(
f"Unrecoverable error while loading document "
f"classification model, deleting model file."
)
os.unlink(settings.MODEL_FILE)
classifier = None
except OSError as e:
logger.error(
f"Error while loading document classification model: {str(e)}"
)
classifier = None
except (EOFError, IncompatibleClassifierVersionError) as e:
# there's something wrong with the model file.
logger.exception(
f"Unrecoverable error while loading document "
f"classification model, deleting model file."
)
os.unlink(settings.MODEL_FILE)
classifier = None
except OSError as e:
logger.error(
f"Error while loading document classification model: {str(e)}"
)
classifier = None
return classifier
@@ -102,9 +95,6 @@ class DocumentClassifier(object):
pickle.dump(self.document_type_classifier, f)
def train(self):
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import MultiLabelBinarizer, LabelBinarizer
data = list()
labels_tags = list()
@@ -169,6 +159,10 @@ class DocumentClassifier(object):
)
)
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import MultiLabelBinarizer, LabelBinarizer
# Step 2: vectorize data
logger.debug("Vectorizing data...")
self.data_vectorizer = CountVectorizer(

View File

@@ -86,6 +86,22 @@ def open_index(recreate=False):
return create_in(settings.INDEX_DIR, get_schema())
@contextmanager
def open_index_writer(ix=None, optimize=False):
if ix:
writer = AsyncWriter(ix)
else:
writer = AsyncWriter(open_index())
try:
yield writer
except Exception as e:
logger.exception(str(e))
writer.cancel()
finally:
writer.commit(optimize=optimize)
def update_document(writer, doc):
tags = ",".join([t.name for t in doc.tags.all()])
writer.update_document(
@@ -110,14 +126,12 @@ def remove_document_by_id(writer, doc_id):
def add_or_update_document(document):
ix = open_index()
with AsyncWriter(ix) as writer:
with open_index_writer() as writer:
update_document(writer, document)
def remove_document_from_index(document):
ix = open_index()
with AsyncWriter(ix) as writer:
with open_index_writer() as writer:
remove_document(writer, document)

View File

@@ -31,10 +31,24 @@ def handle_document(document_id):
parser_class = get_parser_class_for_mime_type(mime_type)
if not parser_class:
logger.error(f"No parser found for mime type {mime_type}, cannot "
f"archive document {document} (ID: {document_id})")
return
parser = parser_class(logging_group=uuid.uuid4())
try:
parser.parse(document.source_path, mime_type)
parser.parse(
document.source_path,
mime_type,
document.get_public_filename())
thumbnail = parser.get_optimised_thumbnail(
document.source_path,
mime_type,
document.get_public_filename()
)
if parser.get_archive_path():
with transaction.atomic():
@@ -55,12 +69,14 @@ def handle_document(document_id):
create_source_path_directory(document.archive_path)
shutil.move(parser.get_archive_path(),
document.archive_path)
shutil.move(thumbnail, document.thumbnail_path)
with AsyncWriter(index.open_index()) as writer:
index.update_document(writer, document)
with index.open_index_writer() as writer:
index.update_document(writer, document)
except Exception as e:
logger.exception(f"Error while parsing document {document}")
logger.exception(f"Error while parsing document {document} "
f"(ID: {document_id})")
finally:
parser.cleanup()

View File

@@ -1,6 +1,7 @@
import logging
import os
from pathlib import Path
from threading import Thread
from time import sleep
from django.conf import settings
@@ -57,6 +58,7 @@ def _consume(filepath):
logger.exception("Error creating tags from path")
try:
logger.info(f"Adding {filepath} to the task queue.")
async_task("documents.tasks.consume_file",
filepath,
override_tag_ids=tag_ids if tag_ids else None,
@@ -68,10 +70,11 @@ def _consume(filepath):
logger.exception("Error while consuming document")
def _consume_wait_unmodified(file, num_tries=20, wait_time=1):
def _consume_wait_unmodified(file):
logger.debug(f"Waiting for file {file} to remain unmodified")
mtime = -1
current_try = 0
while current_try < num_tries:
while current_try < settings.CONSUMER_POLLING_RETRY_COUNT:
try:
new_mtime = os.stat(file).st_mtime
except FileNotFoundError:
@@ -82,7 +85,7 @@ def _consume_wait_unmodified(file, num_tries=20, wait_time=1):
_consume(file)
return
mtime = new_mtime
sleep(wait_time)
sleep(settings.CONSUMER_POLLING_DELAY)
current_try += 1
logger.error(f"Timeout while waiting on file {file} to remain unmodified.")
@@ -91,10 +94,14 @@ def _consume_wait_unmodified(file, num_tries=20, wait_time=1):
class Handler(FileSystemEventHandler):
def on_created(self, event):
_consume_wait_unmodified(event.src_path)
Thread(
target=_consume_wait_unmodified, args=(event.src_path,)
).start()
def on_moved(self, event):
_consume_wait_unmodified(event.dest_path)
Thread(
target=_consume_wait_unmodified, args=(event.dest_path,)
).start()
class Command(BaseCommand):

View File

@@ -0,0 +1,15 @@
from django.core.management.base import BaseCommand
from documents.sanity_checker import check_sanity
class Command(BaseCommand):
help = """
This command checks your document archive for issues.
""".replace(" ", "")
def handle(self, *args, **options):
messages = check_sanity(progress=True)
messages.log_messages()

View File

@@ -6,7 +6,6 @@ import shutil
import subprocess
import tempfile
import dateparser
import magic
from django.conf import settings
from django.utils import timezone
@@ -200,6 +199,8 @@ def parse_date(filename, text):
"""
Call dateparser.parse with a particular date ordering
"""
import dateparser
return dateparser.parse(
ds,
settings={

View File

@@ -1,45 +1,55 @@
import hashlib
import logging
import os
from django.conf import settings
from tqdm import tqdm
from documents.models import Document
class SanityMessage:
message = None
class SanityCheckMessages:
def __init__(self):
self._messages = []
def error(self, message):
self._messages.append({"level": logging.ERROR, "message": message})
def warning(self, message):
self._messages.append({"level": logging.WARNING, "message": message})
def info(self, message):
self._messages.append({"level": logging.INFO, "message": message})
def log_messages(self):
logger = logging.getLogger("paperless.sanity_checker")
if len(self._messages) == 0:
logger.info("Sanity checker detected no issues.")
else:
for msg in self._messages:
logger.log(msg['level'], msg['message'])
def __len__(self):
return len(self._messages)
def __getitem__(self, item):
return self._messages[item]
def has_error(self):
return any([msg['level'] == logging.ERROR for msg in self._messages])
def has_warning(self):
return any([msg['level'] == logging.WARNING for msg in self._messages])
class SanityWarning(SanityMessage):
def __init__(self, message):
self.message = message
def __str__(self):
return f"Warning: {self.message}"
class SanityCheckFailedException(Exception):
pass
class SanityError(SanityMessage):
def __init__(self, message):
self.message = message
def __str__(self):
return f"ERROR: {self.message}"
class SanityFailedError(Exception):
def __init__(self, messages):
self.messages = messages
def __str__(self):
message_string = "\n".join([str(m) for m in self.messages])
return (
f"The following issuse were found by the sanity checker:\n"
f"{message_string}\n\n===============\n\n")
def check_sanity():
messages = []
def check_sanity(progress=False):
messages = SanityCheckMessages()
present_files = []
for root, subdirs, files in os.walk(settings.MEDIA_ROOT):
@@ -50,11 +60,15 @@ def check_sanity():
if lockfile in present_files:
present_files.remove(lockfile)
for doc in Document.objects.all():
if progress:
docs = tqdm(Document.objects.all())
else:
docs = Document.objects.all()
for doc in docs:
# Check sanity of the thumbnail
if not os.path.isfile(doc.thumbnail_path):
messages.append(SanityError(
f"Thumbnail of document {doc.pk} does not exist."))
messages.error(f"Thumbnail of document {doc.pk} does not exist.")
else:
if os.path.normpath(doc.thumbnail_path) in present_files:
present_files.remove(os.path.normpath(doc.thumbnail_path))
@@ -62,15 +76,14 @@ def check_sanity():
with doc.thumbnail_file as f:
f.read()
except OSError as e:
messages.append(SanityError(
messages.error(
f"Cannot read thumbnail file of document {doc.pk}: {e}"
))
)
# Check sanity of the original file
# TODO: extract method
if not os.path.isfile(doc.source_path):
messages.append(SanityError(
f"Original of document {doc.pk} does not exist."))
messages.error(f"Original of document {doc.pk} does not exist.")
else:
if os.path.normpath(doc.source_path) in present_files:
present_files.remove(os.path.normpath(doc.source_path))
@@ -78,31 +91,31 @@ def check_sanity():
with doc.source_file as f:
checksum = hashlib.md5(f.read()).hexdigest()
except OSError as e:
messages.append(SanityError(
f"Cannot read original file of document {doc.pk}: {e}"))
messages.error(
f"Cannot read original file of document {doc.pk}: {e}")
else:
if not checksum == doc.checksum:
messages.append(SanityError(
messages.error(
f"Checksum mismatch of document {doc.pk}. "
f"Stored: {doc.checksum}, actual: {checksum}."
))
)
# Check sanity of the archive file.
if doc.archive_checksum and not doc.archive_filename:
messages.append(SanityError(
messages.error(
f"Document {doc.pk} has an archive file checksum, but no "
f"archive filename."
))
)
elif not doc.archive_checksum and doc.archive_filename:
messages.append(SanityError(
messages.error(
f"Document {doc.pk} has an archive file, but its checksum is "
f"missing."
))
)
elif doc.has_archive_version:
if not os.path.isfile(doc.archive_path):
messages.append(SanityError(
messages.error(
f"Archived version of document {doc.pk} does not exist."
))
)
else:
if os.path.normpath(doc.archive_path) in present_files:
present_files.remove(os.path.normpath(doc.archive_path))
@@ -110,26 +123,23 @@ def check_sanity():
with doc.archive_file as f:
checksum = hashlib.md5(f.read()).hexdigest()
except OSError as e:
messages.append(SanityError(
messages.error(
f"Cannot read archive file of document {doc.pk}: {e}"
))
)
else:
if not checksum == doc.archive_checksum:
messages.append(SanityError(
messages.error(
f"Checksum mismatch of archived document "
f"{doc.pk}. "
f"Stored: {doc.checksum}, actual: {checksum}."
))
f"Stored: {doc.archive_checksum}, "
f"actual: {checksum}."
)
# other document checks
if not doc.content:
messages.append(SanityWarning(
f"Document {doc.pk} has no content."
))
messages.info(f"Document {doc.pk} has no content.")
for extra_file in present_files:
messages.append(SanityWarning(
f"Orphaned file in media dir: {extra_file}"
))
messages.warning(f"Orphaned file in media dir: {extra_file}")
return messages

View File

@@ -192,14 +192,34 @@ class SavedViewSerializer(serializers.ModelSerializer):
return saved_view
class BulkEditSerializer(serializers.Serializer):
class DocumentListSerializer(serializers.Serializer):
documents = serializers.ListField(
child=serializers.IntegerField(),
required=True,
label="Documents",
write_only=True
write_only=True,
child=serializers.IntegerField()
)
def _validate_document_id_list(self, documents, name="documents"):
if not type(documents) == list:
raise serializers.ValidationError(f"{name} must be a list")
if not all([type(i) == int for i in documents]):
raise serializers.ValidationError(
f"{name} must be a list of integers")
count = Document.objects.filter(id__in=documents).count()
if not count == len(documents):
raise serializers.ValidationError(
f"Some documents in {name} don't exist or were "
f"specified twice.")
def validate_documents(self, documents):
self._validate_document_id_list(documents)
return documents
class BulkEditSerializer(DocumentListSerializer):
method = serializers.ChoiceField(
choices=[
"set_correspondent",
@@ -215,18 +235,6 @@ class BulkEditSerializer(serializers.Serializer):
parameters = serializers.DictField(allow_empty=True)
def _validate_document_id_list(self, documents, name="documents"):
if not type(documents) == list:
raise serializers.ValidationError(f"{name} must be a list")
if not all([type(i) == int for i in documents]):
raise serializers.ValidationError(
f"{name} must be a list of integers")
count = Document.objects.filter(id__in=documents).count()
if not count == len(documents):
raise serializers.ValidationError(
f"Some documents in {name} don't exist or were "
f"specified twice.")
def _validate_tag_id_list(self, tags, name="tags"):
if not type(tags) == list:
raise serializers.ValidationError(f"{name} must be a list")
@@ -238,10 +246,6 @@ class BulkEditSerializer(serializers.Serializer):
raise serializers.ValidationError(
f"Some tags in {name} don't exist or were specified twice.")
def validate_documents(self, documents):
self._validate_document_id_list(documents)
return documents
def validate_method(self, method):
if method == "set_correspondent":
return bulk_edit.set_correspondent
@@ -392,9 +396,24 @@ class PostDocumentSerializer(serializers.Serializer):
return None
class SelectionDataSerializer(serializers.Serializer):
class BulkDownloadSerializer(DocumentListSerializer):
documents = serializers.ListField(
required=True,
child=serializers.IntegerField()
content = serializers.ChoiceField(
choices=["archive", "originals", "both"],
default="archive"
)
compression = serializers.ChoiceField(
choices=["none", "deflated", "bzip2", "lzma"],
default="none"
)
def validate_compression(self, compression):
import zipfile
return {
"none": zipfile.ZIP_STORED,
"deflated": zipfile.ZIP_DEFLATED,
"bzip2": zipfile.ZIP_BZIP2,
"lzma": zipfile.ZIP_LZMA
}[compression]

View File

@@ -11,7 +11,7 @@ from django.dispatch import receiver
from django.utils import timezone
from filelock import FileLock
from .. import index, matching
from .. import matching
from ..file_handling import delete_empty_directories, \
create_source_path_directory, \
generate_unique_filename
@@ -305,4 +305,6 @@ def set_log_entry(sender, document=None, logging_group=None, **kwargs):
def add_to_index(sender, document, **kwargs):
from documents import index
index.add_or_update_document(document)

View File

@@ -9,8 +9,7 @@ from documents import index, sanity_checker
from documents.classifier import DocumentClassifier, load_classifier
from documents.consumer import Consumer, ConsumerError
from documents.models import Document, Tag, DocumentType, Correspondent
from documents.sanity_checker import SanityFailedError
from documents.sanity_checker import SanityCheckFailedException
logger = logging.getLogger("paperless.tasks")
@@ -94,8 +93,15 @@ def consume_file(path,
def sanity_check():
messages = sanity_checker.check_sanity()
if len(messages) > 0:
raise SanityFailedError(messages)
messages.log_messages()
if messages.has_error():
raise SanityCheckFailedException(
"Sanity check failed with errors. See log.")
elif messages.has_warning():
return "Sanity check exited with warnings. See log."
elif len(messages) > 0:
return "Sanity check exited with infos. See log."
else:
return "No issues detected."

View File

@@ -4,6 +4,7 @@ from django.contrib.admin.sites import AdminSite
from django.test import TestCase
from django.utils import timezone
from documents import index
from documents.admin import DocumentAdmin
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
@@ -11,37 +12,52 @@ from documents.tests.utils import DirectoriesMixin
class TestDocumentAdmin(DirectoriesMixin, TestCase):
def get_document_from_index(self, doc):
ix = index.open_index()
with ix.searcher() as searcher:
return searcher.document(id=doc.id)
def setUp(self) -> None:
super(TestDocumentAdmin, self).setUp()
self.doc_admin = DocumentAdmin(model=Document, admin_site=AdminSite())
@mock.patch("documents.admin.index.add_or_update_document")
def test_save_model(self, m):
def test_save_model(self):
doc = Document.objects.create(title="test")
doc.title = "new title"
self.doc_admin.save_model(None, doc, None, None)
self.assertEqual(Document.objects.get(id=doc.id).title, "new title")
m.assert_called_once()
self.assertEqual(self.get_document_from_index(doc)['title'], "new title")
@mock.patch("documents.admin.index.remove_document")
def test_delete_model(self, m):
def test_delete_model(self):
doc = Document.objects.create(title="test")
self.doc_admin.delete_model(None, doc)
self.assertRaises(Document.DoesNotExist, Document.objects.get, id=doc.id)
m.assert_called_once()
index.add_or_update_document(doc)
self.assertIsNotNone(self.get_document_from_index(doc))
@mock.patch("documents.admin.index.remove_document")
def test_delete_queryset(self, m):
self.doc_admin.delete_model(None, doc)
self.assertRaises(Document.DoesNotExist, Document.objects.get, id=doc.id)
self.assertIsNone(self.get_document_from_index(doc))
def test_delete_queryset(self):
docs = []
for i in range(42):
Document.objects.create(title="Many documents with the same title", checksum=f"{i:02}")
doc = Document.objects.create(title="Many documents with the same title", checksum=f"{i:02}")
docs.append(doc)
index.add_or_update_document(doc)
self.assertEqual(Document.objects.count(), 42)
for doc in docs:
self.assertIsNotNone(self.get_document_from_index(doc))
self.doc_admin.delete_queryset(None, Document.objects.all())
self.assertEqual(m.call_count, 42)
self.assertEqual(Document.objects.count(), 0)
for doc in docs:
self.assertIsNone(self.get_document_from_index(doc))
def test_created(self):
doc = Document.objects.create(title="test", created=timezone.datetime(2020, 4, 12))
self.assertEqual(self.doc_admin.created_(doc), "2020-04-12")

View File

@@ -1,7 +1,10 @@
import datetime
import io
import json
import os
import shutil
import tempfile
import zipfile
from unittest import mock
from django.conf import settings
@@ -442,6 +445,13 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertEqual(response.data['documents_total'], 3)
self.assertEqual(response.data['documents_inbox'], 1)
def test_statistics_no_inbox_tag(self):
Document.objects.create(title="none1", checksum="A")
response = self.client.get("/api/statistics/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['documents_inbox'], None)
@mock.patch("documents.views.async_task")
def test_upload(self, m):
@@ -577,8 +587,11 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
def test_get_metadata(self):
doc = Document.objects.create(title="test", filename="file.pdf", mime_type="image/png", archive_checksum="A", archive_filename="archive.pdf")
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", "0000001.png"), doc.source_path)
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), doc.archive_path)
source_file = os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", "0000001.png")
archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
shutil.copy(source_file, doc.source_path)
shutil.copy(archive_file, doc.archive_path)
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
self.assertEqual(response.status_code, 200)
@@ -591,6 +604,8 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertGreater(len(meta['archive_metadata']), 0)
self.assertEqual(meta['media_filename'], "file.pdf")
self.assertEqual(meta['archive_media_filename'], "archive.pdf")
self.assertEqual(meta['original_size'], os.stat(source_file).st_size)
self.assertEqual(meta['archive_size'], os.stat(archive_file).st_size)
def test_get_metadata_invalid_doc(self):
response = self.client.get(f"/api/documents/34576/metadata/")
@@ -612,6 +627,21 @@ class TestDocumentApi(DirectoriesMixin, APITestCase):
self.assertIsNone(meta['archive_metadata'])
self.assertIsNone(meta['archive_media_filename'])
def test_get_metadata_missing_files(self):
doc = Document.objects.create(title="test", filename="file.pdf", mime_type="application/pdf", archive_filename="file.pdf", archive_checksum="B", checksum="A")
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
self.assertEqual(response.status_code, 200)
meta = response.data
self.assertTrue(meta['has_archive_version'])
self.assertIsNone(meta['original_metadata'])
self.assertIsNone(meta['original_size'])
self.assertIsNone(meta['archive_metadata'])
self.assertIsNone(meta['archive_size'])
def test_get_empty_suggestions(self):
doc = Document.objects.create(title="test", mime_type="application/pdf")
@@ -1096,6 +1126,113 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
self.assertCountEqual(response.data['selected_document_types'], [{"id": self.c1.id, "document_count": 1}, {"id": self.c2.id, "document_count": 0}])
class TestBulkDownload(DirectoriesMixin, APITestCase):
def setUp(self):
super(TestBulkDownload, self).setUp()
user = User.objects.create_superuser(username="temp_admin")
self.client.force_login(user=user)
self.doc1 = Document.objects.create(title="unrelated", checksum="A")
self.doc2 = Document.objects.create(title="document A", filename="docA.pdf", mime_type="application/pdf", checksum="B", created=datetime.datetime(2021, 1, 1))
self.doc2b = Document.objects.create(title="document A", filename="docA2.pdf", mime_type="application/pdf", checksum="D", created=datetime.datetime(2021, 1, 1))
self.doc3 = Document.objects.create(title="document B", filename="docB.jpg", mime_type="image/jpeg", checksum="C", created=datetime.datetime(2020, 3, 21), archive_filename="docB.pdf", archive_checksum="D")
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), self.doc2.source_path)
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.png"), self.doc2b.source_path)
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"), self.doc3.source_path)
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "test_with_bom.pdf"), self.doc3.archive_path)
def test_download_originals(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc3.id],
"content": "originals"
}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/zip')
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("2020-03-21 document B.jpg", zipf.namelist())
with self.doc2.source_file as f:
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
with self.doc3.source_file as f:
self.assertEqual(f.read(), zipf.read("2020-03-21 document B.jpg"))
def test_download_default(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc3.id]
}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/zip')
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("2020-03-21 document B.pdf", zipf.namelist())
with self.doc2.source_file as f:
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
with self.doc3.archive_file as f:
self.assertEqual(f.read(), zipf.read("2020-03-21 document B.pdf"))
def test_download_both(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc3.id],
"content": "both"
}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/zip')
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
self.assertEqual(len(zipf.filelist), 3)
self.assertIn("originals/2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("archive/2020-03-21 document B.pdf", zipf.namelist())
self.assertIn("originals/2020-03-21 document B.jpg", zipf.namelist())
with self.doc2.source_file as f:
self.assertEqual(f.read(), zipf.read("originals/2021-01-01 document A.pdf"))
with self.doc3.archive_file as f:
self.assertEqual(f.read(), zipf.read("archive/2020-03-21 document B.pdf"))
with self.doc3.source_file as f:
self.assertEqual(f.read(), zipf.read("originals/2020-03-21 document B.jpg"))
def test_filename_clashes(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc2b.id]
}), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/zip')
with zipfile.ZipFile(io.BytesIO(response.content)) as zipf:
self.assertEqual(len(zipf.filelist), 2)
self.assertIn("2021-01-01 document A.pdf", zipf.namelist())
self.assertIn("2021-01-01 document A_01.pdf", zipf.namelist())
with self.doc2.source_file as f:
self.assertEqual(f.read(), zipf.read("2021-01-01 document A.pdf"))
with self.doc2b.source_file as f:
self.assertEqual(f.read(), zipf.read("2021-01-01 document A_01.pdf"))
def test_compression(self):
response = self.client.post("/api/documents/bulk_download/", json.dumps({
"documents": [self.doc2.id, self.doc2b.id],
"compression": "lzma"
}), content_type='application/json')
class TestApiAuth(APITestCase):
def test_auth_required(self):
@@ -1119,4 +1256,5 @@ class TestApiAuth(APITestCase):
self.assertEqual(self.client.get("/api/search/").status_code, 401)
self.assertEqual(self.client.get("/api/search/auto_complete/").status_code, 401)
self.assertEqual(self.client.get("/api/documents/bulk_edit/").status_code, 401)
self.assertEqual(self.client.get("/api/documents/bulk_download/").status_code, 401)
self.assertEqual(self.client.get("/api/documents/selection_data/").status_code, 401)

View File

@@ -3,6 +3,7 @@ import tempfile
from pathlib import Path
from unittest import mock
import pytest
from django.conf import settings
from django.test import TestCase, override_settings
@@ -233,7 +234,6 @@ class TestClassifier(DirectoriesMixin, TestCase):
self.assertFalse(os.path.exists(settings.MODEL_FILE))
self.assertIsNone(load_classifier())
@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}})
@mock.patch("documents.classifier.DocumentClassifier.load")
def test_load_classifier(self, load):
Path(settings.MODEL_FILE).touch()
@@ -242,6 +242,7 @@ class TestClassifier(DirectoriesMixin, TestCase):
@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}})
@override_settings(MODEL_FILE=os.path.join(os.path.dirname(__file__), "data", "model.pickle"))
@pytest.mark.skip(reason="Disabled caching due to high memory usage - need to investigate.")
def test_load_classifier_cached(self):
classifier = load_classifier()
self.assertIsNotNone(classifier)
@@ -250,7 +251,6 @@ class TestClassifier(DirectoriesMixin, TestCase):
classifier2 = load_classifier()
load.assert_not_called()
@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}})
@mock.patch("documents.classifier.DocumentClassifier.load")
def test_load_classifier_incompatible_version(self, load):
Path(settings.MODEL_FILE).touch()
@@ -260,7 +260,6 @@ class TestClassifier(DirectoriesMixin, TestCase):
self.assertIsNone(load_classifier())
self.assertFalse(os.path.exists(settings.MODEL_FILE))
@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}})
@mock.patch("documents.classifier.DocumentClassifier.load")
def test_load_classifier_os_error(self, load):
Path(settings.MODEL_FILE).touch()

View File

@@ -49,6 +49,21 @@ class TestArchiver(DirectoriesMixin, TestCase):
self.assertTrue(filecmp.cmp(sample_file, doc.source_path))
self.assertEqual(doc.archive_filename, "none/A.pdf")
def test_unknown_mime_type(self):
doc = self.make_models()
doc.mime_type = "sdgfh"
doc.save()
shutil.copy(sample_file, doc.source_path)
handle_document(doc.pk)
doc = Document.objects.get(id=doc.id)
self.assertIsNotNone(doc.checksum)
self.assertIsNone(doc.archive_checksum)
self.assertIsNone(doc.archive_filename)
self.assertTrue(os.path.isfile(doc.source_path))
@override_settings(PAPERLESS_FILENAME_FORMAT="{title}")
def test_naming_priorities(self):
doc1 = Document.objects.create(checksum="A", title="document", content="first document", mime_type="application/pdf", filename="document.pdf")
@@ -65,6 +80,7 @@ class TestArchiver(DirectoriesMixin, TestCase):
self.assertEqual(doc1.archive_filename, "document.pdf")
self.assertEqual(doc2.archive_filename, "document_01.pdf")
class TestDecryptDocuments(TestCase):
@override_settings(
@@ -154,3 +170,24 @@ class TestCreateClassifier(TestCase):
call_command("document_create_classifier")
m.assert_called_once()
class TestSanityChecker(DirectoriesMixin, TestCase):
def test_no_issues(self):
with self.assertLogs() as capture:
call_command("document_sanity_checker")
self.assertEqual(len(capture.output), 1)
self.assertIn("Sanity checker detected no issues.", capture.output[0])
def test_errors(self):
doc = Document.objects.create(title="test", content="test", filename="test.pdf", checksum="abc")
Path(doc.source_path).touch()
Path(doc.thumbnail_path).touch()
with self.assertLogs() as capture:
call_command("document_sanity_checker")
self.assertEqual(len(capture.output), 1)
self.assertIn("Checksum mismatch of document", capture.output[0])

View File

@@ -203,7 +203,7 @@ class TestConsumer(DirectoriesMixin, ConsumerMixin, TransactionTestCase):
self.assertRaises(CommandError, call_command, 'document_consumer', '--oneshot')
@override_settings(CONSUMER_POLLING=1)
@override_settings(CONSUMER_POLLING=1, CONSUMER_POLLING_DELAY=1, CONSUMER_POLLING_RETRY_COUNT=20)
class TestConsumerPolling(TestConsumer):
# just do all the tests with polling
pass
@@ -215,8 +215,7 @@ class TestConsumerRecursive(TestConsumer):
pass
@override_settings(CONSUMER_RECURSIVE=True)
@override_settings(CONSUMER_POLLING=1)
@override_settings(CONSUMER_RECURSIVE=True, CONSUMER_POLLING=1, CONSUMER_POLLING_DELAY=1, CONSUMER_POLLING_RETRY_COUNT=20)
class TestConsumerRecursivePolling(TestConsumer):
# just do all the tests with polling and recursive
pass
@@ -257,6 +256,6 @@ class TestConsumerTags(DirectoriesMixin, ConsumerMixin, TransactionTestCase):
# their order.
self.assertCountEqual(kwargs["override_tag_ids"], tag_ids)
@override_settings(CONSUMER_POLLING=1)
@override_settings(CONSUMER_POLLING=1, CONSUMER_POLLING_DELAY=1, CONSUMER_POLLING_RETRY_COUNT=20)
def test_consume_file_with_path_tags_polling(self):
self.test_consume_file_with_path_tags()

View File

@@ -1,3 +1,4 @@
import logging
import os
import shutil
from pathlib import Path
@@ -7,10 +8,59 @@ from django.conf import settings
from django.test import TestCase
from documents.models import Document
from documents.sanity_checker import check_sanity, SanityFailedError
from documents.sanity_checker import check_sanity, SanityCheckMessages
from documents.tests.utils import DirectoriesMixin
class TestSanityCheckMessages(TestCase):
def test_no_messages(self):
messages = SanityCheckMessages()
self.assertEqual(len(messages), 0)
self.assertFalse(messages.has_error())
self.assertFalse(messages.has_warning())
with self.assertLogs() as capture:
messages.log_messages()
self.assertEqual(len(capture.output), 1)
self.assertEqual(capture.records[0].levelno, logging.INFO)
self.assertEqual(capture.records[0].message, "Sanity checker detected no issues.")
def test_info(self):
messages = SanityCheckMessages()
messages.info("Something might be wrong")
self.assertEqual(len(messages), 1)
self.assertFalse(messages.has_error())
self.assertFalse(messages.has_warning())
with self.assertLogs() as capture:
messages.log_messages()
self.assertEqual(len(capture.output), 1)
self.assertEqual(capture.records[0].levelno, logging.INFO)
self.assertEqual(capture.records[0].message, "Something might be wrong")
def test_warning(self):
messages = SanityCheckMessages()
messages.warning("Something is wrong")
self.assertEqual(len(messages), 1)
self.assertFalse(messages.has_error())
self.assertTrue(messages.has_warning())
with self.assertLogs() as capture:
messages.log_messages()
self.assertEqual(len(capture.output), 1)
self.assertEqual(capture.records[0].levelno, logging.WARNING)
self.assertEqual(capture.records[0].message, "Something is wrong")
def test_error(self):
messages = SanityCheckMessages()
messages.error("Something is seriously wrong")
self.assertEqual(len(messages), 1)
self.assertTrue(messages.has_error())
self.assertFalse(messages.has_warning())
with self.assertLogs() as capture:
messages.log_messages()
self.assertEqual(len(capture.output), 1)
self.assertEqual(capture.records[0].levelno, logging.ERROR)
self.assertEqual(capture.records[0].message, "Something is seriously wrong")
class TestSanityCheck(DirectoriesMixin, TestCase):
def make_test_data(self):
@@ -23,6 +73,11 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
return Document.objects.create(title="test", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", content="test", pk=1, filename="0000001.pdf", mime_type="application/pdf", archive_filename="0000001.pdf")
def assertSanityError(self, messageRegex):
messages = check_sanity()
self.assertTrue(messages.has_error())
self.assertRegex(messages[0]['message'], messageRegex)
def test_no_docs(self):
self.assertEqual(len(check_sanity()), 0)
@@ -33,72 +88,75 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
def test_no_thumbnail(self):
doc = self.make_test_data()
os.remove(doc.thumbnail_path)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Thumbnail of document .* does not exist")
def test_thumbnail_no_access(self):
doc = self.make_test_data()
os.chmod(doc.thumbnail_path, 0o000)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Cannot read thumbnail file of document")
os.chmod(doc.thumbnail_path, 0o777)
def test_no_original(self):
doc = self.make_test_data()
os.remove(doc.source_path)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Original of document .* does not exist.")
def test_original_no_access(self):
doc = self.make_test_data()
os.chmod(doc.source_path, 0o000)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Cannot read original file of document")
os.chmod(doc.source_path, 0o777)
def test_original_checksum_mismatch(self):
doc = self.make_test_data()
doc.checksum = "WOW"
doc.save()
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Checksum mismatch of document")
def test_no_archive(self):
doc = self.make_test_data()
os.remove(doc.archive_path)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Archived version of document .* does not exist.")
def test_archive_no_access(self):
doc = self.make_test_data()
os.chmod(doc.archive_path, 0o000)
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Cannot read archive file of document")
os.chmod(doc.archive_path, 0o777)
def test_archive_checksum_mismatch(self):
doc = self.make_test_data()
doc.archive_checksum = "WOW"
doc.save()
self.assertEqual(len(check_sanity()), 1)
self.assertSanityError("Checksum mismatch of archived document")
def test_empty_content(self):
doc = self.make_test_data()
doc.content = ""
doc.save()
self.assertEqual(len(check_sanity()), 1)
messages = check_sanity()
self.assertFalse(messages.has_error())
self.assertFalse(messages.has_warning())
self.assertEqual(len(messages), 1)
self.assertRegex(messages[0]['message'], "Document .* has no content.")
def test_orphaned_file(self):
doc = self.make_test_data()
Path(self.dirs.originals_dir, "orphaned").touch()
self.assertEqual(len(check_sanity()), 1)
def test_error_tostring(self):
Document.objects.create(title="test", checksum="dgfhj", archive_checksum="dfhg", content="", pk=1, filename="0000001.pdf", archive_filename="0000001.pdf")
string = str(SanityFailedError(check_sanity()))
self.assertIsNotNone(string)
messages = check_sanity()
self.assertFalse(messages.has_error())
self.assertTrue(messages.has_warning())
self.assertEqual(len(messages), 1)
self.assertRegex(messages[0]['message'], "Orphaned file in media dir")
def test_archive_filename_no_checksum(self):
doc = self.make_test_data()
doc.archive_checksum = None
doc.save()
self.assertEqual(len(check_sanity()), 2)
self.assertSanityError("has an archive file, but its checksum is missing.")
def test_archive_checksum_no_filename(self):
doc = self.make_test_data()
doc.archive_filename = None
doc.save()
self.assertEqual(len(check_sanity()), 2)
self.assertSanityError("has an archive file checksum, but no archive filename.")

View File

@@ -2,12 +2,12 @@ import os
from unittest import mock
from django.conf import settings
from django.test import TestCase, override_settings
from django.test import TestCase
from django.utils import timezone
from documents import tasks
from documents.models import Document, Tag, Correspondent, DocumentType
from documents.sanity_checker import SanityError, SanityFailedError
from documents.sanity_checker import SanityCheckMessages, SanityCheckFailedException
from documents.tests.utils import DirectoriesMixin
@@ -52,7 +52,6 @@ class TestTasks(DirectoriesMixin, TestCase):
load_classifier.assert_called_once()
self.assertFalse(os.path.isfile(settings.MODEL_FILE))
@override_settings(CACHES={'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}})
def test_train_classifier(self):
c = Correspondent.objects.create(matching_algorithm=Tag.MATCH_AUTO, name="test")
doc = Document.objects.create(correspondent=c, content="test", title="test")
@@ -75,13 +74,33 @@ class TestTasks(DirectoriesMixin, TestCase):
self.assertNotEqual(mtime2, mtime3)
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check(self, m):
m.return_value = []
tasks.sanity_check()
def test_sanity_check_success(self, m):
m.return_value = SanityCheckMessages()
self.assertEqual(tasks.sanity_check(), "No issues detected.")
m.assert_called_once()
m.reset_mock()
m.return_value = [SanityError("")]
self.assertRaises(SanityFailedError, tasks.sanity_check)
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check_error(self, m):
messages = SanityCheckMessages()
messages.error("Some error")
m.return_value = messages
self.assertRaises(SanityCheckFailedException, tasks.sanity_check)
m.assert_called_once()
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check_warning(self, m):
messages = SanityCheckMessages()
messages.warning("Some warning")
m.return_value = messages
self.assertEqual(tasks.sanity_check(), "Sanity check exited with warnings. See log.")
m.assert_called_once()
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check_info(self, m):
messages = SanityCheckMessages()
messages.info("Some info")
m.return_value = messages
self.assertEqual(tasks.sanity_check(), "Sanity check exited with infos. See log.")
m.assert_called_once()
def test_bulk_update_documents(self):

View File

@@ -2,6 +2,7 @@ import logging
import os
import tempfile
import uuid
import zipfile
from datetime import datetime
from time import mktime
@@ -32,9 +33,10 @@ from rest_framework.viewsets import (
ViewSet
)
import documents.index as index
from paperless.db import GnuPG
from paperless.views import StandardPagination
from .bulk_download import OriginalAndArchiveStrategy, OriginalsOnlyStrategy, \
ArchiveOnlyStrategy
from .classifier import load_classifier
from .filters import (
CorrespondentFilterSet,
@@ -52,7 +54,9 @@ from .serialisers import (
DocumentTypeSerializer,
PostDocumentSerializer,
SavedViewSerializer,
BulkEditSerializer, SelectionDataSerializer
BulkEditSerializer,
DocumentListSerializer,
BulkDownloadSerializer
)
@@ -176,10 +180,12 @@ class DocumentViewSet(RetrieveModelMixin,
def update(self, request, *args, **kwargs):
response = super(DocumentViewSet, self).update(
request, *args, **kwargs)
from documents import index
index.add_or_update_document(self.get_object())
return response
def destroy(self, request, *args, **kwargs):
from documents import index
index.remove_document_from_index(self.get_object())
return super(DocumentViewSet, self).destroy(request, *args, **kwargs)
@@ -225,6 +231,12 @@ class DocumentViewSet(RetrieveModelMixin,
else:
return []
def get_filesize(self, filename):
if os.path.isfile(filename):
return os.stat(filename).st_size
else:
return None
@action(methods=['get'], detail=True)
def metadata(self, request, pk=None):
try:
@@ -234,7 +246,7 @@ class DocumentViewSet(RetrieveModelMixin,
meta = {
"original_checksum": doc.checksum,
"original_size": os.stat(doc.source_path).st_size,
"original_size": self.get_filesize(doc.source_path),
"original_mime_type": doc.mime_type,
"media_filename": doc.filename,
"has_archive_version": doc.has_archive_version,
@@ -245,7 +257,7 @@ class DocumentViewSet(RetrieveModelMixin,
}
if doc.has_archive_version:
meta['archive_size'] = os.stat(doc.archive_path).st_size,
meta['archive_size'] = self.get_filesize(doc.archive_path)
meta['archive_metadata'] = self.get_metadata(
doc.archive_path, "application/pdf")
else:
@@ -437,7 +449,7 @@ class PostDocumentView(APIView):
class SelectionDataView(APIView):
permission_classes = (IsAuthenticated,)
serializer_class = SelectionDataSerializer
serializer_class = DocumentListSerializer
parser_classes = (parsers.MultiPartParser, parsers.JSONParser)
def get_serializer_context(self):
@@ -495,10 +507,6 @@ class SearchView(APIView):
permission_classes = (IsAuthenticated,)
def __init__(self, *args, **kwargs):
super(SearchView, self).__init__(*args, **kwargs)
self.ix = index.open_index()
def add_infos_to_hit(self, r):
try:
doc = Document.objects.get(id=r['id'])
@@ -519,6 +527,7 @@ class SearchView(APIView):
}
def get(self, request, format=None):
from documents import index
if 'query' in request.query_params:
query = request.query_params['query']
@@ -548,8 +557,10 @@ class SearchView(APIView):
if page < 1:
page = 1
ix = index.open_index()
try:
with index.query_page(self.ix, page, query, more_like_id, more_like_content) as (result_page, corrected_query): # NOQA: E501
with index.query_page(ix, page, query, more_like_id, more_like_content) as (result_page, corrected_query): # NOQA: E501
return Response(
{'count': len(result_page),
'page': result_page.pagenum,
@@ -564,10 +575,6 @@ class SearchAutoCompleteView(APIView):
permission_classes = (IsAuthenticated,)
def __init__(self, *args, **kwargs):
super(SearchAutoCompleteView, self).__init__(*args, **kwargs)
self.ix = index.open_index()
def get(self, request, format=None):
if 'term' in request.query_params:
term = request.query_params['term']
@@ -581,7 +588,11 @@ class SearchAutoCompleteView(APIView):
else:
limit = 10
return Response(index.autocomplete(self.ix, term, limit))
from documents import index
ix = index.open_index()
return Response(index.autocomplete(ix, term, limit))
class StatisticsView(APIView):
@@ -589,8 +600,66 @@ class StatisticsView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
return Response({
'documents_total': Document.objects.all().count(),
'documents_inbox': Document.objects.filter(
documents_total = Document.objects.all().count()
if Tag.objects.filter(is_inbox_tag=True).exists():
documents_inbox = Document.objects.filter(
tags__is_inbox_tag=True).distinct().count()
else:
documents_inbox = None
return Response({
'documents_total': documents_total,
'documents_inbox': documents_inbox,
})
class BulkDownloadView(APIView):
permission_classes = (IsAuthenticated,)
serializer_class = BulkDownloadSerializer
parser_classes = (parsers.JSONParser,)
def get_serializer_context(self):
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def post(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
ids = serializer.validated_data.get('documents')
compression = serializer.validated_data.get('compression')
content = serializer.validated_data.get('content')
os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
temp = tempfile.NamedTemporaryFile(
dir=settings.SCRATCH_DIR,
suffix="-compressed-archive",
delete=False)
if content == 'both':
strategy_class = OriginalAndArchiveStrategy
elif content == 'originals':
strategy_class = OriginalsOnlyStrategy
else:
strategy_class = ArchiveOnlyStrategy
with zipfile.ZipFile(temp.name, "w", compression) as zipf:
strategy = strategy_class(zipf)
for id in ids:
doc = Document.objects.get(id=id)
strategy.add_document(doc)
with open(temp.name, "rb") as f:
response = HttpResponse(f, content_type="application/zip")
response["Content-Disposition"] = '{}; filename="{}"'.format(
"attachment", "documents.zip")
return response

View File

@@ -0,0 +1,650 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Štěpán Šebestian <mys.orangeorange0123@gmail.com>, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-28 22:02+0100\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"Last-Translator: Štěpán Šebestian <mys.orangeorange0123@gmail.com>, 2021\n"
"Language-Team: Czech (https://www.transifex.com/paperless/teams/115905/cs/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: cs\n"
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Dokumenty"
#: documents/models.py:33
msgid "Any word"
msgstr "Jakékoliv slovo"
#: documents/models.py:34
msgid "All words"
msgstr "Všechna slova"
#: documents/models.py:35
msgid "Exact match"
msgstr "Přesná shoda"
#: documents/models.py:36
msgid "Regular expression"
msgstr "Regulární výraz"
#: documents/models.py:37
msgid "Fuzzy word"
msgstr "Fuzzy slovo"
#: documents/models.py:38
msgid "Automatic"
msgstr "Automatický"
#: documents/models.py:42 documents/models.py:352 paperless_mail/models.py:25
#: paperless_mail/models.py:109
msgid "name"
msgstr "název"
#: documents/models.py:46
msgid "match"
msgstr "shoda"
#: documents/models.py:50
msgid "matching algorithm"
msgstr "algoritmus pro shodu"
#: documents/models.py:56
msgid "is insensitive"
msgstr "je ignorováno"
#: documents/models.py:75 documents/models.py:135
msgid "correspondent"
msgstr "korespondent"
#: documents/models.py:76
msgid "correspondents"
msgstr "korespondenti"
#: documents/models.py:98
msgid "color"
msgstr "barva"
#: documents/models.py:102
msgid "is inbox tag"
msgstr "tag přichozí"
#: documents/models.py:104
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
msgstr ""
"Označí tento tag jako tag pro příchozí: Všechny nově zkonzumované dokumenty "
"budou označeny tagem pro přichozí"
#: documents/models.py:109
msgid "tag"
msgstr "tag"
#: documents/models.py:110 documents/models.py:166
msgid "tags"
msgstr "tagy"
#: documents/models.py:116 documents/models.py:148
msgid "document type"
msgstr "typ dokumentu"
#: documents/models.py:117
msgid "document types"
msgstr "typy dokumentu"
#: documents/models.py:125
msgid "Unencrypted"
msgstr "Nešifrované"
#: documents/models.py:126
msgid "Encrypted with GNU Privacy Guard"
msgstr "Šifrované pomocí GNU Privacy Guard"
#: documents/models.py:139
msgid "title"
msgstr "titulek"
#: documents/models.py:152
msgid "content"
msgstr "obsah"
#: documents/models.py:154
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
msgstr ""
"Nezpracovaná, pouze textová data dokumentu. Toto pole je používáno především"
" pro vyhledávání."
#: documents/models.py:159
msgid "mime type"
msgstr "mime typ"
#: documents/models.py:170
msgid "checksum"
msgstr "kontrolní součet"
#: documents/models.py:174
msgid "The checksum of the original document."
msgstr "Kontrolní součet původního dokumentu"
#: documents/models.py:178
msgid "archive checksum"
msgstr "kontrolní součet archivu"
#: documents/models.py:183
msgid "The checksum of the archived document."
msgstr "Kontrolní součet archivovaného dokumentu."
#: documents/models.py:187 documents/models.py:330
msgid "created"
msgstr "vytvořeno"
#: documents/models.py:191
msgid "modified"
msgstr "upraveno"
#: documents/models.py:195
msgid "storage type"
msgstr "typ úložiště"
#: documents/models.py:203
msgid "added"
msgstr "přidáno"
#: documents/models.py:207
msgid "filename"
msgstr "název souboru"
#: documents/models.py:212
msgid "Current filename in storage"
msgstr "Aktuální název souboru v úložišti"
#: documents/models.py:216
msgid "archive serial number"
msgstr "sériové číslo archivu"
#: documents/models.py:221
msgid "The position of this document in your physical document archive."
msgstr "Pozice dokumentu ve vašem archivu fyzických dokumentů"
#: documents/models.py:227
msgid "document"
msgstr "dokument"
#: documents/models.py:228
msgid "documents"
msgstr "dokumenty"
#: documents/models.py:313
msgid "debug"
msgstr "debug"
#: documents/models.py:314
msgid "information"
msgstr "informace"
#: documents/models.py:315
msgid "warning"
msgstr "varování"
#: documents/models.py:316
msgid "error"
msgstr "chyba"
#: documents/models.py:317
msgid "critical"
msgstr "kritická"
#: documents/models.py:321
msgid "group"
msgstr "skupina"
#: documents/models.py:324
msgid "message"
msgstr "zpráva"
#: documents/models.py:327
msgid "level"
msgstr "úroveň"
#: documents/models.py:334
msgid "log"
msgstr "záznam"
#: documents/models.py:335
msgid "logs"
msgstr "záznamy"
#: documents/models.py:346 documents/models.py:396
msgid "saved view"
msgstr "uložený pohled"
#: documents/models.py:347
msgid "saved views"
msgstr "uložené pohledy"
#: documents/models.py:350
msgid "user"
msgstr "uživatel"
#: documents/models.py:356
msgid "show on dashboard"
msgstr "zobrazit v dashboardu"
#: documents/models.py:359
msgid "show in sidebar"
msgstr "zobrazit v postranním menu"
#: documents/models.py:363
msgid "sort field"
msgstr "pole na řazení"
#: documents/models.py:366
msgid "sort reverse"
msgstr "třídit opačně"
#: documents/models.py:372
msgid "title contains"
msgstr "titulek obsahuje"
#: documents/models.py:373
msgid "content contains"
msgstr "obsah obsahuje"
#: documents/models.py:374
msgid "ASN is"
msgstr "ASN je"
#: documents/models.py:375
msgid "correspondent is"
msgstr "korespondent je"
#: documents/models.py:376
msgid "document type is"
msgstr "typ dokumentu je"
#: documents/models.py:377
msgid "is in inbox"
msgstr "je v příchozích"
#: documents/models.py:378
msgid "has tag"
msgstr "má tag"
#: documents/models.py:379
msgid "has any tag"
msgstr "má jakýkoliv tag"
#: documents/models.py:380
msgid "created before"
msgstr "vytvořeno před"
#: documents/models.py:381
msgid "created after"
msgstr "vytvořeno po"
#: documents/models.py:382
msgid "created year is"
msgstr "rok vytvoření je"
#: documents/models.py:383
msgid "created month is"
msgstr "měsíc vytvoření je"
#: documents/models.py:384
msgid "created day is"
msgstr "den vytvoření je"
#: documents/models.py:385
msgid "added before"
msgstr "přidáno před"
#: documents/models.py:386
msgid "added after"
msgstr "přidáno po"
#: documents/models.py:387
msgid "modified before"
msgstr "upraveno před"
#: documents/models.py:388
msgid "modified after"
msgstr "upraveno po"
#: documents/models.py:389
msgid "does not have tag"
msgstr "nemá tag"
#: documents/models.py:400
msgid "rule type"
msgstr "typ pravidla"
#: documents/models.py:404
msgid "value"
msgstr "hodnota"
#: documents/models.py:410
msgid "filter rule"
msgstr "filtrovací pravidlo"
#: documents/models.py:411
msgid "filter rules"
msgstr "filtrovací pravidla"
#: documents/serialisers.py:383
#, python-format
msgid "File type %(type)s not supported"
msgstr "Typ souboru %(type)s není podporován"
#: documents/templates/index.html:20
msgid "Paperless-ng is loading..."
msgstr "Paperless-ng se načítá..."
#: documents/templates/registration/logged_out.html:13
msgid "Paperless-ng signed out"
msgstr "Odhlášeno od Paperless-ng"
#: documents/templates/registration/logged_out.html:41
msgid "You have been successfully logged out. Bye!"
msgstr "Byli jste úspěšně odhlášeni. Nashledanou!"
#: documents/templates/registration/logged_out.html:42
msgid "Sign in again"
msgstr "Přihlašte se znovu"
#: documents/templates/registration/login.html:13
msgid "Paperless-ng sign in"
msgstr "Paperless-ng přihlášení"
#: documents/templates/registration/login.html:42
msgid "Please sign in."
msgstr "Prosím přihlaste se."
#: documents/templates/registration/login.html:45
msgid "Your username and password didn't match. Please try again."
msgstr "Vaše uživatelské jméno a heslo se neshodují. Prosím, zkuste to znovu."
#: documents/templates/registration/login.html:48
msgid "Username"
msgstr "Uživatelské jméno"
#: documents/templates/registration/login.html:49
msgid "Password"
msgstr "Heslo"
#: documents/templates/registration/login.html:54
msgid "Sign in"
msgstr "Přihlásit se"
#: paperless/settings.py:286
msgid "English"
msgstr "Angličtina"
#: paperless/settings.py:287
msgid "German"
msgstr "Němčina"
#: paperless/settings.py:288
msgid "Dutch"
msgstr "Holandština"
#: paperless/settings.py:289
msgid "French"
msgstr "Francouzština"
#: paperless/urls.py:114
msgid "Paperless-ng administration"
msgstr "Správa Paperless-ng"
#: paperless_mail/admin.py:25
msgid "Filter"
msgstr "Filtr"
#: paperless_mail/admin.py:27
msgid ""
"Paperless will only process mails that match ALL of the filters given below."
msgstr ""
"Paperless zpracuje pouze emaily které odpovídají VŠEM níže zadaným filtrům."
#: paperless_mail/admin.py:37
msgid "Actions"
msgstr "Akce"
#: paperless_mail/admin.py:39
msgid ""
"The action applied to the mail. This action is only performed when documents"
" were consumed from the mail. Mails without attachments will remain entirely"
" untouched."
msgstr ""
"Akce provedena na emailu. Tato akce je provedena jen pokud byly dokumenty "
"zkonzumovány z emailu. Emaily bez příloh zůstanou nedotčeny."
#: paperless_mail/admin.py:46
msgid "Metadata"
msgstr "Metadata"
#: paperless_mail/admin.py:48
msgid ""
"Assign metadata to documents consumed from this rule automatically. If you "
"do not assign tags, types or correspondents here, paperless will still "
"process all matching rules that you have defined."
msgstr ""
"Automaticky přiřadit metadata dokumentům zkonzumovaných z tohoto pravidla. "
"Pokud zde nepřiřadíte tagy, typy nebo korespondenty, paperless stále "
"zpracuje všechna shodující-se pravidla které jste definovali."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless pošta"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "emailový účet"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "emailové účty"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Žádné šifrování"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Používat SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Používat STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "IMAP server"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "IMAP port"
#: paperless_mail/models.py:36
msgid ""
"This is usually 143 for unencrypted and STARTTLS connections, and 993 for "
"SSL connections."
msgstr ""
"Toto je většinou 143 pro nešifrovaná připojení/připojení používající "
"STARTTLS a 993 pro SSL připojení."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "IMAP bezpečnost"
#: paperless_mail/models.py:46
msgid "username"
msgstr "uživatelské jméno"
#: paperless_mail/models.py:50
msgid "password"
msgstr "heslo"
#: paperless_mail/models.py:60
msgid "mail rule"
msgstr "mailové pravidlo"
#: paperless_mail/models.py:61
msgid "mail rules"
msgstr "mailová pravidla"
#: paperless_mail/models.py:67
msgid "Only process attachments."
msgstr "Zpracovávat jen přílohy"
#: paperless_mail/models.py:68
msgid "Process all files, including 'inline' attachments."
msgstr "Zpracovat všechny soubory, včetně vložených příloh"
#: paperless_mail/models.py:78
msgid "Mark as read, don't process read mails"
msgstr "Označit jako přečtené, nezpracovávat přečtené emaily"
#: paperless_mail/models.py:79
msgid "Flag the mail, don't process flagged mails"
msgstr "Označit email, nezpracovávat označené emaily"
#: paperless_mail/models.py:80
msgid "Move to specified folder"
msgstr "Přesunout do specifikované složky"
#: paperless_mail/models.py:81
msgid "Delete"
msgstr "Odstranit"
#: paperless_mail/models.py:88
msgid "Use subject as title"
msgstr "Použít předmět jako titulek"
#: paperless_mail/models.py:89
msgid "Use attachment filename as title"
msgstr "Použít název souboru u přílohy jako titulek"
#: paperless_mail/models.py:99
msgid "Do not assign a correspondent"
msgstr "Nepřiřazovat korespondenta"
#: paperless_mail/models.py:101
msgid "Use mail address"
msgstr "Použít emailovou adresu"
#: paperless_mail/models.py:103
msgid "Use name (or mail address if not available)"
msgstr "Použít jméno (nebo emailovou adresu pokud jméno není dostupné)"
#: paperless_mail/models.py:105
msgid "Use correspondent selected below"
msgstr "Použít korespondenta vybraného níže"
#: paperless_mail/models.py:113
msgid "order"
msgstr "pořadí"
#: paperless_mail/models.py:120
msgid "account"
msgstr "účet"
#: paperless_mail/models.py:124
msgid "folder"
msgstr "složka"
#: paperless_mail/models.py:128
msgid "filter from"
msgstr "filtrovat z"
#: paperless_mail/models.py:131
msgid "filter subject"
msgstr "název filtru"
#: paperless_mail/models.py:134
msgid "filter body"
msgstr "tělo filtru"
#: paperless_mail/models.py:138
msgid "filter attachment filename"
msgstr "název souboru u přílohy filtru"
#: paperless_mail/models.py:140
msgid ""
"Only consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr ""
"Konzumovat jen dokumenty které přesně odpovídají tomuto názvu souboru pokud "
"specifikováno. Zástupné znaky jako *.pdf nebo *invoice* jsou povoleny. "
"Nezáleží na velikosti písmen."
#: paperless_mail/models.py:146
msgid "maximum age"
msgstr "maximální stáří"
#: paperless_mail/models.py:148
msgid "Specified in days."
msgstr "Specifikováno ve dnech."
#: paperless_mail/models.py:151
msgid "attachment type"
msgstr "typ přílohy"
#: paperless_mail/models.py:154
msgid ""
"Inline attachments include embedded images, so it's best to combine this "
"option with a filename filter."
msgstr ""
"Vložené přílohy zahrnují vložené obrázky, takže je nejlepší tuto možnost "
"kombinovat s filtrem na název souboru"
#: paperless_mail/models.py:159
msgid "action"
msgstr "akce"
#: paperless_mail/models.py:165
msgid "action parameter"
msgstr "parametr akce"
#: paperless_mail/models.py:167
msgid ""
"Additional parameter for the action selected above, i.e., the target folder "
"of the move to folder action."
msgstr ""
"Další parametr pro výše vybranou akci, napříkad cílová složka akce přesunutí"
" do složky."
#: paperless_mail/models.py:173
msgid "assign title from"
msgstr "nastavit titulek z"
#: paperless_mail/models.py:183
msgid "assign this tag"
msgstr "přiřadit tento tag"
#: paperless_mail/models.py:191
msgid "assign this document type"
msgstr "přiřadit tento typ dokumentu"
#: paperless_mail/models.py:195
msgid "assign correspondent from"
msgstr "přiřadit korespondenta z"
#: paperless_mail/models.py:205
msgid "assign this correspondent"
msgstr "přiřadit tohoto korespondenta"

View File

@@ -11,8 +11,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-28 22:02+0100\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"POT-Creation-Date: 2021-02-16 14:52+0100\n"
"PO-Revision-Date: 2021-02-16 18:37+0000\n"
"Last-Translator: Jonas Winkler, 2021\n"
"Language-Team: German (https://www.transifex.com/paperless/teams/115905/de/)\n"
"MIME-Version: 1.0\n"
@@ -25,64 +25,64 @@ msgstr ""
msgid "Documents"
msgstr "Dokumente"
#: documents/models.py:33
#: documents/models.py:32
msgid "Any word"
msgstr "Irgendein Wort"
#: documents/models.py:34
#: documents/models.py:33
msgid "All words"
msgstr "Alle Wörter"
#: documents/models.py:35
#: documents/models.py:34
msgid "Exact match"
msgstr "Exakte Übereinstimmung"
#: documents/models.py:36
#: documents/models.py:35
msgid "Regular expression"
msgstr "Regulärer Ausdruck"
#: documents/models.py:37
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Ungenaues Wort"
#: documents/models.py:38
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatisch"
#: documents/models.py:42 documents/models.py:352 paperless_mail/models.py:25
#: documents/models.py:41 documents/models.py:364 paperless_mail/models.py:25
#: paperless_mail/models.py:109
msgid "name"
msgstr "Name"
#: documents/models.py:46
#: documents/models.py:45
msgid "match"
msgstr "Zuweisungsmuster"
#: documents/models.py:50
#: documents/models.py:49
msgid "matching algorithm"
msgstr "Zuweisungsalgorithmus"
#: documents/models.py:56
#: documents/models.py:55
msgid "is insensitive"
msgstr "Groß-/Kleinschreibung irrelevant"
#: documents/models.py:75 documents/models.py:135
#: documents/models.py:74 documents/models.py:134
msgid "correspondent"
msgstr "Korrespondent"
#: documents/models.py:76
#: documents/models.py:75
msgid "correspondents"
msgstr "Korrespondenten"
#: documents/models.py:98
#: documents/models.py:97
msgid "color"
msgstr "Farbe"
#: documents/models.py:102
#: documents/models.py:101
msgid "is inbox tag"
msgstr "Posteingangs-Tag"
#: documents/models.py:104
#: documents/models.py:103
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
@@ -90,39 +90,39 @@ msgstr ""
"Markiert das Tag als Posteingangs-Tag. Neue Dokumente werden immer mit "
"diesem Tag versehen."
#: documents/models.py:109
#: documents/models.py:108
msgid "tag"
msgstr "Tag"
#: documents/models.py:110 documents/models.py:166
#: documents/models.py:109 documents/models.py:165
msgid "tags"
msgstr "Tags"
#: documents/models.py:116 documents/models.py:148
#: documents/models.py:115 documents/models.py:147
msgid "document type"
msgstr "Dokumenttyp"
#: documents/models.py:117
#: documents/models.py:116
msgid "document types"
msgstr "Dokumenttypen"
#: documents/models.py:125
#: documents/models.py:124
msgid "Unencrypted"
msgstr "Nicht verschlüsselt"
#: documents/models.py:126
#: documents/models.py:125
msgid "Encrypted with GNU Privacy Guard"
msgstr "Verschlüsselt mit GNU Privacy Guard"
#: documents/models.py:139
#: documents/models.py:138
msgid "title"
msgstr "Titel"
#: documents/models.py:152
#: documents/models.py:151
msgid "content"
msgstr "Inhalt"
#: documents/models.py:154
#: documents/models.py:153
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
@@ -130,43 +130,43 @@ msgstr ""
"Der Inhalt des Dokuments in Textform. Dieses Feld wird primär für die Suche "
"verwendet."
#: documents/models.py:159
#: documents/models.py:158
msgid "mime type"
msgstr "MIME-Typ"
#: documents/models.py:170
#: documents/models.py:169
msgid "checksum"
msgstr "Prüfsumme"
#: documents/models.py:174
#: documents/models.py:173
msgid "The checksum of the original document."
msgstr "Die Prüfsumme des originalen Dokuments."
#: documents/models.py:178
#: documents/models.py:177
msgid "archive checksum"
msgstr "Archiv-Prüfsumme"
#: documents/models.py:183
#: documents/models.py:182
msgid "The checksum of the archived document."
msgstr "Die Prüfsumme des archivierten Dokuments."
#: documents/models.py:187 documents/models.py:330
#: documents/models.py:186 documents/models.py:342
msgid "created"
msgstr "Ausgestellt"
#: documents/models.py:191
#: documents/models.py:190
msgid "modified"
msgstr "Geändert"
#: documents/models.py:195
#: documents/models.py:194
msgid "storage type"
msgstr "Speichertyp"
#: documents/models.py:203
#: documents/models.py:202
msgid "added"
msgstr "Hinzugefügt"
#: documents/models.py:207
#: documents/models.py:206
msgid "filename"
msgstr "Dateiname"
@@ -175,178 +175,186 @@ msgid "Current filename in storage"
msgstr "Aktueller Dateiname im Datenspeicher"
#: documents/models.py:216
msgid "archive filename"
msgstr "Archiv-Dateiname"
#: documents/models.py:222
msgid "Current archive filename in storage"
msgstr "Aktueller Dateiname im Archiv"
#: documents/models.py:226
msgid "archive serial number"
msgstr "Archiv-Seriennummer"
#: documents/models.py:221
#: documents/models.py:231
msgid "The position of this document in your physical document archive."
msgstr "Die Position dieses Dokuments in Ihrem physischen Dokumentenarchiv."
#: documents/models.py:227
#: documents/models.py:237
msgid "document"
msgstr "Dokument"
#: documents/models.py:228
#: documents/models.py:238
msgid "documents"
msgstr "Dokumente"
#: documents/models.py:313
#: documents/models.py:325
msgid "debug"
msgstr "Debug"
#: documents/models.py:314
#: documents/models.py:326
msgid "information"
msgstr "Information"
#: documents/models.py:315
#: documents/models.py:327
msgid "warning"
msgstr "Warnung"
#: documents/models.py:316
#: documents/models.py:328
msgid "error"
msgstr "Fehler"
#: documents/models.py:317
#: documents/models.py:329
msgid "critical"
msgstr "Kritisch"
#: documents/models.py:321
#: documents/models.py:333
msgid "group"
msgstr "Gruppe"
#: documents/models.py:324
#: documents/models.py:336
msgid "message"
msgstr "Nachricht"
#: documents/models.py:327
#: documents/models.py:339
msgid "level"
msgstr "Level"
#: documents/models.py:334
#: documents/models.py:346
msgid "log"
msgstr "Protokoll"
#: documents/models.py:335
#: documents/models.py:347
msgid "logs"
msgstr "Protokoll"
#: documents/models.py:346 documents/models.py:396
#: documents/models.py:358 documents/models.py:408
msgid "saved view"
msgstr "Gespeicherte Ansicht"
#: documents/models.py:347
#: documents/models.py:359
msgid "saved views"
msgstr "Gespeicherte Ansichten"
#: documents/models.py:350
#: documents/models.py:362
msgid "user"
msgstr "Benutzer"
#: documents/models.py:356
#: documents/models.py:368
msgid "show on dashboard"
msgstr "Auf Startseite zeigen"
#: documents/models.py:359
#: documents/models.py:371
msgid "show in sidebar"
msgstr "In Seitenleiste zeigen"
#: documents/models.py:363
#: documents/models.py:375
msgid "sort field"
msgstr "Sortierfeld"
#: documents/models.py:366
#: documents/models.py:378
msgid "sort reverse"
msgstr "Umgekehrte Sortierung"
#: documents/models.py:372
#: documents/models.py:384
msgid "title contains"
msgstr "Titel enthält"
#: documents/models.py:373
#: documents/models.py:385
msgid "content contains"
msgstr "Inhalt enthält"
#: documents/models.py:374
#: documents/models.py:386
msgid "ASN is"
msgstr "ASN ist"
#: documents/models.py:375
#: documents/models.py:387
msgid "correspondent is"
msgstr "Korrespondent ist"
#: documents/models.py:376
#: documents/models.py:388
msgid "document type is"
msgstr "Dokumenttyp ist"
#: documents/models.py:377
#: documents/models.py:389
msgid "is in inbox"
msgstr "Ist im Posteingang"
#: documents/models.py:378
#: documents/models.py:390
msgid "has tag"
msgstr "Hat Tag"
#: documents/models.py:379
#: documents/models.py:391
msgid "has any tag"
msgstr "Hat irgendein Tag"
#: documents/models.py:380
#: documents/models.py:392
msgid "created before"
msgstr "Ausgestellt vor"
#: documents/models.py:381
#: documents/models.py:393
msgid "created after"
msgstr "Ausgestellt nach"
#: documents/models.py:382
#: documents/models.py:394
msgid "created year is"
msgstr "Ausgestellt im Jahr"
#: documents/models.py:383
#: documents/models.py:395
msgid "created month is"
msgstr "Ausgestellt im Monat"
#: documents/models.py:384
#: documents/models.py:396
msgid "created day is"
msgstr "Ausgestellt am Tag"
#: documents/models.py:385
#: documents/models.py:397
msgid "added before"
msgstr "Hinzugefügt vor"
#: documents/models.py:386
#: documents/models.py:398
msgid "added after"
msgstr "Hinzugefügt nach"
#: documents/models.py:387
#: documents/models.py:399
msgid "modified before"
msgstr "Geändert vor"
#: documents/models.py:388
#: documents/models.py:400
msgid "modified after"
msgstr "Geändert nach"
#: documents/models.py:389
#: documents/models.py:401
msgid "does not have tag"
msgstr "Hat nicht folgendes Tag"
#: documents/models.py:400
#: documents/models.py:412
msgid "rule type"
msgstr "Regeltyp"
#: documents/models.py:404
#: documents/models.py:416
msgid "value"
msgstr "Wert"
#: documents/models.py:410
#: documents/models.py:422
msgid "filter rule"
msgstr "Filterregel"
#: documents/models.py:411
#: documents/models.py:423
msgid "filter rules"
msgstr "Filterregeln"
#: documents/serialisers.py:383
#: documents/serialisers.py:370
#, python-format
msgid "File type %(type)s not supported"
msgstr "Dateityp %(type)s nicht unterstützt"
@@ -393,19 +401,23 @@ msgstr "Passwort"
msgid "Sign in"
msgstr "Anmelden"
#: paperless/settings.py:286
msgid "English"
msgstr "Englisch"
#: paperless/settings.py:291
msgid "English (US)"
msgstr "Englisch (US)"
#: paperless/settings.py:287
#: paperless/settings.py:292
msgid "English (GB)"
msgstr "Englisch (UK)"
#: paperless/settings.py:293
msgid "German"
msgstr "Deutsch"
#: paperless/settings.py:288
#: paperless/settings.py:294
msgid "Dutch"
msgstr "Niederländisch"
#: paperless/settings.py:289
#: paperless/settings.py:295
msgid "French"
msgstr "Französisch"

View File

@@ -4,16 +4,17 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Ali Bates <xadium@gmail.com>, 2021
# Ali Bates, 2021
# Jonas Winkler, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-28 22:02+0100\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"Last-Translator: Ali Bates <xadium@gmail.com>, 2021\n"
"POT-Creation-Date: 2021-02-16 14:52+0100\n"
"PO-Revision-Date: 2021-02-16 18:37+0000\n"
"Last-Translator: Jonas Winkler, 2021\n"
"Language-Team: English (United Kingdom) (https://www.transifex.com/paperless/teams/115905/en_GB/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -25,64 +26,64 @@ msgstr ""
msgid "Documents"
msgstr "Documents"
#: documents/models.py:33
#: documents/models.py:32
msgid "Any word"
msgstr "Any word"
#: documents/models.py:34
#: documents/models.py:33
msgid "All words"
msgstr "All words"
#: documents/models.py:35
#: documents/models.py:34
msgid "Exact match"
msgstr "Exact match"
#: documents/models.py:36
#: documents/models.py:35
msgid "Regular expression"
msgstr "Regular expression"
#: documents/models.py:37
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Fuzzy word"
#: documents/models.py:38
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatic"
#: documents/models.py:42 documents/models.py:352 paperless_mail/models.py:25
#: documents/models.py:41 documents/models.py:364 paperless_mail/models.py:25
#: paperless_mail/models.py:109
msgid "name"
msgstr "name"
#: documents/models.py:46
#: documents/models.py:45
msgid "match"
msgstr "match"
#: documents/models.py:50
#: documents/models.py:49
msgid "matching algorithm"
msgstr "matching algorithm"
#: documents/models.py:56
#: documents/models.py:55
msgid "is insensitive"
msgstr "is insensitive"
#: documents/models.py:75 documents/models.py:135
#: documents/models.py:74 documents/models.py:134
msgid "correspondent"
msgstr "correspondent"
#: documents/models.py:76
#: documents/models.py:75
msgid "correspondents"
msgstr "correspondents"
#: documents/models.py:98
#: documents/models.py:97
msgid "color"
msgstr "color"
msgstr "colour"
#: documents/models.py:102
#: documents/models.py:101
msgid "is inbox tag"
msgstr "is inbox tag"
#: documents/models.py:104
#: documents/models.py:103
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
@@ -90,39 +91,39 @@ msgstr ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
#: documents/models.py:109
#: documents/models.py:108
msgid "tag"
msgstr "tag"
#: documents/models.py:110 documents/models.py:166
#: documents/models.py:109 documents/models.py:165
msgid "tags"
msgstr "tags"
#: documents/models.py:116 documents/models.py:148
#: documents/models.py:115 documents/models.py:147
msgid "document type"
msgstr "document type"
#: documents/models.py:117
#: documents/models.py:116
msgid "document types"
msgstr "document types"
#: documents/models.py:125
#: documents/models.py:124
msgid "Unencrypted"
msgstr "Unencrypted"
#: documents/models.py:126
#: documents/models.py:125
msgid "Encrypted with GNU Privacy Guard"
msgstr "Encrypted with GNU Privacy Guard"
#: documents/models.py:139
#: documents/models.py:138
msgid "title"
msgstr "title"
#: documents/models.py:152
#: documents/models.py:151
msgid "content"
msgstr "content"
#: documents/models.py:154
#: documents/models.py:153
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
@@ -130,43 +131,43 @@ msgstr ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
#: documents/models.py:159
#: documents/models.py:158
msgid "mime type"
msgstr "mime type"
#: documents/models.py:170
#: documents/models.py:169
msgid "checksum"
msgstr "checksum"
#: documents/models.py:174
#: documents/models.py:173
msgid "The checksum of the original document."
msgstr "The checksum of the original document."
#: documents/models.py:178
#: documents/models.py:177
msgid "archive checksum"
msgstr "archive checksum"
#: documents/models.py:183
#: documents/models.py:182
msgid "The checksum of the archived document."
msgstr "The checksum of the archived document."
#: documents/models.py:187 documents/models.py:330
#: documents/models.py:186 documents/models.py:342
msgid "created"
msgstr "created"
#: documents/models.py:191
#: documents/models.py:190
msgid "modified"
msgstr "modified"
#: documents/models.py:195
#: documents/models.py:194
msgid "storage type"
msgstr "storage type"
#: documents/models.py:203
#: documents/models.py:202
msgid "added"
msgstr "added"
#: documents/models.py:207
#: documents/models.py:206
msgid "filename"
msgstr "filename"
@@ -175,178 +176,186 @@ msgid "Current filename in storage"
msgstr "Current filename in storage"
#: documents/models.py:216
msgid "archive filename"
msgstr "archive filename"
#: documents/models.py:222
msgid "Current archive filename in storage"
msgstr "Current archive filename in storage"
#: documents/models.py:226
msgid "archive serial number"
msgstr "archive serial number"
#: documents/models.py:221
#: documents/models.py:231
msgid "The position of this document in your physical document archive."
msgstr "The position of this document in your physical document archive."
#: documents/models.py:227
#: documents/models.py:237
msgid "document"
msgstr "document"
#: documents/models.py:228
#: documents/models.py:238
msgid "documents"
msgstr "documents"
#: documents/models.py:313
#: documents/models.py:325
msgid "debug"
msgstr "debug"
#: documents/models.py:314
#: documents/models.py:326
msgid "information"
msgstr "information"
#: documents/models.py:315
#: documents/models.py:327
msgid "warning"
msgstr "warning"
#: documents/models.py:316
#: documents/models.py:328
msgid "error"
msgstr "error"
#: documents/models.py:317
#: documents/models.py:329
msgid "critical"
msgstr "critical"
#: documents/models.py:321
#: documents/models.py:333
msgid "group"
msgstr "group"
#: documents/models.py:324
#: documents/models.py:336
msgid "message"
msgstr "message"
#: documents/models.py:327
#: documents/models.py:339
msgid "level"
msgstr "level"
#: documents/models.py:334
#: documents/models.py:346
msgid "log"
msgstr "log"
#: documents/models.py:335
#: documents/models.py:347
msgid "logs"
msgstr "logs"
#: documents/models.py:346 documents/models.py:396
#: documents/models.py:358 documents/models.py:408
msgid "saved view"
msgstr "saved view"
#: documents/models.py:347
#: documents/models.py:359
msgid "saved views"
msgstr "saved views"
#: documents/models.py:350
#: documents/models.py:362
msgid "user"
msgstr "user"
#: documents/models.py:356
#: documents/models.py:368
msgid "show on dashboard"
msgstr "show on dashboard"
#: documents/models.py:359
#: documents/models.py:371
msgid "show in sidebar"
msgstr "show in sidebar"
#: documents/models.py:363
#: documents/models.py:375
msgid "sort field"
msgstr "sort field"
#: documents/models.py:366
#: documents/models.py:378
msgid "sort reverse"
msgstr "sort reverse"
#: documents/models.py:372
#: documents/models.py:384
msgid "title contains"
msgstr "title contains"
#: documents/models.py:373
#: documents/models.py:385
msgid "content contains"
msgstr "content contains"
#: documents/models.py:374
#: documents/models.py:386
msgid "ASN is"
msgstr "ASN is"
#: documents/models.py:375
#: documents/models.py:387
msgid "correspondent is"
msgstr "correspondent is"
#: documents/models.py:376
#: documents/models.py:388
msgid "document type is"
msgstr "document type is"
#: documents/models.py:377
#: documents/models.py:389
msgid "is in inbox"
msgstr "is in inbox"
#: documents/models.py:378
#: documents/models.py:390
msgid "has tag"
msgstr "has tag"
#: documents/models.py:379
#: documents/models.py:391
msgid "has any tag"
msgstr "has any tag"
#: documents/models.py:380
#: documents/models.py:392
msgid "created before"
msgstr "created before"
#: documents/models.py:381
#: documents/models.py:393
msgid "created after"
msgstr "created after"
#: documents/models.py:382
#: documents/models.py:394
msgid "created year is"
msgstr "created year is"
#: documents/models.py:383
#: documents/models.py:395
msgid "created month is"
msgstr "created month is"
#: documents/models.py:384
#: documents/models.py:396
msgid "created day is"
msgstr "created day is"
#: documents/models.py:385
#: documents/models.py:397
msgid "added before"
msgstr "added before"
#: documents/models.py:386
#: documents/models.py:398
msgid "added after"
msgstr "added after"
#: documents/models.py:387
#: documents/models.py:399
msgid "modified before"
msgstr "modified before"
#: documents/models.py:388
#: documents/models.py:400
msgid "modified after"
msgstr "modified after"
#: documents/models.py:389
#: documents/models.py:401
msgid "does not have tag"
msgstr "does not have tag"
#: documents/models.py:400
#: documents/models.py:412
msgid "rule type"
msgstr "rule type"
#: documents/models.py:404
#: documents/models.py:416
msgid "value"
msgstr "value"
#: documents/models.py:410
#: documents/models.py:422
msgid "filter rule"
msgstr "filter rule"
#: documents/models.py:411
#: documents/models.py:423
msgid "filter rules"
msgstr "filter rules"
#: documents/serialisers.py:383
#: documents/serialisers.py:370
#, python-format
msgid "File type %(type)s not supported"
msgstr "File type %(type)s not supported"
@@ -391,19 +400,23 @@ msgstr "Password"
msgid "Sign in"
msgstr "Sign in"
#: paperless/settings.py:286
msgid "English"
msgstr "English"
#: paperless/settings.py:291
msgid "English (US)"
msgstr "English (US)"
#: paperless/settings.py:287
#: paperless/settings.py:292
msgid "English (GB)"
msgstr "English (GB)"
#: paperless/settings.py:293
msgid "German"
msgstr "German"
#: paperless/settings.py:288
#: paperless/settings.py:294
msgid "Dutch"
msgstr "Dutch"
#: paperless/settings.py:289
#: paperless/settings.py:295
msgid "French"
msgstr "French"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-28 22:02+0100\n"
"POT-Creation-Date: 2021-02-16 14:52+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -21,144 +21,144 @@ msgstr ""
msgid "Documents"
msgstr ""
#: documents/models.py:33
#: documents/models.py:32
msgid "Any word"
msgstr ""
#: documents/models.py:34
#: documents/models.py:33
msgid "All words"
msgstr ""
#: documents/models.py:35
#: documents/models.py:34
msgid "Exact match"
msgstr ""
#: documents/models.py:36
#: documents/models.py:35
msgid "Regular expression"
msgstr ""
#: documents/models.py:37
#: documents/models.py:36
msgid "Fuzzy word"
msgstr ""
#: documents/models.py:38
#: documents/models.py:37
msgid "Automatic"
msgstr ""
#: documents/models.py:42 documents/models.py:352 paperless_mail/models.py:25
#: documents/models.py:41 documents/models.py:364 paperless_mail/models.py:25
#: paperless_mail/models.py:109
msgid "name"
msgstr ""
#: documents/models.py:46
#: documents/models.py:45
msgid "match"
msgstr ""
#: documents/models.py:50
#: documents/models.py:49
msgid "matching algorithm"
msgstr ""
#: documents/models.py:56
#: documents/models.py:55
msgid "is insensitive"
msgstr ""
#: documents/models.py:75 documents/models.py:135
#: documents/models.py:74 documents/models.py:134
msgid "correspondent"
msgstr ""
#: documents/models.py:76
#: documents/models.py:75
msgid "correspondents"
msgstr ""
#: documents/models.py:98
#: documents/models.py:97
msgid "color"
msgstr ""
#: documents/models.py:102
#: documents/models.py:101
msgid "is inbox tag"
msgstr ""
#: documents/models.py:104
#: documents/models.py:103
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
msgstr ""
#: documents/models.py:109
#: documents/models.py:108
msgid "tag"
msgstr ""
#: documents/models.py:110 documents/models.py:166
#: documents/models.py:109 documents/models.py:165
msgid "tags"
msgstr ""
#: documents/models.py:116 documents/models.py:148
#: documents/models.py:115 documents/models.py:147
msgid "document type"
msgstr ""
#: documents/models.py:117
#: documents/models.py:116
msgid "document types"
msgstr ""
#: documents/models.py:125
#: documents/models.py:124
msgid "Unencrypted"
msgstr ""
#: documents/models.py:126
#: documents/models.py:125
msgid "Encrypted with GNU Privacy Guard"
msgstr ""
#: documents/models.py:139
#: documents/models.py:138
msgid "title"
msgstr ""
#: documents/models.py:152
#: documents/models.py:151
msgid "content"
msgstr ""
#: documents/models.py:154
#: documents/models.py:153
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
msgstr ""
#: documents/models.py:159
#: documents/models.py:158
msgid "mime type"
msgstr ""
#: documents/models.py:170
#: documents/models.py:169
msgid "checksum"
msgstr ""
#: documents/models.py:174
#: documents/models.py:173
msgid "The checksum of the original document."
msgstr ""
#: documents/models.py:178
#: documents/models.py:177
msgid "archive checksum"
msgstr ""
#: documents/models.py:183
#: documents/models.py:182
msgid "The checksum of the archived document."
msgstr ""
#: documents/models.py:187 documents/models.py:330
#: documents/models.py:186 documents/models.py:342
msgid "created"
msgstr ""
#: documents/models.py:191
#: documents/models.py:190
msgid "modified"
msgstr ""
#: documents/models.py:195
#: documents/models.py:194
msgid "storage type"
msgstr ""
#: documents/models.py:203
#: documents/models.py:202
msgid "added"
msgstr ""
#: documents/models.py:207
#: documents/models.py:206
msgid "filename"
msgstr ""
@@ -167,178 +167,186 @@ msgid "Current filename in storage"
msgstr ""
#: documents/models.py:216
msgid "archive filename"
msgstr ""
#: documents/models.py:222
msgid "Current archive filename in storage"
msgstr ""
#: documents/models.py:226
msgid "archive serial number"
msgstr ""
#: documents/models.py:221
#: documents/models.py:231
msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:227
#: documents/models.py:237
msgid "document"
msgstr ""
#: documents/models.py:228
#: documents/models.py:238
msgid "documents"
msgstr ""
#: documents/models.py:313
#: documents/models.py:325
msgid "debug"
msgstr ""
#: documents/models.py:314
#: documents/models.py:326
msgid "information"
msgstr ""
#: documents/models.py:315
#: documents/models.py:327
msgid "warning"
msgstr ""
#: documents/models.py:316
#: documents/models.py:328
msgid "error"
msgstr ""
#: documents/models.py:317
#: documents/models.py:329
msgid "critical"
msgstr ""
#: documents/models.py:321
#: documents/models.py:333
msgid "group"
msgstr ""
#: documents/models.py:324
#: documents/models.py:336
msgid "message"
msgstr ""
#: documents/models.py:327
#: documents/models.py:339
msgid "level"
msgstr ""
#: documents/models.py:334
#: documents/models.py:346
msgid "log"
msgstr ""
#: documents/models.py:335
#: documents/models.py:347
msgid "logs"
msgstr ""
#: documents/models.py:346 documents/models.py:396
#: documents/models.py:358 documents/models.py:408
msgid "saved view"
msgstr ""
#: documents/models.py:347
#: documents/models.py:359
msgid "saved views"
msgstr ""
#: documents/models.py:350
#: documents/models.py:362
msgid "user"
msgstr ""
#: documents/models.py:356
#: documents/models.py:368
msgid "show on dashboard"
msgstr ""
#: documents/models.py:359
#: documents/models.py:371
msgid "show in sidebar"
msgstr ""
#: documents/models.py:363
#: documents/models.py:375
msgid "sort field"
msgstr ""
#: documents/models.py:366
#: documents/models.py:378
msgid "sort reverse"
msgstr ""
#: documents/models.py:372
#: documents/models.py:384
msgid "title contains"
msgstr ""
#: documents/models.py:373
#: documents/models.py:385
msgid "content contains"
msgstr ""
#: documents/models.py:374
#: documents/models.py:386
msgid "ASN is"
msgstr ""
#: documents/models.py:375
#: documents/models.py:387
msgid "correspondent is"
msgstr ""
#: documents/models.py:376
#: documents/models.py:388
msgid "document type is"
msgstr ""
#: documents/models.py:377
#: documents/models.py:389
msgid "is in inbox"
msgstr ""
#: documents/models.py:378
#: documents/models.py:390
msgid "has tag"
msgstr ""
#: documents/models.py:379
#: documents/models.py:391
msgid "has any tag"
msgstr ""
#: documents/models.py:380
#: documents/models.py:392
msgid "created before"
msgstr ""
#: documents/models.py:381
#: documents/models.py:393
msgid "created after"
msgstr ""
#: documents/models.py:382
#: documents/models.py:394
msgid "created year is"
msgstr ""
#: documents/models.py:383
#: documents/models.py:395
msgid "created month is"
msgstr ""
#: documents/models.py:384
#: documents/models.py:396
msgid "created day is"
msgstr ""
#: documents/models.py:385
#: documents/models.py:397
msgid "added before"
msgstr ""
#: documents/models.py:386
#: documents/models.py:398
msgid "added after"
msgstr ""
#: documents/models.py:387
#: documents/models.py:399
msgid "modified before"
msgstr ""
#: documents/models.py:388
#: documents/models.py:400
msgid "modified after"
msgstr ""
#: documents/models.py:389
#: documents/models.py:401
msgid "does not have tag"
msgstr ""
#: documents/models.py:400
#: documents/models.py:412
msgid "rule type"
msgstr ""
#: documents/models.py:404
#: documents/models.py:416
msgid "value"
msgstr ""
#: documents/models.py:410
#: documents/models.py:422
msgid "filter rule"
msgstr ""
#: documents/models.py:411
#: documents/models.py:423
msgid "filter rules"
msgstr ""
#: documents/serialisers.py:383
#: documents/serialisers.py:370
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
@@ -383,19 +391,23 @@ msgstr ""
msgid "Sign in"
msgstr ""
#: paperless/settings.py:286
msgid "English"
#: paperless/settings.py:291
msgid "English (US)"
msgstr ""
#: paperless/settings.py:287
#: paperless/settings.py:292
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:293
msgid "German"
msgstr ""
#: paperless/settings.py:288
#: paperless/settings.py:294
msgid "Dutch"
msgstr ""
#: paperless/settings.py:289
#: paperless/settings.py:295
msgid "French"
msgstr ""

View File

@@ -4,7 +4,7 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jonas Winkler, 2020
# Jonas Winkler, 2021
# Philmo67, 2021
#
#, fuzzy
@@ -12,8 +12,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-28 22:02+0100\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"POT-Creation-Date: 2021-02-16 14:52+0100\n"
"PO-Revision-Date: 2021-02-16 18:37+0000\n"
"Last-Translator: Philmo67, 2021\n"
"Language-Team: French (https://www.transifex.com/paperless/teams/115905/fr/)\n"
"MIME-Version: 1.0\n"
@@ -26,64 +26,64 @@ msgstr ""
msgid "Documents"
msgstr "Documents"
#: documents/models.py:33
#: documents/models.py:32
msgid "Any word"
msgstr "Un des mots"
#: documents/models.py:34
#: documents/models.py:33
msgid "All words"
msgstr "Tous les mots"
#: documents/models.py:35
#: documents/models.py:34
msgid "Exact match"
msgstr "Concordance exacte"
#: documents/models.py:36
#: documents/models.py:35
msgid "Regular expression"
msgstr "Expression régulière"
#: documents/models.py:37
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Mot approximatif"
#: documents/models.py:38
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatique"
#: documents/models.py:42 documents/models.py:352 paperless_mail/models.py:25
#: documents/models.py:41 documents/models.py:364 paperless_mail/models.py:25
#: paperless_mail/models.py:109
msgid "name"
msgstr "nom"
#: documents/models.py:46
#: documents/models.py:45
msgid "match"
msgstr "rapprochement"
#: documents/models.py:50
#: documents/models.py:49
msgid "matching algorithm"
msgstr "algorithme de rapprochement"
#: documents/models.py:56
#: documents/models.py:55
msgid "is insensitive"
msgstr "est insensible à la casse"
#: documents/models.py:75 documents/models.py:135
#: documents/models.py:74 documents/models.py:134
msgid "correspondent"
msgstr "correspondant"
#: documents/models.py:76
#: documents/models.py:75
msgid "correspondents"
msgstr "correspondants"
#: documents/models.py:98
#: documents/models.py:97
msgid "color"
msgstr "couleur"
#: documents/models.py:102
#: documents/models.py:101
msgid "is inbox tag"
msgstr "est une étiquette de boîte de réception"
#: documents/models.py:104
#: documents/models.py:103
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
@@ -91,39 +91,39 @@ msgstr ""
"Marque cette étiquette comme étiquette de boîte de réception : ces "
"étiquettes sont affectées à tous les documents nouvellement traités."
#: documents/models.py:109
#: documents/models.py:108
msgid "tag"
msgstr "étiquette"
#: documents/models.py:110 documents/models.py:166
#: documents/models.py:109 documents/models.py:165
msgid "tags"
msgstr "étiquettes"
#: documents/models.py:116 documents/models.py:148
#: documents/models.py:115 documents/models.py:147
msgid "document type"
msgstr "type de document"
#: documents/models.py:117
#: documents/models.py:116
msgid "document types"
msgstr "types de document"
#: documents/models.py:125
#: documents/models.py:124
msgid "Unencrypted"
msgstr "Non chiffré"
#: documents/models.py:126
#: documents/models.py:125
msgid "Encrypted with GNU Privacy Guard"
msgstr "Chiffré avec GNU Privacy Guard"
#: documents/models.py:139
#: documents/models.py:138
msgid "title"
msgstr "titre"
#: documents/models.py:152
#: documents/models.py:151
msgid "content"
msgstr "contenu"
#: documents/models.py:154
#: documents/models.py:153
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
@@ -131,43 +131,43 @@ msgstr ""
"Les données brutes du document, en format texte uniquement. Ce champ est "
"principalement utilisé pour la recherche."
#: documents/models.py:159
#: documents/models.py:158
msgid "mime type"
msgstr "type mime"
#: documents/models.py:170
#: documents/models.py:169
msgid "checksum"
msgstr "somme de contrôle"
#: documents/models.py:174
#: documents/models.py:173
msgid "The checksum of the original document."
msgstr "La somme de contrôle du document original."
#: documents/models.py:178
#: documents/models.py:177
msgid "archive checksum"
msgstr "somme de contrôle de l'archive"
#: documents/models.py:183
#: documents/models.py:182
msgid "The checksum of the archived document."
msgstr "La somme de contrôle du document archivé."
#: documents/models.py:187 documents/models.py:330
#: documents/models.py:186 documents/models.py:342
msgid "created"
msgstr "créé le"
#: documents/models.py:191
#: documents/models.py:190
msgid "modified"
msgstr "modifié"
#: documents/models.py:195
#: documents/models.py:194
msgid "storage type"
msgstr "forme d'enregistrement :"
#: documents/models.py:203
#: documents/models.py:202
msgid "added"
msgstr "date d'ajout"
#: documents/models.py:207
#: documents/models.py:206
msgid "filename"
msgstr "nom du fichier"
@@ -176,179 +176,187 @@ msgid "Current filename in storage"
msgstr "Nom du fichier courant en base de données"
#: documents/models.py:216
msgid "archive filename"
msgstr "nom de fichier de l'archive"
#: documents/models.py:222
msgid "Current archive filename in storage"
msgstr "Nom du fichier d'archive courant en base de données"
#: documents/models.py:226
msgid "archive serial number"
msgstr "numéro de série de l'archive"
#: documents/models.py:221
#: documents/models.py:231
msgid "The position of this document in your physical document archive."
msgstr ""
"Le classement de ce document dans votre archive de documents physiques."
#: documents/models.py:227
#: documents/models.py:237
msgid "document"
msgstr "document"
#: documents/models.py:228
#: documents/models.py:238
msgid "documents"
msgstr "documents"
#: documents/models.py:313
#: documents/models.py:325
msgid "debug"
msgstr "débogage"
#: documents/models.py:314
#: documents/models.py:326
msgid "information"
msgstr "information"
#: documents/models.py:315
#: documents/models.py:327
msgid "warning"
msgstr "avertissement"
#: documents/models.py:316
#: documents/models.py:328
msgid "error"
msgstr "erreur"
#: documents/models.py:317
#: documents/models.py:329
msgid "critical"
msgstr "critique"
#: documents/models.py:321
#: documents/models.py:333
msgid "group"
msgstr "groupe"
#: documents/models.py:324
#: documents/models.py:336
msgid "message"
msgstr "message"
#: documents/models.py:327
#: documents/models.py:339
msgid "level"
msgstr "niveau"
#: documents/models.py:334
#: documents/models.py:346
msgid "log"
msgstr "rapport"
#: documents/models.py:335
#: documents/models.py:347
msgid "logs"
msgstr "rapports"
#: documents/models.py:346 documents/models.py:396
#: documents/models.py:358 documents/models.py:408
msgid "saved view"
msgstr "vue enregistrée"
#: documents/models.py:347
#: documents/models.py:359
msgid "saved views"
msgstr "vues enregistrées"
#: documents/models.py:350
#: documents/models.py:362
msgid "user"
msgstr "utilisateur"
#: documents/models.py:356
#: documents/models.py:368
msgid "show on dashboard"
msgstr "montrer sur le tableau de bord"
#: documents/models.py:359
#: documents/models.py:371
msgid "show in sidebar"
msgstr "montrer dans la barre latérale"
#: documents/models.py:363
#: documents/models.py:375
msgid "sort field"
msgstr "champ de tri"
#: documents/models.py:366
#: documents/models.py:378
msgid "sort reverse"
msgstr "tri inverse"
#: documents/models.py:372
#: documents/models.py:384
msgid "title contains"
msgstr "le titre contient"
#: documents/models.py:373
#: documents/models.py:385
msgid "content contains"
msgstr "le contenu contient"
#: documents/models.py:374
#: documents/models.py:386
msgid "ASN is"
msgstr "le NSA est"
#: documents/models.py:375
#: documents/models.py:387
msgid "correspondent is"
msgstr "le correspondant est"
#: documents/models.py:376
#: documents/models.py:388
msgid "document type is"
msgstr "le type de document est"
#: documents/models.py:377
#: documents/models.py:389
msgid "is in inbox"
msgstr "est dans la boîte de réception"
#: documents/models.py:378
#: documents/models.py:390
msgid "has tag"
msgstr "porte l'étiquette"
#: documents/models.py:379
#: documents/models.py:391
msgid "has any tag"
msgstr "porte l'une des étiquettes"
#: documents/models.py:380
#: documents/models.py:392
msgid "created before"
msgstr "créé avant"
#: documents/models.py:381
#: documents/models.py:393
msgid "created after"
msgstr "créé après"
#: documents/models.py:382
#: documents/models.py:394
msgid "created year is"
msgstr "l'année de création est"
#: documents/models.py:383
#: documents/models.py:395
msgid "created month is"
msgstr "le mois de création est"
#: documents/models.py:384
#: documents/models.py:396
msgid "created day is"
msgstr "le jour de création est"
#: documents/models.py:385
#: documents/models.py:397
msgid "added before"
msgstr "ajouté avant"
#: documents/models.py:386
#: documents/models.py:398
msgid "added after"
msgstr "ajouté après"
#: documents/models.py:387
#: documents/models.py:399
msgid "modified before"
msgstr "modifié avant"
#: documents/models.py:388
#: documents/models.py:400
msgid "modified after"
msgstr "modifié après"
#: documents/models.py:389
#: documents/models.py:401
msgid "does not have tag"
msgstr "ne porte pas d'étiquette"
#: documents/models.py:400
#: documents/models.py:412
msgid "rule type"
msgstr "type de règle"
#: documents/models.py:404
#: documents/models.py:416
msgid "value"
msgstr "valeur"
#: documents/models.py:410
#: documents/models.py:422
msgid "filter rule"
msgstr "règle de filtrage"
#: documents/models.py:411
#: documents/models.py:423
msgid "filter rules"
msgstr "règles de filtrage"
#: documents/serialisers.py:383
#: documents/serialisers.py:370
#, python-format
msgid "File type %(type)s not supported"
msgstr "Type de fichier %(type)s non pris en charge"
@@ -395,19 +403,23 @@ msgstr "Mot de passe"
msgid "Sign in"
msgstr "S'identifier"
#: paperless/settings.py:286
msgid "English"
msgstr "Anglais"
#: paperless/settings.py:291
msgid "English (US)"
msgstr "Anglais (US)"
#: paperless/settings.py:287
#: paperless/settings.py:292
msgid "English (GB)"
msgstr "Anglais (GB)"
#: paperless/settings.py:293
msgid "German"
msgstr "Allemand"
#: paperless/settings.py:288
#: paperless/settings.py:294
msgid "Dutch"
msgstr "Néerlandais"
#: paperless/settings.py:289
#: paperless/settings.py:295
msgid "French"
msgstr "Français"

View File

@@ -4,17 +4,17 @@
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jonas Winkler, 2021
# Jo Vandeginste <jo.vandeginste@gmail.com>, 2021
# Ben <bzweekhorst@gmail.com>, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-28 22:02+0100\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"Last-Translator: Ben <bzweekhorst@gmail.com>, 2021\n"
"POT-Creation-Date: 2021-02-16 14:52+0100\n"
"PO-Revision-Date: 2021-02-16 18:37+0000\n"
"Last-Translator: Jo Vandeginste <jo.vandeginste@gmail.com>, 2021\n"
"Language-Team: Dutch (Netherlands) (https://www.transifex.com/paperless/teams/115905/nl_NL/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -26,64 +26,64 @@ msgstr ""
msgid "Documents"
msgstr "Documenten"
#: documents/models.py:33
#: documents/models.py:32
msgid "Any word"
msgstr "Eender welk woord"
#: documents/models.py:34
#: documents/models.py:33
msgid "All words"
msgstr "Alle woorden"
#: documents/models.py:35
#: documents/models.py:34
msgid "Exact match"
msgstr "Exacte overeenkomst"
#: documents/models.py:36
#: documents/models.py:35
msgid "Regular expression"
msgstr "Reguliere expressie"
#: documents/models.py:37
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Gelijkaardig woord"
#: documents/models.py:38
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatisch"
#: documents/models.py:42 documents/models.py:352 paperless_mail/models.py:25
#: documents/models.py:41 documents/models.py:364 paperless_mail/models.py:25
#: paperless_mail/models.py:109
msgid "name"
msgstr "naam"
#: documents/models.py:46
#: documents/models.py:45
msgid "match"
msgstr "Overeenkomst"
#: documents/models.py:50
#: documents/models.py:49
msgid "matching algorithm"
msgstr "Algoritme voor het bepalen van de overeenkomst"
#: documents/models.py:56
#: documents/models.py:55
msgid "is insensitive"
msgstr "is niet hoofdlettergevoelig"
#: documents/models.py:75 documents/models.py:135
#: documents/models.py:74 documents/models.py:134
msgid "correspondent"
msgstr "correspondent"
#: documents/models.py:76
#: documents/models.py:75
msgid "correspondents"
msgstr "correspondenten"
#: documents/models.py:98
#: documents/models.py:97
msgid "color"
msgstr "Kleur"
#: documents/models.py:102
#: documents/models.py:101
msgid "is inbox tag"
msgstr "is \"Postvak in\"-etiket"
#: documents/models.py:104
#: documents/models.py:103
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
@@ -91,39 +91,39 @@ msgstr ""
"Markeer dit etiket als een \"Postvak in\"-etiket: alle nieuw verwerkte "
"documenten krijgen de \"Postvak in\"-etiketten."
#: documents/models.py:109
#: documents/models.py:108
msgid "tag"
msgstr "etiket"
#: documents/models.py:110 documents/models.py:166
#: documents/models.py:109 documents/models.py:165
msgid "tags"
msgstr "etiketten"
#: documents/models.py:116 documents/models.py:148
#: documents/models.py:115 documents/models.py:147
msgid "document type"
msgstr "documenttype"
#: documents/models.py:117
#: documents/models.py:116
msgid "document types"
msgstr "documenttypen"
#: documents/models.py:125
#: documents/models.py:124
msgid "Unencrypted"
msgstr "Niet versleuteld"
#: documents/models.py:126
#: documents/models.py:125
msgid "Encrypted with GNU Privacy Guard"
msgstr "Versleuteld met GNU Privacy Guard"
#: documents/models.py:139
#: documents/models.py:138
msgid "title"
msgstr "titel"
#: documents/models.py:152
#: documents/models.py:151
msgid "content"
msgstr "inhoud"
#: documents/models.py:154
#: documents/models.py:153
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
@@ -131,43 +131,43 @@ msgstr ""
"De onbewerkte gegevens van het document. Dit veld wordt voornamelijk "
"gebruikt om te zoeken."
#: documents/models.py:159
#: documents/models.py:158
msgid "mime type"
msgstr "mimetype"
#: documents/models.py:170
#: documents/models.py:169
msgid "checksum"
msgstr "checksum"
#: documents/models.py:174
#: documents/models.py:173
msgid "The checksum of the original document."
msgstr "Het controlecijfer van het originele document."
#: documents/models.py:178
#: documents/models.py:177
msgid "archive checksum"
msgstr "archief checksum"
#: documents/models.py:183
#: documents/models.py:182
msgid "The checksum of the archived document."
msgstr "De checksum van het gearchiveerde document."
#: documents/models.py:187 documents/models.py:330
#: documents/models.py:186 documents/models.py:342
msgid "created"
msgstr "aangemaakt"
#: documents/models.py:191
#: documents/models.py:190
msgid "modified"
msgstr "gewijzigd"
#: documents/models.py:195
#: documents/models.py:194
msgid "storage type"
msgstr "type opslag"
#: documents/models.py:203
#: documents/models.py:202
msgid "added"
msgstr "toegevoegd"
#: documents/models.py:207
#: documents/models.py:206
msgid "filename"
msgstr "bestandsnaam"
@@ -176,178 +176,186 @@ msgid "Current filename in storage"
msgstr "Huidige bestandsnaam in opslag"
#: documents/models.py:216
msgid "archive filename"
msgstr "Bestandsnaam in archief"
#: documents/models.py:222
msgid "Current archive filename in storage"
msgstr "Huidige bestandsnaam in archief"
#: documents/models.py:226
msgid "archive serial number"
msgstr "serienummer in archief"
#: documents/models.py:221
#: documents/models.py:231
msgid "The position of this document in your physical document archive."
msgstr "De positie van dit document in je fysieke documentenarchief."
#: documents/models.py:227
#: documents/models.py:237
msgid "document"
msgstr "document"
#: documents/models.py:228
#: documents/models.py:238
msgid "documents"
msgstr "documenten"
#: documents/models.py:313
#: documents/models.py:325
msgid "debug"
msgstr "debug"
#: documents/models.py:314
#: documents/models.py:326
msgid "information"
msgstr "informatie"
#: documents/models.py:315
#: documents/models.py:327
msgid "warning"
msgstr "waarschuwing"
#: documents/models.py:316
#: documents/models.py:328
msgid "error"
msgstr "fout"
#: documents/models.py:317
#: documents/models.py:329
msgid "critical"
msgstr "kritisch"
#: documents/models.py:321
#: documents/models.py:333
msgid "group"
msgstr "groep"
#: documents/models.py:324
#: documents/models.py:336
msgid "message"
msgstr "bericht"
#: documents/models.py:327
#: documents/models.py:339
msgid "level"
msgstr "niveau"
#: documents/models.py:334
#: documents/models.py:346
msgid "log"
msgstr "bericht"
#: documents/models.py:335
#: documents/models.py:347
msgid "logs"
msgstr "berichten"
#: documents/models.py:346 documents/models.py:396
#: documents/models.py:358 documents/models.py:408
msgid "saved view"
msgstr "opgeslagen view"
#: documents/models.py:347
#: documents/models.py:359
msgid "saved views"
msgstr "opgeslagen views"
#: documents/models.py:350
#: documents/models.py:362
msgid "user"
msgstr "gebruiker"
#: documents/models.py:356
#: documents/models.py:368
msgid "show on dashboard"
msgstr "weergeven op dashboard"
#: documents/models.py:359
#: documents/models.py:371
msgid "show in sidebar"
msgstr "weergeven in zijbalk"
#: documents/models.py:363
#: documents/models.py:375
msgid "sort field"
msgstr "sorteerveld"
#: documents/models.py:366
#: documents/models.py:378
msgid "sort reverse"
msgstr "omgekeerd sorteren"
#: documents/models.py:372
#: documents/models.py:384
msgid "title contains"
msgstr "titel bevat"
#: documents/models.py:373
#: documents/models.py:385
msgid "content contains"
msgstr "inhoud bevat"
#: documents/models.py:374
#: documents/models.py:386
msgid "ASN is"
msgstr "ASN is"
#: documents/models.py:375
#: documents/models.py:387
msgid "correspondent is"
msgstr "correspondent is"
#: documents/models.py:376
#: documents/models.py:388
msgid "document type is"
msgstr "documenttype is"
#: documents/models.py:377
#: documents/models.py:389
msgid "is in inbox"
msgstr "zit in \"Postvak in\""
#: documents/models.py:378
#: documents/models.py:390
msgid "has tag"
msgstr "heeft etiket"
#: documents/models.py:379
#: documents/models.py:391
msgid "has any tag"
msgstr "heeft één van de etiketten"
#: documents/models.py:380
#: documents/models.py:392
msgid "created before"
msgstr "aangemaakt voor"
#: documents/models.py:381
#: documents/models.py:393
msgid "created after"
msgstr "aangemaakt na"
#: documents/models.py:382
#: documents/models.py:394
msgid "created year is"
msgstr "aangemaakt jaar is"
#: documents/models.py:383
#: documents/models.py:395
msgid "created month is"
msgstr "aangemaakte maand is"
#: documents/models.py:384
#: documents/models.py:396
msgid "created day is"
msgstr "aangemaakte dag is"
#: documents/models.py:385
#: documents/models.py:397
msgid "added before"
msgstr "toegevoegd voor"
#: documents/models.py:386
#: documents/models.py:398
msgid "added after"
msgstr "toegevoegd na"
#: documents/models.py:387
#: documents/models.py:399
msgid "modified before"
msgstr "gewijzigd voor"
#: documents/models.py:388
#: documents/models.py:400
msgid "modified after"
msgstr "gewijzigd na"
#: documents/models.py:389
#: documents/models.py:401
msgid "does not have tag"
msgstr "heeft geen etiket"
#: documents/models.py:400
#: documents/models.py:412
msgid "rule type"
msgstr "type regel"
#: documents/models.py:404
#: documents/models.py:416
msgid "value"
msgstr "waarde"
#: documents/models.py:410
#: documents/models.py:422
msgid "filter rule"
msgstr "filterregel"
#: documents/models.py:411
#: documents/models.py:423
msgid "filter rules"
msgstr "filterregels"
#: documents/serialisers.py:383
#: documents/serialisers.py:370
#, python-format
msgid "File type %(type)s not supported"
msgstr "Bestandstype %(type)s niet ondersteund"
@@ -392,19 +400,23 @@ msgstr "Wachtwoord"
msgid "Sign in"
msgstr "Aanmelden"
#: paperless/settings.py:286
msgid "English"
msgstr "Engels"
#: paperless/settings.py:291
msgid "English (US)"
msgstr "Engels (US)"
#: paperless/settings.py:287
#: paperless/settings.py:292
msgid "English (GB)"
msgstr "Engels (Brits)"
#: paperless/settings.py:293
msgid "German"
msgstr "Duits"
#: paperless/settings.py:288
#: paperless/settings.py:294
msgid "Dutch"
msgstr "Nederlands"
#: paperless/settings.py:289
#: paperless/settings.py:295
msgid "French"
msgstr "Frans"

View File

@@ -102,10 +102,11 @@ INSTALLED_APPS = [
"django_q",
"channels",
] + env_apps
if DEBUG:
INSTALLED_APPS.append("channels")
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
@@ -169,16 +170,6 @@ CHANNEL_LAYERS = {
},
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": os.getenv("PAPERLESS_REDIS", "redis://localhost:6379"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
###############################################################################
# Security #
###############################################################################
@@ -297,7 +288,8 @@ if os.getenv("PAPERLESS_DBHOST"):
LANGUAGE_CODE = 'en-us'
LANGUAGES = [
("en-us", _("English")),
("en-us", _("English (US)")),
("en-gb", _("English (GB)")),
("de", _("German")),
("nl-nl", _("Dutch")),
("fr", _("French"))
@@ -407,8 +399,9 @@ TASK_WORKERS = int(os.getenv("PAPERLESS_TASK_WORKERS", default_task_workers()))
Q_CLUSTER = {
'name': 'paperless',
'catch_up': False,
'recycle': 1,
'workers': TASK_WORKERS,
'django_redis': 'default'
'redis': os.getenv("PAPERLESS_REDIS", "redis://localhost:6379")
}
@@ -432,6 +425,12 @@ THREADS_PER_WORKER = os.getenv("PAPERLESS_THREADS_PER_WORKER", default_threads_p
CONSUMER_POLLING = int(os.getenv("PAPERLESS_CONSUMER_POLLING", 0))
CONSUMER_POLLING_DELAY = int(os.getenv("PAPERLESS_CONSUMER_POLLING_DELAY", 5))
CONSUMER_POLLING_RETRY_COUNT = int(
os.getenv("PAPERLESS_CONSUMER_POLLING_RETRY_COUNT", 5)
)
CONSUMER_DELETE_DUPLICATES = __get_boolean("PAPERLESS_CONSUMER_DELETE_DUPLICATES")
CONSUMER_RECURSIVE = __get_boolean("PAPERLESS_CONSUMER_RECURSIVE")
@@ -456,6 +455,14 @@ OCR_MODE = os.getenv("PAPERLESS_OCR_MODE", "skip")
OCR_IMAGE_DPI = os.getenv("PAPERLESS_OCR_IMAGE_DPI")
OCR_CLEAN = os.getenv("PAPERLESS_OCR_CLEAN", "clean")
OCR_DESKEW = __get_boolean("PAPERLESS_OCR_DESKEW", "true")
OCR_ROTATE_PAGES = __get_boolean("PAPERLESS_OCR_ROTATE_PAGES", "true")
OCR_ROTATE_PAGES_THRESHOLD = float(os.getenv("PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD", 12.0))
OCR_USER_ARGS = os.getenv("PAPERLESS_OCR_USER_ARGS", "{}")
# GNUPG needs a home directory for some reason

View File

@@ -23,7 +23,8 @@ from documents.views import (
PostDocumentView,
SavedViewViewSet,
BulkEditView,
SelectionDataView
SelectionDataView,
BulkDownloadView
)
from paperless.views import FaviconView
@@ -63,6 +64,9 @@ urlpatterns = [
re_path(r"^documents/selection_data/", SelectionDataView.as_view(),
name="selection_data"),
re_path(r"^documents/bulk_download/", BulkDownloadView.as_view(),
name="bulk_download"),
path('token/', views.obtain_auth_token)
] + api_router.urls)),

View File

@@ -1 +1 @@
__version__ = (1, 1, 1)
__version__ = (1, 2, 0)

View File

@@ -2,17 +2,17 @@ import json
import os
import re
import ocrmypdf
import pdftotext
import pikepdf
from PIL import Image
from django.conf import settings
from ocrmypdf import InputFileError, EncryptedPdfError
from documents.parsers import DocumentParser, ParseError, \
make_thumbnail_from_pdf
class NoTextFoundException(Exception):
pass
class RasterisedDocumentParser(DocumentParser):
"""
This parser uses Tesseract to try and get some text out of a rasterised
@@ -22,10 +22,13 @@ class RasterisedDocumentParser(DocumentParser):
logging_name = "paperless.parsing.tesseract"
def extract_metadata(self, document_path, mime_type):
namespace_pattern = re.compile(r"\{(.*)\}(.*)")
result = []
if mime_type == 'application/pdf':
import pikepdf
namespace_pattern = re.compile(r"\{(.*)\}(.*)")
pdf = pikepdf.open(document_path)
meta = pdf.open_metadata()
for key, value in meta.items():
@@ -50,7 +53,9 @@ class RasterisedDocumentParser(DocumentParser):
def get_thumbnail(self, document_path, mime_type, file_name=None):
return make_thumbnail_from_pdf(
document_path, self.tempdir, self.logging_group)
self.archive_path or document_path,
self.tempdir,
self.logging_group)
def is_image(self, mime_type):
return mime_type in [
@@ -90,122 +95,202 @@ class RasterisedDocumentParser(DocumentParser):
f"Error while calculating DPI for image {image}: {e}")
return None
def extract_text(self, sidecar_file, pdf_file):
if sidecar_file and os.path.isfile(sidecar_file):
with open(sidecar_file, "r") as f:
text = f.read()
if "[OCR skipped on page" not in text:
# This happens when there's already text in the input file.
# The sidecar file will only contain text for OCR'ed pages.
self.log("debug", "Using text from sidecar file")
return text
else:
self.log("debug", "Incomplete sidecar file: discarding.")
# no success with the sidecar file, try PDF
if not os.path.isfile(pdf_file):
return None
from pdfminer.high_level import extract_text
from pdfminer.pdftypes import PDFException
try:
text = extract_text(pdf_file)
stripped = strip_excess_whitespace(text)
self.log("debug", f"Extracted text from PDF file {pdf_file}")
return stripped
except PDFException:
# probably not a PDF file.
return None
def construct_ocrmypdf_parameters(self,
input_file,
mime_type,
output_file,
sidecar_file,
safe_fallback=False):
ocrmypdf_args = {
'input_file': input_file,
'output_file': output_file,
# need to use threads, since this will be run in daemonized
# processes by django-q.
'use_threads': True,
'jobs': settings.THREADS_PER_WORKER,
'language': settings.OCR_LANGUAGE,
'output_type': settings.OCR_OUTPUT_TYPE,
'progress_bar': False
}
if settings.OCR_MODE == 'force' or safe_fallback:
ocrmypdf_args['force_ocr'] = True
elif settings.OCR_MODE in ['skip', 'skip_noarchive']:
ocrmypdf_args['skip_text'] = True
elif settings.OCR_MODE == 'redo':
ocrmypdf_args['redo_ocr'] = True
else:
raise ParseError(
f"Invalid ocr mode: {settings.OCR_MODE}")
if settings.OCR_CLEAN == 'clean':
ocrmypdf_args['clean'] = True
elif settings.OCR_CLEAN == 'clean-final':
if settings.OCR_MODE == 'redo':
ocrmypdf_args['clean'] = True
else:
ocrmypdf_args['clean_final'] = True
if settings.OCR_DESKEW and not settings.OCR_MODE == 'redo':
ocrmypdf_args['deskew'] = True
if settings.OCR_ROTATE_PAGES:
ocrmypdf_args['rotate_pages'] = True
ocrmypdf_args['rotate_pages_threshold'] = settings.OCR_ROTATE_PAGES_THRESHOLD # NOQA: E501
if settings.OCR_PAGES > 0:
ocrmypdf_args['pages'] = f"1-{settings.OCR_PAGES}"
else:
# sidecar is incompatible with pages
ocrmypdf_args['sidecar'] = sidecar_file
if self.is_image(mime_type):
dpi = self.get_dpi(input_file)
a4_dpi = self.calculate_a4_dpi(input_file)
if dpi:
self.log(
"debug",
f"Detected DPI for image {input_file}: {dpi}"
)
ocrmypdf_args['image_dpi'] = dpi
elif settings.OCR_IMAGE_DPI:
ocrmypdf_args['image_dpi'] = settings.OCR_IMAGE_DPI
elif a4_dpi:
ocrmypdf_args['image_dpi'] = a4_dpi
else:
raise ParseError(
f"Cannot produce archive PDF for image {input_file}, "
f"no DPI information is present in this image and "
f"OCR_IMAGE_DPI is not set.")
if settings.OCR_USER_ARGS and not safe_fallback:
try:
user_args = json.loads(settings.OCR_USER_ARGS)
ocrmypdf_args = {**ocrmypdf_args, **user_args}
except Exception as e:
self.log(
"warning",
f"There is an issue with PAPERLESS_OCR_USER_ARGS, so "
f"they will not be used. Error: {e}")
return ocrmypdf_args
def parse(self, document_path, mime_type, file_name=None):
mode = settings.OCR_MODE
# This forces tesseract to use one core per page.
os.environ['OMP_THREAD_LIMIT'] = "1"
text_original = get_text_from_pdf(document_path)
has_text = text_original and len(text_original) > 50
text_original = self.extract_text(None, document_path)
original_has_text = text_original and len(text_original) > 50
if mode == "skip_noarchive" and has_text:
if settings.OCR_MODE == "skip_noarchive" and original_has_text:
self.log("debug",
"Document has text, skipping OCRmyPDF entirely.")
self.text = text_original
return
if mode in ['skip', 'skip_noarchive'] and not has_text:
# upgrade to redo, since there appears to be no text in the
# document. This happens to some weird encrypted documents or
# documents with failed OCR attempts for which OCRmyPDF will
# still report that there actually is text in them.
self.log("debug",
"No text was found in the document and skip is "
"specified. Upgrading OCR mode to redo.")
mode = "redo"
import ocrmypdf
from ocrmypdf import InputFileError, EncryptedPdfError
archive_path = os.path.join(self.tempdir, "archive.pdf")
sidecar_file = os.path.join(self.tempdir, "sidecar.txt")
ocr_args = {
'input_file': document_path,
'output_file': archive_path,
'use_threads': True,
'jobs': settings.THREADS_PER_WORKER,
'language': settings.OCR_LANGUAGE,
'output_type': settings.OCR_OUTPUT_TYPE,
'progress_bar': False,
'clean': True
}
if settings.OCR_PAGES > 0:
ocr_args['pages'] = f"1-{settings.OCR_PAGES}"
# Mode selection.
if mode in ['skip', 'skip_noarchive']:
ocr_args['skip_text'] = True
elif mode == 'redo':
ocr_args['redo_ocr'] = True
elif mode == 'force':
ocr_args['force_ocr'] = True
else:
raise ParseError(
f"Invalid ocr mode: {mode}")
if self.is_image(mime_type):
dpi = self.get_dpi(document_path)
a4_dpi = self.calculate_a4_dpi(document_path)
if dpi:
self.log(
"debug",
f"Detected DPI for image {document_path}: {dpi}"
)
ocr_args['image_dpi'] = dpi
elif settings.OCR_IMAGE_DPI:
ocr_args['image_dpi'] = settings.OCR_IMAGE_DPI
elif a4_dpi:
ocr_args['image_dpi'] = a4_dpi
else:
raise ParseError(
f"Cannot produce archive PDF for image {document_path}, "
f"no DPI information is present in this image and "
f"OCR_IMAGE_DPI is not set.")
if settings.OCR_USER_ARGS:
try:
user_args = json.loads(settings.OCR_USER_ARGS)
ocr_args = {**ocr_args, **user_args}
except Exception as e:
self.log(
"warning",
f"There is an issue with PAPERLESS_OCR_USER_ARGS, so "
f"they will not be used: {e}")
# This forces tesseract to use one core per page.
os.environ['OMP_THREAD_LIMIT'] = "1"
args = self.construct_ocrmypdf_parameters(
document_path, mime_type, archive_path, sidecar_file)
try:
self.log("debug",
f"Calling OCRmyPDF with {str(ocr_args)}")
ocrmypdf.ocr(**ocr_args)
# success! announce results
self.log("debug", f"Calling OCRmyPDF with args: {args}")
ocrmypdf.ocr(**args)
self.archive_path = archive_path
self.text = get_text_from_pdf(archive_path)
self.text = self.extract_text(sidecar_file, archive_path)
except (InputFileError, EncryptedPdfError) as e:
self.log("debug",
f"Encountered an error: {e}. Trying to use text from "
f"original.")
# This happens with some PDFs when used with the redo_ocr option.
# This is not the end of the world, we'll just use what we already
# have in the document.
self.text = text_original
# Also, no archived file.
if not self.text:
# However, if we don't have anything, fail:
raise NoTextFoundException(
"No text was found in the original document")
except EncryptedPdfError:
self.log("warning",
"This file is encrypted, OCR is impossible. Using "
"any text present in the original file.")
if original_has_text:
self.text = text_original
except (NoTextFoundException, InputFileError) as e:
self.log("exception",
f"Encountered the following error while running OCR, "
f"attempting force OCR to get the text.")
archive_path_fallback = os.path.join(
self.tempdir, "archive-fallback.pdf")
sidecar_file_fallback = os.path.join(
self.tempdir, "sidecar-fallback.txt")
# Attempt to run OCR with safe settings.
args = self.construct_ocrmypdf_parameters(
document_path, mime_type,
archive_path_fallback, sidecar_file_fallback,
safe_fallback=True
)
try:
self.log("debug",
f"Fallback: Calling OCRmyPDF with args: {args}")
ocrmypdf.ocr(**args)
# Don't return the archived file here, since this file
# is bigger and blurry due to --force-ocr.
self.text = self.extract_text(
sidecar_file_fallback, archive_path_fallback)
except Exception as e:
# If this fails, we have a serious issue at hand.
raise ParseError(f"{e.__class__.__name__}: {str(e)}")
except Exception as e:
# Anything else is probably serious.
raise ParseError(f"{e.__class__.__name__}: {str(e)}")
# As a last resort, if we still don't have any text for any reason,
# try to extract the text from the original document.
if not self.text:
# This may happen for files that don't have any text.
self.log(
'warning',
f"Document {document_path} does not have any text. "
f"This is probably an error or you tried to add an image "
f"without text, or something is wrong with this document.")
self.text = ""
if original_has_text:
self.text = text_original
else:
self.log(
"warning",
f"No text was found in {document_path}, the content will "
f"be empty."
)
def strip_excess_whitespace(text):
@@ -220,20 +305,3 @@ def strip_excess_whitespace(text):
# TODO: this needs a rework
return no_trailing_whitespace.strip()
def get_text_from_pdf(pdf_file):
if not os.path.isfile(pdf_file):
return None
with open(pdf_file, "rb") as f:
try:
pdf = pdftotext.PDF(f)
except pdftotext.Error:
# might not be a PDF file
return None
text = "\n".join(pdf)
return strip_excess_whitespace(text)

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,7 @@ from django.test import TestCase, override_settings
from documents.parsers import ParseError, run_convert
from documents.tests.utils import DirectoriesMixin
from paperless_tesseract.parsers import RasterisedDocumentParser, get_text_from_pdf, strip_excess_whitespace
from paperless_tesseract.parsers import RasterisedDocumentParser, strip_excess_whitespace
image_to_string_calls = []
@@ -38,7 +38,12 @@ class TestParser(DirectoriesMixin, TestCase):
def assertContainsStrings(self, content, strings):
# Asserts that all strings appear in content, in the given order.
indices = [content.index(s) for s in strings]
indices = []
for s in strings:
if s in content:
indices.append(content.index(s))
else:
self.fail(f"'{s}' is not in '{content}'")
self.assertListEqual(indices, sorted(indices))
text_cases = [
@@ -69,7 +74,8 @@ class TestParser(DirectoriesMixin, TestCase):
SAMPLE_FILES = os.path.join(os.path.dirname(__file__), "samples")
def test_get_text_from_pdf(self):
text = get_text_from_pdf(os.path.join(self.SAMPLE_FILES, 'simple-digital.pdf'))
parser = RasterisedDocumentParser(uuid.uuid4())
text = parser.extract_text(None, os.path.join(self.SAMPLE_FILES, 'simple-digital.pdf'))
self.assertContainsStrings(text.strip(), ["This is a test document."])
@@ -129,15 +135,21 @@ class TestParser(DirectoriesMixin, TestCase):
self.assertIsNone(parser.archive_path)
self.assertContainsStrings(parser.get_text(), ["Please enter your name in here:", "This is a PDF document with a form."])
@override_settings(OCR_MODE="redo")
@mock.patch("paperless_tesseract.parsers.get_text_from_pdf", lambda _: None)
def test_with_form_error_notext(self):
@override_settings(OCR_MODE="skip")
def test_encrypted(self):
parser = RasterisedDocumentParser(None)
def f():
parser.parse(os.path.join(self.SAMPLE_FILES, "with-form.pdf"), "application/pdf")
parser.parse(os.path.join(self.SAMPLE_FILES, "encrypted.pdf"), "application/pdf")
self.assertRaises(ParseError, f)
self.assertIsNone(parser.archive_path)
self.assertContainsStrings(parser.get_text(), ["This is a digitally signed PDF, created with Acrobat Pro for the Paperless project to enable", "automated testing of signed/encrypted PDFs"])
@override_settings(OCR_MODE="redo")
def test_with_form_error_notext(self):
parser = RasterisedDocumentParser(None)
parser.parse(os.path.join(self.SAMPLE_FILES, "with-form.pdf"), "application/pdf")
self.assertContainsStrings(parser.get_text(), ["Please enter your name in here:", "This is a PDF document with a form."])
@override_settings(OCR_MODE="force")
def test_with_form_force(self):
@@ -164,17 +176,12 @@ class TestParser(DirectoriesMixin, TestCase):
self.assertRaises(ParseError, f)
@mock.patch("paperless_tesseract.parsers.ocrmypdf.ocr")
def test_image_calc_a4_dpi(self, m):
def test_image_calc_a4_dpi(self):
parser = RasterisedDocumentParser(None)
parser.parse(os.path.join(self.SAMPLE_FILES, "simple-no-dpi.png"), "image/png")
dpi = parser.calculate_a4_dpi(os.path.join(self.SAMPLE_FILES, "simple-no-dpi.png"))
m.assert_called_once()
args, kwargs = m.call_args
self.assertEqual(kwargs['image_dpi'], 62)
self.assertEqual(dpi, 62)
@mock.patch("paperless_tesseract.parsers.RasterisedDocumentParser.calculate_a4_dpi")
def test_image_dpi_fail(self, m):
@@ -258,9 +265,82 @@ class TestParser(DirectoriesMixin, TestCase):
def test_skip_noarchive_notext(self):
parser = RasterisedDocumentParser(None)
parser.parse(os.path.join(self.SAMPLE_FILES, "multi-page-images.pdf"), "application/pdf")
self.assertTrue(os.path.join(parser.archive_path))
self.assertTrue(os.path.isfile(parser.archive_path))
self.assertContainsStrings(parser.get_text().lower(), ["page 1", "page 2", "page 3"])
@override_settings(OCR_MODE="skip")
def test_multi_page_mixed(self):
parser = RasterisedDocumentParser(None)
parser.parse(os.path.join(self.SAMPLE_FILES, "multi-page-mixed.pdf"), "application/pdf")
self.assertTrue(os.path.isfile(parser.archive_path))
self.assertContainsStrings(parser.get_text().lower(), ["page 1", "page 2", "page 3", "page 4", "page 5", "page 6"])
with open(os.path.join(parser.tempdir, "sidecar.txt")) as f:
sidecar = f.read()
self.assertIn("[OCR skipped on page 4]", sidecar)
self.assertIn("[OCR skipped on page 5]", sidecar)
self.assertIn("[OCR skipped on page 6]", sidecar)
@override_settings(OCR_MODE="skip_noarchive")
def test_multi_page_mixed_no_archive(self):
parser = RasterisedDocumentParser(None)
parser.parse(os.path.join(self.SAMPLE_FILES, "multi-page-mixed.pdf"), "application/pdf")
self.assertIsNone(parser.archive_path)
self.assertContainsStrings(parser.get_text().lower(), ["page 4", "page 5", "page 6"])
@override_settings(OCR_MODE="skip", OCR_ROTATE_PAGES=True)
def test_rotate(self):
parser = RasterisedDocumentParser(None)
parser.parse(os.path.join(self.SAMPLE_FILES, "rotated.pdf"), "application/pdf")
self.assertContainsStrings(parser.get_text(), [
"This is the text that appears on the first page. Its a lot of text.",
"Even if the pages are rotated, OCRmyPDF still gets the job done.",
"This is a really weird file with lots of nonsense text.",
"If you read this, its your own fault. Also check your screen orientation."
])
def test_ocrmypdf_parameters(self):
parser = RasterisedDocumentParser(None)
params = parser.construct_ocrmypdf_parameters(input_file="input.pdf", output_file="output.pdf",
sidecar_file="sidecar.txt", mime_type="application/pdf",
safe_fallback=False)
self.assertEqual(params['input_file'], "input.pdf")
self.assertEqual(params['output_file'], "output.pdf")
self.assertEqual(params['sidecar'], "sidecar.txt")
with override_settings(OCR_CLEAN="none"):
params = parser.construct_ocrmypdf_parameters("", "", "", "")
self.assertNotIn("clean", params)
self.assertNotIn("clean_final", params)
with override_settings(OCR_CLEAN="clean"):
params = parser.construct_ocrmypdf_parameters("", "", "", "")
self.assertTrue(params['clean'])
self.assertNotIn("clean_final", params)
with override_settings(OCR_CLEAN="clean-final", OCR_MODE="skip"):
params = parser.construct_ocrmypdf_parameters("", "", "", "")
self.assertTrue(params['clean_final'])
self.assertNotIn("clean", params)
with override_settings(OCR_CLEAN="clean-final", OCR_MODE="redo"):
params = parser.construct_ocrmypdf_parameters("", "", "", "")
self.assertTrue(params['clean'])
self.assertNotIn("clean_final", params)
with override_settings(OCR_DESKEW=True, OCR_MODE="skip"):
params = parser.construct_ocrmypdf_parameters("", "", "", "")
self.assertTrue(params['deskew'])
with override_settings(OCR_DESKEW=True, OCR_MODE="redo"):
params = parser.construct_ocrmypdf_parameters("", "", "", "")
self.assertNotIn('deskew', params)
with override_settings(OCR_DESKEW=False, OCR_MODE="skip"):
params = parser.construct_ocrmypdf_parameters("", "", "", "")
self.assertNotIn('deskew', params)
class TestParserFileTypes(DirectoriesMixin, TestCase):