mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge remote-tracking branch 'upstream/dev' into feature/remote-user
This commit is contained in:
commit
f0a1aed029
1
Pipfile
1
Pipfile
@ -42,6 +42,7 @@ whoosh="~=2.7.4"
|
|||||||
inotifyrecursive = "~=0.3.4"
|
inotifyrecursive = "~=0.3.4"
|
||||||
ocrmypdf = "*"
|
ocrmypdf = "*"
|
||||||
tqdm = "*"
|
tqdm = "*"
|
||||||
|
tika = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
|
57
Pipfile.lock
generated
57
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "3d576f289958226a7583e4c471c7f8c11bff6933bf093185f623cfb381a92412"
|
"sha256": "993e362c31af6b8094693075f614270a820cf0b557369d66d674e1a107b7bd31"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -44,6 +44,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.17.12"
|
"version": "==1.17.12"
|
||||||
},
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
||||||
|
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
||||||
|
],
|
||||||
|
"version": "==2020.12.5"
|
||||||
|
},
|
||||||
"cffi": {
|
"cffi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
|
"sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
|
||||||
@ -229,6 +236,15 @@
|
|||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==9.0"
|
"version": "==9.0"
|
||||||
},
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226",
|
||||||
|
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||||
|
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==2.10"
|
||||||
|
},
|
||||||
"imap-tools": {
|
"imap-tools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65",
|
"sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65",
|
||||||
@ -683,6 +699,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==3.5.56"
|
"version": "==3.5.56"
|
||||||
},
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||||
|
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
|
"version": "==2.25.1"
|
||||||
|
},
|
||||||
"scikit-learn": {
|
"scikit-learn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:090bbf144fd5823c1f2efa3e1a9bf180295b24294ca8f478e75b40ed54f8036e",
|
"sha256:090bbf144fd5823c1f2efa3e1a9bf180295b24294ca8f478e75b40ed54f8036e",
|
||||||
@ -769,6 +793,14 @@
|
|||||||
"markers": "python_version >= '3.5'",
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==2.1.0"
|
"version": "==2.1.0"
|
||||||
},
|
},
|
||||||
|
"tika": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c2c50f405622f74531841104f9e85c17511aede11de8e5385eab1a29a31f191b",
|
||||||
|
"sha256:d1f2eddb93caa9a2857569486aa2bc0320d0bf1796cdbe03066954cbc4b4bf62"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.24"
|
||||||
|
},
|
||||||
"tqdm": {
|
"tqdm": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
|
"sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
|
||||||
@ -777,6 +809,15 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.54.1"
|
"version": "==4.54.1"
|
||||||
},
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||||
|
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||||
|
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==3.7.4.3"
|
||||||
|
},
|
||||||
"tzlocal": {
|
"tzlocal": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44",
|
"sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44",
|
||||||
@ -784,6 +825,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.1"
|
"version": "==2.1"
|
||||||
},
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
|
||||||
|
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||||
|
"version": "==1.26.2"
|
||||||
|
},
|
||||||
"watchdog": {
|
"watchdog": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3caefdcc8f06a57fdc5ef2d22aa7c0bfda4f55e71a0bee74cbf3176d97536ef3",
|
"sha256:3caefdcc8f06a57fdc5ef2d22aa7c0bfda4f55e71a0bee74cbf3176d97536ef3",
|
||||||
@ -1197,11 +1246,11 @@
|
|||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
|
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||||
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
|
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==2.25.0"
|
"version": "==2.25.1"
|
||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
[](https://travis-ci.org/jonaswinkler/paperless-ng)
|
[](https://travis-ci.com/jonaswinkler/paperless-ng)
|
||||||
[](https://paperless-ng.readthedocs.io/en/latest/?badge=latest)
|
[](https://paperless-ng.readthedocs.io/en/latest/?badge=latest)
|
||||||
[](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
[](https://hub.docker.com/r/jonaswinkler/paperless-ng)
|
[](https://hub.docker.com/r/jonaswinkler/paperless-ng)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
bind = ['[::]:8000', 'localhost:8000']
|
bind = '0.0.0.0:8000'
|
||||||
backlog = 2048
|
backlog = 2048
|
||||||
workers = 3
|
workers = 3
|
||||||
worker_class = 'sync'
|
worker_class = 'sync'
|
||||||
|
@ -15,7 +15,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: paperless
|
POSTGRES_PASSWORD: paperless
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
image: jonaswinkler/paperless-ng:0.9.10
|
image: jonaswinkler/paperless-ng:0.9.11
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
@ -5,7 +5,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
image: jonaswinkler/paperless-ng:0.9.10
|
image: jonaswinkler/paperless-ng:0.9.11
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
- broker
|
- broker
|
||||||
|
43
docker/hub/docker-compose.tika.yml
Normal file
43
docker/hub/docker-compose.tika.yml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
broker:
|
||||||
|
image: redis:6.0
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
webserver:
|
||||||
|
image: jonaswinkler/paperless-ng:0.9.9
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- broker
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- data:/usr/src/paperless/data
|
||||||
|
- media:/usr/src/paperless/media
|
||||||
|
- ./export:/usr/src/paperless/export
|
||||||
|
- ./consume:/usr/src/paperless/consume
|
||||||
|
env_file: docker-compose.env
|
||||||
|
environment:
|
||||||
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
|
PAPERLESS_TIKA_ENABLED: 1
|
||||||
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
|
|
||||||
|
gotenberg:
|
||||||
|
image: thecodingmachine/gotenberg
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DISABLE_GOOGLE_CHROME: 1
|
||||||
|
|
||||||
|
tika:
|
||||||
|
image: apache/tika
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
media:
|
@ -63,6 +63,8 @@ WORKDIR /usr/src/paperless/src/
|
|||||||
|
|
||||||
RUN sudo -HEu paperless python3 manage.py collectstatic --clear --no-input
|
RUN sudo -HEu paperless python3 manage.py collectstatic --clear --no-input
|
||||||
|
|
||||||
|
RUN sudo -HEu paperless python3 manage.py compilemessages
|
||||||
|
|
||||||
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
|
VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/usr/src/paperless/consume", "/usr/src/paperless/export"]
|
||||||
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/sbin/docker-entrypoint.sh"]
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
43
docker/local/docker-compose.tika.yml
Normal file
43
docker/local/docker-compose.tika.yml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
broker:
|
||||||
|
image: redis:6.0
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
webserver:
|
||||||
|
build: .
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- broker
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- data:/usr/src/paperless/data
|
||||||
|
- media:/usr/src/paperless/media
|
||||||
|
- ./export:/usr/src/paperless/export
|
||||||
|
- ./consume:/usr/src/paperless/consume
|
||||||
|
env_file: docker-compose.env
|
||||||
|
environment:
|
||||||
|
PAPERLESS_REDIS: redis://broker:6379
|
||||||
|
PAPERLESS_TIKA_ENABLED: 1
|
||||||
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
|
|
||||||
|
gotenberg:
|
||||||
|
image: thecodingmachine/gotenberg
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DISABLE_GOOGLE_CHROME: 1
|
||||||
|
|
||||||
|
tika:
|
||||||
|
image: apache/tika
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
media:
|
@ -149,6 +149,12 @@ After grabbing the new release and unpacking the contents, do the following:
|
|||||||
$ cd src
|
$ cd src
|
||||||
$ pipenv run python3 manage.py migrate
|
$ pipenv run python3 manage.py migrate
|
||||||
|
|
||||||
|
5. Update translation files.
|
||||||
|
|
||||||
|
.. code:: shell-session
|
||||||
|
|
||||||
|
$ cd src
|
||||||
|
$ pipenv run python3 manage.py compilemessages
|
||||||
|
|
||||||
Management utilities
|
Management utilities
|
||||||
####################
|
####################
|
||||||
|
@ -5,6 +5,13 @@
|
|||||||
Changelog
|
Changelog
|
||||||
*********
|
*********
|
||||||
|
|
||||||
|
|
||||||
|
paperless-ng 0.9.11
|
||||||
|
###################
|
||||||
|
|
||||||
|
* Fixed an issue with the docker image not starting at all due to a configuration change of the web server.
|
||||||
|
|
||||||
|
|
||||||
paperless-ng 0.9.10
|
paperless-ng 0.9.10
|
||||||
###################
|
###################
|
||||||
|
|
||||||
@ -15,6 +22,7 @@ paperless-ng 0.9.10
|
|||||||
|
|
||||||
* Other changes and additions
|
* Other changes and additions
|
||||||
|
|
||||||
|
* Thanks to `zjean`_, paperless now publishes a webmanifest, which is useful for adding the application to home screens on mobile devices.
|
||||||
* The Paperless-ng logo now navigates to the dashboard.
|
* The Paperless-ng logo now navigates to the dashboard.
|
||||||
* Filter for documents that don't have any correspondents, types or tags assigned.
|
* Filter for documents that don't have any correspondents, types or tags assigned.
|
||||||
* Tags, types and correspondents are now sorted case insensitive.
|
* Tags, types and correspondents are now sorted case insensitive.
|
||||||
@ -25,6 +33,8 @@ paperless-ng 0.9.10
|
|||||||
* Added missing dependencies for Raspberry Pi builds.
|
* Added missing dependencies for Raspberry Pi builds.
|
||||||
* Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts.
|
* Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts.
|
||||||
* An issue with the search index reporting missing documents after bulk deletes was fixed.
|
* An issue with the search index reporting missing documents after bulk deletes was fixed.
|
||||||
|
* Issue with the tag selector not clearing input correctly.
|
||||||
|
* The consumer used to stop working when encountering an incomplete classifier model file.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@ -956,6 +966,7 @@ bulk of the work on this big change.
|
|||||||
|
|
||||||
* Initial release
|
* Initial release
|
||||||
|
|
||||||
|
.. _zjean: https://github.com/zjean
|
||||||
.. _rYR79435: https://github.com/rYR79435
|
.. _rYR79435: https://github.com/rYR79435
|
||||||
.. _Michael Shamoon: https://github.com/shamoon
|
.. _Michael Shamoon: https://github.com/shamoon
|
||||||
.. _jayme-github: http://github.com/jayme-github
|
.. _jayme-github: http://github.com/jayme-github
|
||||||
|
@ -283,6 +283,35 @@ PAPERLESS_OCR_USER_ARG=<json>
|
|||||||
|
|
||||||
{"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"}
|
{"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"}
|
||||||
|
|
||||||
|
.. _configuration-tika:
|
||||||
|
|
||||||
|
Tika settings
|
||||||
|
#############
|
||||||
|
|
||||||
|
Paperless can make use of `Tika <https://tika.apache.org/>`_ and
|
||||||
|
`Gotenberg <https://thecodingmachine.github.io/gotenberg/>`_ for parsing and
|
||||||
|
converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). If you
|
||||||
|
wish to use this, you must provide a Tika server and a Gotenberg server,
|
||||||
|
configure their endpoints, and enable the feature.
|
||||||
|
|
||||||
|
If you run paperless on docker, you can add those services to the docker-compose
|
||||||
|
file (see the examples provided).
|
||||||
|
|
||||||
|
PAPERLESS_TIKA_ENABLED=<bool>
|
||||||
|
Enable (or disable) the Tika parser.
|
||||||
|
|
||||||
|
Defaults to false.
|
||||||
|
|
||||||
|
PAPERLESS_TIKA_ENDPOINT=<url>
|
||||||
|
Set the endpoint URL were Paperless can reach your Tika server.
|
||||||
|
|
||||||
|
Defaults to "http://localhost:9998".
|
||||||
|
|
||||||
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT=<url>
|
||||||
|
Set the endpoint URL were Paperless can reach your Gotenberg server.
|
||||||
|
|
||||||
|
Defaults to "http://localhost:3000".
|
||||||
|
|
||||||
|
|
||||||
Software tweaks
|
Software tweaks
|
||||||
###############
|
###############
|
||||||
|
@ -293,6 +293,9 @@ writing. Windows is not and will never be supported.
|
|||||||
# This creates the database schema.
|
# This creates the database schema.
|
||||||
python3 manage.py migrate
|
python3 manage.py migrate
|
||||||
|
|
||||||
|
# This creates the translation files for paperless.
|
||||||
|
python3 manage.py compilemessages
|
||||||
|
|
||||||
# This creates your first paperless user
|
# This creates your first paperless user
|
||||||
python3 manage.py createsuperuser
|
python3 manage.py createsuperuser
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa
|
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa
|
||||||
#PAPERLESS_OCR_PAGES=1
|
#PAPERLESS_OCR_PAGES=1
|
||||||
#PAPERLESS_OCR_IMAGE_DPI=300
|
#PAPERLESS_OCR_IMAGE_DPI=300
|
||||||
#PAPERLESS_OCR_USER_ARG={}
|
#PAPERLESS_OCR_USER_ARGS={}
|
||||||
#PAPERLESS_CONVERT_MEMORY_LIMIT=0
|
#PAPERLESS_CONVERT_MEMORY_LIMIT=0
|
||||||
#PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless
|
#PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless
|
||||||
|
|
||||||
@ -57,6 +57,12 @@
|
|||||||
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
|
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
|
||||||
#PAPERLESS_THUMBNAIL_FONT_NAME=
|
#PAPERLESS_THUMBNAIL_FONT_NAME=
|
||||||
|
|
||||||
|
# Tika settings
|
||||||
|
|
||||||
|
#PAPERLESS_TIKA_ENABLED=false
|
||||||
|
#PAPERLESS_TIKA_ENDPOINT=http://localhost:9998
|
||||||
|
#PAPERLESS_TIKA_GOTENBERG_ENDPOINT=http://localhost:3000
|
||||||
|
|
||||||
# Binaries
|
# Binaries
|
||||||
|
|
||||||
#PAPERLESS_CONVERT_BINARY=/usr/bin/convert
|
#PAPERLESS_CONVERT_BINARY=/usr/bin/convert
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
docker run -p 5432:5432 -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
docker run -p 5432:5432 -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
||||||
docker run -d -p 6379:6379 redis:latest
|
docker run -d -p 6379:6379 redis:latest
|
||||||
|
docker run -p 3000:3000 -d thecodingmachine/gotenberg
|
||||||
|
docker run -p 9998:9998 -d apache/tika
|
||||||
|
@ -13,6 +13,12 @@
|
|||||||
"root": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
|
"i18n": {
|
||||||
|
"sourceLocale": "en-US",
|
||||||
|
"locales": {
|
||||||
|
"de": "src/locale/messages.de.xlf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
@ -23,10 +29,16 @@
|
|||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"localize": true,
|
||||||
"aot": true,
|
"aot": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets"
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest", {
|
||||||
|
"glob": "pdf.worker.min.js",
|
||||||
|
"input": "node_modules/pdfjs-dist/build/",
|
||||||
|
"output": "/assets/js/"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
@ -64,13 +76,16 @@
|
|||||||
"maximumError": "10kb"
|
"maximumError": "10kb"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"en-US": {
|
||||||
|
"localize": ["en-US"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "paperless-ui:build"
|
"browserTarget": "paperless-ui:build:en-US"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@ -93,7 +108,8 @@
|
|||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/assets"
|
"src/assets",
|
||||||
|
"src/manifest.webmanifest"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
|
@ -79,22 +79,15 @@
|
|||||||
<context context-type="linenumber">71</context>
|
<context context-type="linenumber">71</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="546b2014cc578af06b6023a7f38fa77aa9d58f5d" datatype="html">
|
<trans-unit id="439e7cc3c1ecefded167ed4d37f7d22dad6a9159" datatype="html">
|
||||||
<source>{VAR_PLURAL, plural, =1 {document} other {documents}}</source>
|
<source>{VAR_PLURAL, plural, =1 {Selected <x id="INTERPOLATION"/> of one document} other {Selected <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> documents}}</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">86</context>
|
<context context-type="linenumber">86</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3c298bb50741c8b2641889b0a0e0009769e66370" datatype="html">
|
<trans-unit id="bb773fdeaad5e7fb8e6cd77e1cc558e1b194a0c9" datatype="html">
|
||||||
<source>Selected <x id="INTERPOLATION" equiv-text="{{list.selected.size}}"/> of <x id="INTERPOLATION_1" equiv-text="{{list.collectionSize || 0}}"/> <x id="ICU" equiv-text="{list.collectionSize, plural, =1 {document} other {documents}}"/></source>
|
<source>{VAR_PLURAL, plural, =1 {One document} other {<x id="INTERPOLATION"/> documents}}</source>
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
|
||||||
<context context-type="linenumber">86</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8b04a8e375ac6760a54aabaf5df0287d53ce4a4a" datatype="html">
|
|
||||||
<source>{VAR_PLURAL, plural, =1 {1 document} other {<x id="INTERPOLATION"/> documents}}</source>
|
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">87</context>
|
<context context-type="linenumber">87</context>
|
||||||
@ -149,8 +142,8 @@
|
|||||||
<context context-type="linenumber">161</context>
|
<context context-type="linenumber">161</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5277522254327902345" datatype="html">
|
<trans-unit id="5382975254277698192" datatype="html">
|
||||||
<source>Do you really want to delete document '<x id="PH" equiv-text="this.document.title"/>'?</source>
|
<source>Do you really want to delete document "<x id="PH" equiv-text="this.document.title"/>"?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">162</context>
|
<context context-type="linenumber">162</context>
|
||||||
@ -373,11 +366,11 @@
|
|||||||
<context context-type="linenumber">1</context>
|
<context context-type="linenumber">1</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2133075428913430816" datatype="html">
|
<trans-unit id="93754014749412887" datatype="html">
|
||||||
<source>Do you really want to delete the tag <x id="PH" equiv-text="object.name"/>?</source>
|
<source>Do you really want to delete the tag "<x id="PH" equiv-text="object.name"/>"?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context>
|
||||||
<context context-type="linenumber">30</context>
|
<context context-type="linenumber">28</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="70a67e04629f6d412db0a12d51820b480788d795" datatype="html">
|
<trans-unit id="70a67e04629f6d412db0a12d51820b480788d795" datatype="html">
|
||||||
@ -436,11 +429,11 @@
|
|||||||
<context context-type="linenumber">37</context>
|
<context context-type="linenumber">37</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3954409824493602446" datatype="html">
|
<trans-unit id="4990731724078522539" datatype="html">
|
||||||
<source>Do you really want to delete the document type <x id="PH" equiv-text="object.name"/>?</source>
|
<source>Do you really want to delete the document type "<x id="PH" equiv-text="object.name"/>"?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/document-type-list/document-type-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/document-type-list/document-type-list.component.ts</context>
|
||||||
<context context-type="linenumber">26</context>
|
<context context-type="linenumber">24</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bc000b39af12c0925c424f4cb85f0c31c0f8eca8" datatype="html">
|
<trans-unit id="bc000b39af12c0925c424f4cb85f0c31c0f8eca8" datatype="html">
|
||||||
@ -464,25 +457,32 @@
|
|||||||
<context context-type="linenumber">7</context>
|
<context context-type="linenumber">7</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1153806754022288374" datatype="html">
|
<trans-unit id="5610279464668232148" datatype="html">
|
||||||
<source>Saved view "<x id="PH" equiv-text="savedView.name"/> deleted.</source>
|
<source>Saved view "<x id="PH" equiv-text="savedView.name"/>" deleted.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">52</context>
|
<context context-type="linenumber">54</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5647210819299459618" datatype="html">
|
<trans-unit id="5647210819299459618" datatype="html">
|
||||||
<source>Settings saved successfully.</source>
|
<source>Settings saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">61</context>
|
<context context-type="linenumber">74</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8488620293789898901" datatype="html">
|
<trans-unit id="8488620293789898901" datatype="html">
|
||||||
<source>Error while storing settings on server: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source>
|
<source>Error while storing settings on server: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
|
||||||
<context context-type="linenumber">73</context>
|
<context context-type="linenumber">86</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
|
||||||
|
<source>Settings</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">1</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="11ebd254cc9294717105c5982eb0cd2af30a446d" datatype="html">
|
<trans-unit id="11ebd254cc9294717105c5982eb0cd2af30a446d" datatype="html">
|
||||||
@ -496,11 +496,11 @@
|
|||||||
<source>Saved views</source>
|
<source>Saved views</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">41</context>
|
<context context-type="linenumber">56</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="0d8ceb153aa715eb905da0710cc0b2ac73159abc" datatype="html">
|
<trans-unit id="bbe41ac2ea4a6c00ea941a41b33105048f8e9f13" datatype="html">
|
||||||
<source>Document list</source>
|
<source>Appearance</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</context>
|
||||||
@ -513,60 +513,74 @@
|
|||||||
<context context-type="linenumber">17</context>
|
<context context-type="linenumber">17</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="9ee5d1cbfd6ee168dae37aaba2b59b50bcabb2ff" datatype="html">
|
||||||
|
<source>Dark mode</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">33</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="f8cb5506e70fd71fddc9bb71cee18bfff7b29637" datatype="html">
|
||||||
|
<source>Use system settings</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
|
<context context-type="linenumber">36</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="3863a86cd9e69a61d143d3daf51df44203df4a82" datatype="html">
|
<trans-unit id="3863a86cd9e69a61d143d3daf51df44203df4a82" datatype="html">
|
||||||
<source>Bulk editing</source>
|
<source>Bulk editing</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">33</context>
|
<context context-type="linenumber">44</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c0ac61661c6c326d6e0e00c231b95cf2ac0c6586" datatype="html">
|
<trans-unit id="c0ac61661c6c326d6e0e00c231b95cf2ac0c6586" datatype="html">
|
||||||
<source>Show confirmation dialogs</source>
|
<source>Show confirmation dialogs</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">35</context>
|
<context context-type="linenumber">48</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="291bbe56ecbe945dcf05580a57d679fa7bd1e06a" datatype="html">
|
<trans-unit id="291bbe56ecbe945dcf05580a57d679fa7bd1e06a" datatype="html">
|
||||||
<source>Deleting documents will always ask for confirmation.</source>
|
<source>Deleting documents will always ask for confirmation.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">35</context>
|
<context context-type="linenumber">48</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8cfddc13e04f5545ac63f419ef363505d6f78c2e" datatype="html">
|
<trans-unit id="8cfddc13e04f5545ac63f419ef363505d6f78c2e" datatype="html">
|
||||||
<source>Apply on close</source>
|
<source>Apply on close</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">36</context>
|
<context context-type="linenumber">49</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8cb90334f5dfd7fc67205085f59381e2a334ccfc" datatype="html">
|
<trans-unit id="8cb90334f5dfd7fc67205085f59381e2a334ccfc" datatype="html">
|
||||||
<source>Appears on</source>
|
<source>Appears on</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">53</context>
|
<context context-type="linenumber">68</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6717cf1acf04728fc2b7c39f6d3297f8ff15fde5" datatype="html">
|
<trans-unit id="6717cf1acf04728fc2b7c39f6d3297f8ff15fde5" datatype="html">
|
||||||
<source>Show on dashboard</source>
|
<source>Show on dashboard</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">56</context>
|
<context context-type="linenumber">71</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="541bfc5b123b3f8867fd681eaceefb663a811973" datatype="html">
|
<trans-unit id="541bfc5b123b3f8867fd681eaceefb663a811973" datatype="html">
|
||||||
<source>Show in sidebar</source>
|
<source>Show in sidebar</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">60</context>
|
<context context-type="linenumber">75</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="abba764a7a595d04dc8c3b26e04b3780d4fdb540" datatype="html">
|
<trans-unit id="abba764a7a595d04dc8c3b26e04b3780d4fdb540" datatype="html">
|
||||||
<source>No saved views defined.</source>
|
<source>No saved views defined.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">70</context>
|
<context context-type="linenumber">85</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ef60a738a565f498b858e903e42bc5ffc3cc1299" datatype="html">
|
<trans-unit id="ef60a738a565f498b858e903e42bc5ffc3cc1299" datatype="html">
|
||||||
@ -576,11 +590,11 @@
|
|||||||
<context context-type="linenumber">7</context>
|
<context context-type="linenumber">7</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2337099367951805921" datatype="html">
|
<trans-unit id="7427874343955308724" datatype="html">
|
||||||
<source>Do you really want to delete the correspondent <x id="PH" equiv-text="object.name"/>?</source>
|
<source>Do you really want to delete the correspondent "<x id="PH" equiv-text="object.name"/>"?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.ts</context>
|
||||||
<context context-type="linenumber">26</context>
|
<context context-type="linenumber">24</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c3f3334de899327bf3ec8999236e10798ff76e72" datatype="html">
|
<trans-unit id="c3f3334de899327bf3ec8999236e10798ff76e72" datatype="html">
|
||||||
@ -639,8 +653,8 @@
|
|||||||
<context context-type="linenumber">11</context>
|
<context context-type="linenumber">11</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="180092a6b8a6151a05f4a7552a2fb75fd159dfa8" datatype="html">
|
<trans-unit id="eab7fc7cf2d663e54de934b779fce4275a303f0f" datatype="html">
|
||||||
<source>Match</source>
|
<source>Matching pattern</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
|
||||||
<context context-type="linenumber">12</context>
|
<context context-type="linenumber">12</context>
|
||||||
@ -776,26 +790,26 @@
|
|||||||
<source>Paperless-ng</source>
|
<source>Paperless-ng</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">11</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">app title</note>
|
<note priority="1" from="description">app title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8d667444401ef6380fd262e4fe4795f261a427b1" datatype="html">
|
<trans-unit id="069566c6ed4f051b5b5617ef1935837226585dad" datatype="html">
|
||||||
<source>Search for documents</source>
|
<source>Search documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">12</context>
|
<context context-type="linenumber">15</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="bb694b49d408265c91c62799c2b3a7e3151c824d" datatype="html">
|
||||||
|
<source>Logout</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
|
<context context-type="linenumber">45</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="68949525c4d9a901e0cd15a94e3fc8d2711e9918" datatype="html">
|
<trans-unit id="68949525c4d9a901e0cd15a94e3fc8d2711e9918" datatype="html">
|
||||||
<source>Manage</source>
|
<source>Manage</source>
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
|
||||||
<context context-type="linenumber">77</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
|
|
||||||
<source>Settings</source>
|
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">112</context>
|
<context context-type="linenumber">112</context>
|
||||||
@ -805,70 +819,91 @@
|
|||||||
<source>Admin</source>
|
<source>Admin</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">119</context>
|
<context context-type="linenumber">147</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="46aa32e581922d6d2c3d7bc4c87209ad5808b029" datatype="html">
|
<trans-unit id="46aa32e581922d6d2c3d7bc4c87209ad5808b029" datatype="html">
|
||||||
<source>Misc</source>
|
<source>Misc</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">125</context>
|
<context context-type="linenumber">153</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7" datatype="html">
|
<trans-unit id="fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7" datatype="html">
|
||||||
<source>Documentation</source>
|
<source>Documentation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">132</context>
|
<context context-type="linenumber">160</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="355a222236bc01b9a8cd3cb9ecf76891125aed69" datatype="html">
|
<trans-unit id="355a222236bc01b9a8cd3cb9ecf76891125aed69" datatype="html">
|
||||||
<source>GitHub</source>
|
<source>GitHub</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">139</context>
|
<context context-type="linenumber">167</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bb694b49d408265c91c62799c2b3a7e3151c824d" datatype="html">
|
<trans-unit id="af665f8de8fabe306aaf27443957e69bcbbce63c" datatype="html">
|
||||||
<source>Logout</source>
|
<source>Logged in as <x id="INTERPOLATION" equiv-text="{{displayName}}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">146</context>
|
<context context-type="linenumber">34</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4f55b670f49d927c6026bb614c7c62b1f2a394c0" datatype="html">
|
<trans-unit id="4f55b670f49d927c6026bb614c7c62b1f2a394c0" datatype="html">
|
||||||
<source>Open documents</source>
|
<source>Open documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">57</context>
|
<context context-type="linenumber">92</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="dca5bf9344a759fa5a07f1b21f50286ec242ba44" datatype="html">
|
<trans-unit id="dca5bf9344a759fa5a07f1b21f50286ec242ba44" datatype="html">
|
||||||
<source>Close all</source>
|
<source>Close all</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">71</context>
|
<context context-type="linenumber">106</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5195932016807797291" datatype="html">
|
<trans-unit id="5195932016807797291" datatype="html">
|
||||||
<source>Correspondent: <x id="PH" equiv-text="this.correspondents.find(c => c.id == +rule.value)?.name"/></source>
|
<source>Correspondent: <x id="PH" equiv-text="this.correspondents.find(c => c.id == +rule.value)?.name"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||||
<context context-type="linenumber">28</context>
|
<context context-type="linenumber">29</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8170755470576301659" datatype="html">
|
||||||
|
<source>Without correspondent</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">31</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8705701325879965907" datatype="html">
|
<trans-unit id="8705701325879965907" datatype="html">
|
||||||
<source>Type: <x id="PH" equiv-text="this.documentTypes.find(dt => dt.id == +rule.value)?.name"/></source>
|
<source>Type: <x id="PH" equiv-text="this.documentTypes.find(dt => dt.id == +rule.value)?.name"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||||
<context context-type="linenumber">31</context>
|
<context context-type="linenumber">36</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="4362173610367509215" datatype="html">
|
||||||
|
<source>Without document type</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">38</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8180755793012580465" datatype="html">
|
<trans-unit id="8180755793012580465" datatype="html">
|
||||||
<source>Tag: <x id="PH" equiv-text="this.tags.find(t => t.id == +rule.value)?.name"/></source>
|
<source>Tag: <x id="PH" equiv-text="this.tags.find(t => t.id == +rule.value)?.name"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||||
<context context-type="linenumber">34</context>
|
<context context-type="linenumber">42</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="6494566478302448576" datatype="html">
|
||||||
|
<source>Without any tag</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">46</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ddb40946e790522301687ecddb9ce1cb8ad40dd1" datatype="html">
|
<trans-unit id="ddb40946e790522301687ecddb9ce1cb8ad40dd1" datatype="html">
|
||||||
@ -878,19 +913,41 @@
|
|||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">4</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="02d184c288f567825a1fcbf83bcd3099a10853d5" datatype="html">
|
||||||
|
<source>Filter tags</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">12</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="4b089ca12c472cf0b46167bb5afe4b527b301bbc" datatype="html">
|
||||||
|
<source>Filter correspondents</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">19</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="0ad509732aaf702b7ea8c771c7809fa84bc85908" datatype="html">
|
||||||
|
<source>Filter document types</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">25</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="2f33515a935c36763660e8420940b8a7e11fb1f4" datatype="html">
|
<trans-unit id="2f33515a935c36763660e8420940b8a7e11fb1f4" datatype="html">
|
||||||
<source>Clear all filters</source>
|
<source>Clear all filters</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||||
<context context-type="linenumber">23</context>
|
<context context-type="linenumber">47</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7593728289020204896" datatype="html">
|
<trans-unit id="7593728289020204896" datatype="html">
|
||||||
<source>Not assigned</source>
|
<source>Not assigned</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
|
||||||
<context context-type="linenumber">145</context>
|
<context context-type="linenumber">161</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
|
<note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="c2d0ac9f528bbd5f53fd34269fde8b59e029621b" datatype="html">
|
<trans-unit id="c2d0ac9f528bbd5f53fd34269fde8b59e029621b" datatype="html">
|
||||||
<source>Apply</source>
|
<source>Apply</source>
|
||||||
@ -955,13 +1012,6 @@
|
|||||||
<context context-type="linenumber">50</context>
|
<context context-type="linenumber">50</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9c5fdf21ec2cc7baa6f062f2dc417af45c8dbe60" datatype="html">
|
|
||||||
<source>Score:</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
|
|
||||||
<context context-type="linenumber">61</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1b29a8153575e5ad26cc7dd8bd75c4f45f6bfe7e" datatype="html">
|
<trans-unit id="1b29a8153575e5ad26cc7dd8bd75c4f45f6bfe7e" datatype="html">
|
||||||
<source>Created: <x id="INTERPOLATION" equiv-text="{{document.created | date}}"/></source>
|
<source>Created: <x id="INTERPOLATION" equiv-text="{{document.created | date}}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@ -983,6 +1033,13 @@
|
|||||||
<context context-type="linenumber">24</context>
|
<context context-type="linenumber">24</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="9c5fdf21ec2cc7baa6f062f2dc417af45c8dbe60" datatype="html">
|
||||||
|
<source>Score:</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
|
||||||
|
<context context-type="linenumber">61</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="2840db547019ce8c76b2cdbe3a1653c5b68b06af" datatype="html">
|
<trans-unit id="2840db547019ce8c76b2cdbe3a1653c5b68b06af" datatype="html">
|
||||||
<source>View in browser</source>
|
<source>View in browser</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@ -990,12 +1047,20 @@
|
|||||||
<context context-type="linenumber">40</context>
|
<context context-type="linenumber">40</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5033601776243148314" datatype="html">
|
<trans-unit id="8639884465898458690" datatype="html">
|
||||||
<source><x id="PH" equiv-text="items[0].name"/> and <x id="PH_1" equiv-text="items[1].name"/></source>
|
<source>"<x id="PH" equiv-text="items[0].name"/>" and "<x id="PH_1" equiv-text="items[1].name"/>"</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">103</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
|
<note priority="1" from="description">This is for messages like 'modify "tag1" and "tag2"'</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7894972847287473517" datatype="html">
|
||||||
|
<source>"<x id="PH" equiv-text="i.name"/>"</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">105</context>
|
||||||
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="760986369763309193" datatype="html">
|
<trans-unit id="760986369763309193" datatype="html">
|
||||||
<source>, </source>
|
<source>, </source>
|
||||||
@ -1003,6 +1068,15 @@
|
|||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">105</context>
|
<context context-type="linenumber">105</context>
|
||||||
</context-group>
|
</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>
|
||||||
|
<trans-unit id="1822679894391095557" datatype="html">
|
||||||
|
<source><x id="PH" equiv-text="list"/> and "<x id="PH_1" equiv-text="items[items.length - 1].name"/>"</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">106</context>
|
||||||
|
</context-group>
|
||||||
|
<note priority="1" from="description">this is for messages like 'modify "tag1", "tag2" and "tag3"'</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4137232459980262849" datatype="html">
|
<trans-unit id="4137232459980262849" datatype="html">
|
||||||
<source>Confirm tags assignment</source>
|
<source>Confirm tags assignment</source>
|
||||||
@ -1011,36 +1085,36 @@
|
|||||||
<context context-type="linenumber">115</context>
|
<context context-type="linenumber">115</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5778291417880283825" datatype="html">
|
<trans-unit id="6619516195038467207" datatype="html">
|
||||||
<source>This operation will add the tag <x id="PH" equiv-text="tag.name"/> to all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<source>This operation will add the tag "<x id="PH" equiv-text="tag.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">118</context>
|
<context context-type="linenumber">118</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4791265247184178563" datatype="html">
|
<trans-unit id="1894412783609570695" datatype="html">
|
||||||
<source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> to all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">120</context>
|
<context context-type="linenumber">120</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7202114001606049276" datatype="html">
|
<trans-unit id="7181166515756808573" datatype="html">
|
||||||
<source>This operation will remove the tag <x id="PH" equiv-text="tag.name"/> from all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<source>This operation will remove the tag "<x id="PH" equiv-text="tag.name"/>" from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">123</context>
|
<context context-type="linenumber">123</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="247266594076352528" datatype="html">
|
<trans-unit id="3819792277998068944" datatype="html">
|
||||||
<source>This operation will remove the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> from all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">125</context>
|
<context context-type="linenumber">125</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4286636723521919383" datatype="html">
|
<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 all <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">127</context>
|
<context context-type="linenumber">127</context>
|
||||||
@ -1053,15 +1127,15 @@
|
|||||||
<context context-type="linenumber">157</context>
|
<context context-type="linenumber">157</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9000739289559833849" datatype="html">
|
<trans-unit id="6900893559485781849" datatype="html">
|
||||||
<source>This operation will assign the correspondent <x id="PH" equiv-text="correspondent.name"/> to all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<source>This operation will assign the correspondent "<x id="PH" equiv-text="correspondent.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">159</context>
|
<context context-type="linenumber">159</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5197985579238314950" datatype="html">
|
<trans-unit id="1257522660364398440" datatype="html">
|
||||||
<source>This operation will remove the correspondent from all <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">161</context>
|
<context context-type="linenumber">161</context>
|
||||||
@ -1074,15 +1148,15 @@
|
|||||||
<context context-type="linenumber">190</context>
|
<context context-type="linenumber">190</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="471313288900612996" datatype="html">
|
<trans-unit id="332180123895325027" datatype="html">
|
||||||
<source>This operation will assign the document type <x id="PH" equiv-text="documentType.name"/> to all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<source>This operation will assign the document type "<x id="PH" equiv-text="documentType.name"/>" to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">192</context>
|
<context context-type="linenumber">192</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6005206188202839923" datatype="html">
|
<trans-unit id="2236642492594872779" datatype="html">
|
||||||
<source>This operation will remove the document type from all <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<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-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">194</context>
|
<context context-type="linenumber">194</context>
|
||||||
@ -1095,8 +1169,8 @@
|
|||||||
<context context-type="linenumber">219</context>
|
<context context-type="linenumber">219</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3928393581343272038" datatype="html">
|
<trans-unit id="4303174930844518780" datatype="html">
|
||||||
<source>This operation will permanently delete all <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
<source>This operation will permanently delete <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
<context context-type="linenumber">220</context>
|
<context context-type="linenumber">220</context>
|
||||||
@ -1214,8 +1288,8 @@
|
|||||||
<context context-type="linenumber">5</context>
|
<context context-type="linenumber">5</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1be5ea6494cb95abc74f42ee9cfddb7ba3a53709" datatype="html">
|
<trans-unit id="33c76d75ce25ce3b05ab22877f1b6b09dcf603ae" datatype="html">
|
||||||
<source>Uploading <x id="INTERPOLATION" equiv-text="{{uploadStatus.length}}"/> file(s)</source>
|
<source>{VAR_PLURAL, plural, =1 {Uploading file...} =other {Uploading <x id="INTERPOLATION"/> files...}}</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.html</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</context>
|
||||||
@ -1235,15 +1309,15 @@
|
|||||||
<context context-type="linenumber">5</context>
|
<context context-type="linenumber">5</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="73d73c4f994d21fcb441cf316884db54693be3fa" datatype="html">
|
<trans-unit id="ea8d9a9486d5639d1c38c012900b8d34d5e4135d" datatype="html">
|
||||||
<source>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list. After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and have them displayed on the dashboard instead of this message.</source>
|
<source>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list. After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and they will appear on the dashboard instead of this message.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html</context>
|
||||||
<context context-type="linenumber">6,7</context>
|
<context context-type="linenumber">6,7</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="0b87e4267fd45103b1a9c474d243b3366dbf12ee" datatype="html">
|
<trans-unit id="cf5f85690feaba6e29343f9881e57a6c0ea6e82b" datatype="html">
|
||||||
<source>Paperless offers some more features that try to make your life easier, such as:</source>
|
<source>Paperless offers some more features that try to make your life easier:</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html</context>
|
||||||
<context context-type="linenumber">8</context>
|
<context context-type="linenumber">8</context>
|
||||||
@ -1515,22 +1589,43 @@
|
|||||||
<context context-type="linenumber">97</context>
|
<context context-type="linenumber">97</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3184700926171002527" datatype="html">
|
<trans-unit id="5851669019930456395" datatype="html">
|
||||||
<source>Any</source>
|
<source>Any word</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
<context context-type="linenumber">12</context>
|
<context context-type="linenumber">12</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1616102757855967475" datatype="html">
|
<trans-unit id="7517655726614958140" datatype="html">
|
||||||
<source>All</source>
|
<source>Any: Document contains any of these words (space separated)</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
|
<context context-type="linenumber">12</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="700315718208181326" datatype="html">
|
||||||
|
<source>All words</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1968183742008490888" datatype="html">
|
<trans-unit id="111914402588955480" datatype="html">
|
||||||
<source>Literal</source>
|
<source>All: Document contains all of these words (space separated)</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
|
<context context-type="linenumber">13</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="9180173992399180575" datatype="html">
|
||||||
|
<source>Exact match</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
|
<context context-type="linenumber">14</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7109184332944610787" datatype="html">
|
||||||
|
<source>Exact: Document contains this string</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
<context context-type="linenumber">14</context>
|
<context context-type="linenumber">14</context>
|
||||||
@ -1543,15 +1638,29 @@
|
|||||||
<context context-type="linenumber">15</context>
|
<context context-type="linenumber">15</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="701356546322112069" datatype="html">
|
<trans-unit id="7548151332424148033" datatype="html">
|
||||||
<source>Fuzzy match</source>
|
<source>Regular expression: Document matches this regular expression</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
|
<context context-type="linenumber">15</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="1856513373880048959" datatype="html">
|
||||||
|
<source>Fuzzy word</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
<context context-type="linenumber">16</context>
|
<context context-type="linenumber">16</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="616064537937996961" datatype="html">
|
<trans-unit id="8419167206585286450" datatype="html">
|
||||||
<source>Auto</source>
|
<source>Fuzzy: Document contains a word similar to this word</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
|
<context context-type="linenumber">16</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2167862279705099846" datatype="html">
|
||||||
|
<source>Auto: Learn matching automatically</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
|
||||||
<context context-type="linenumber">17</context>
|
<context context-type="linenumber">17</context>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { SettingsService } from './services/settings.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -7,8 +8,10 @@ import { Component } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
|
||||||
constructor () {
|
constructor (private settings: SettingsService) {
|
||||||
|
let anyWindow = (window as any)
|
||||||
|
anyWindow.pdfWorkerSrc = '/assets/js/pdf.worker.min.js';
|
||||||
|
this.settings.updateDarkModeSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,52 @@
|
|||||||
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
|
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
|
||||||
<a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" routerLink="/dashboard">
|
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
||||||
<img src="assets/logo-dark-notext.svg" height="18px" class="mr-2">
|
|
||||||
<ng-container i18n="app title">Paperless-ng</ng-container>
|
|
||||||
</a>
|
|
||||||
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse"
|
|
||||||
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<form (ngSubmit)="search()" class="w-100 m-1">
|
<a class="navbar-brand col-auto col-md-3 col-lg-2 mr-0 px-3 py-3 order-sm-0" routerLink="/dashboard">
|
||||||
<input class="form-control form-control-dark" type="text" placeholder="Search for documents" aria-label="Search"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1rem" class="mr-2" fill="currentColor">
|
||||||
|
<path d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" transform="translate(0 0)"/>
|
||||||
|
</svg>
|
||||||
|
<ng-container i18n="app title">Paperless-ng</ng-container>
|
||||||
|
</a>
|
||||||
|
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 pl-md-4 mr-sm-auto order-3 order-sm-1">
|
||||||
|
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
|
||||||
|
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
|
||||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder>
|
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder>
|
||||||
|
<svg width="1em" height="1em">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#search"/>
|
||||||
|
</svg>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
<ul ngbNav class="order-sm-3">
|
||||||
|
<li ngbDropdown class="nav-item dropdown">
|
||||||
|
<button class="btn text-light" id="userDropdown" ngbDropdownToggle>
|
||||||
|
<span *ngIf="displayName" class="navbar-text small mr-2 text-light d-none d-sm-inline">
|
||||||
|
{{displayName}}
|
||||||
|
</span>
|
||||||
|
<svg width="1.3em" height="1.3em">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#person-circle"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdownMenu class="dropdown-menu-right shadow mr-2" aria-labelledby="userDropdown">
|
||||||
|
<div *ngIf="displayName" class="d-sm-none">
|
||||||
|
<p class="small mb-0 px-3" i18n>Logged in as {{displayName}}</p>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
</div>
|
||||||
|
<a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()">
|
||||||
|
<svg class="sidebaricon mr-2" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
||||||
|
</svg><ng-container i18n>Settings</ng-container>
|
||||||
|
</a>
|
||||||
|
<a ngbDropdownItem class="nav-link" href="accounts/logout/">
|
||||||
|
<svg class="sidebaricon mr-2" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
|
||||||
|
</svg><ng-container i18n>Logout</ng-container>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -105,13 +140,6 @@
|
|||||||
</svg> <ng-container i18n>Logs</ng-container>
|
</svg> <ng-container i18n>Logs</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
|
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
|
|
||||||
</svg> <ng-container i18n>Settings</ng-container>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="admin/">
|
<a class="nav-link" href="admin/">
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
@ -139,13 +167,6 @@
|
|||||||
</svg> <ng-container i18n>GitHub</ng-container>
|
</svg> <ng-container i18n>GitHub</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="accounts/logout/">
|
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#door-open"/>
|
|
||||||
</svg> <ng-container i18n>Logout</ng-container>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -1,36 +1,30 @@
|
|||||||
|
|
||||||
@import "/src/theme";
|
@import "/src/theme";
|
||||||
|
/*
|
||||||
/*
|
|
||||||
* Sidebar
|
* Sidebar
|
||||||
*/
|
*/
|
||||||
|
.sidebar {
|
||||||
.sidebar {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 100; /* Behind the navbar */
|
z-index: 100; /* Behind the navbar */
|
||||||
padding: 48px 0 0; /* Height of navbar */
|
padding: 50px 0 0; /* Height of navbar */
|
||||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
top: 3rem;
|
top: 3.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sticky {
|
.sidebar-sticky {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
/* height: calc(100vh - 48px); */
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-top: .5rem;
|
padding-top: 0.5rem;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||||
}
|
}
|
||||||
|
|
||||||
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
@supports ((position: -webkit-sticky) or (position: sticky)) {
|
||||||
.sidebar-sticky {
|
.sidebar-sticky {
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
@ -53,36 +47,85 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link:hover .sidebaricon,
|
.sidebar .nav-link.active .sidebaricon,
|
||||||
.sidebar .nav-link.active .sidebaricon {
|
.sidebar .nav-link:hover .sidebaricon {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-heading {
|
.sidebar-heading {
|
||||||
font-size: .75rem;
|
font-size: 0.75rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Navbar
|
* Navbar
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
padding-top: .75rem;
|
padding-top: 0.75rem;
|
||||||
padding-bottom: .75rem;
|
padding-bottom: 0.75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
background-color: rgba(0, 0, 0, .25);
|
|
||||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-toggler {
|
.dropdown.show .dropdown-toggle,
|
||||||
top: .25rem;
|
.dropdown-toggle:hover {
|
||||||
right: 1rem;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .form-control {
|
.dropdown-toggle::after {
|
||||||
padding: .75rem 1rem;
|
margin-left: 0.4em;
|
||||||
border-width: 0;
|
vertical-align: 0.155em;
|
||||||
border-radius: 0;
|
}
|
||||||
|
|
||||||
|
.navbar .dropdown-menu {
|
||||||
|
font-size: 0.875rem; // body size
|
||||||
|
|
||||||
|
a svg {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .search-form-container {
|
||||||
|
max-width: 550px;
|
||||||
|
|
||||||
|
form {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.6rem;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
padding-left: 1.8rem;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
transition: flex 0.3s ease;
|
||||||
|
max-width: 600px;
|
||||||
|
min-width: 300px; // 1/2 max
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #212529;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service';
|
|||||||
import { SearchService } from 'src/app/services/rest/search.service';
|
import { SearchService } from 'src/app/services/rest/search.service';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
|
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
|
||||||
|
import { Meta } from '@angular/platform-browser';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-app-frame',
|
selector: 'app-app-frame',
|
||||||
@ -22,8 +23,10 @@ export class AppFrameComponent implements OnInit, OnDestroy {
|
|||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private openDocumentsService: OpenDocumentsService,
|
private openDocumentsService: OpenDocumentsService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
public savedViewService: SavedViewService
|
public savedViewService: SavedViewService,
|
||||||
|
private meta: Meta
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
versionString = `${environment.appTitle} ${environment.version}`
|
versionString = `${environment.appTitle} ${environment.version}`
|
||||||
@ -98,4 +101,17 @@ export class AppFrameComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get displayName() {
|
||||||
|
// TODO: taken from dashboard component, is this the best way to pass around username?
|
||||||
|
let tagFullName = this.meta.getTag('name=full_name')
|
||||||
|
let tagUsername = this.meta.getTag('name=username')
|
||||||
|
if (tagFullName && tagFullName.content) {
|
||||||
|
return tagFullName.content
|
||||||
|
} else if (tagUsername && tagUsername.content) {
|
||||||
|
return tagUsername.content
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
|||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model';
|
import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model';
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
import { ObjectWithId } from 'src/app/data/object-with-id';
|
||||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
@ -61,6 +61,10 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
|
|||||||
return MATCHING_ALGORITHMS
|
return MATCHING_ALGORITHMS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get patternRequired(): boolean {
|
||||||
|
return this.objectForm?.value.matching_algorithm !== MATCH_AUTO
|
||||||
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value)
|
var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value)
|
||||||
var serverResponse: Observable<T>
|
var serverResponse: Observable<T>
|
||||||
|
@ -16,11 +16,11 @@
|
|||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input class="form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="selectionModel.items" class="items">
|
<div *ngIf="selectionModel.items" class="items">
|
||||||
<ng-container *ngFor="let item of selectionModel.items | filter: filterText">
|
<ng-container *ngFor="let item of (editing ? selectionModel.itemsSorted : selectionModel.items) | filter: filterText">
|
||||||
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
|
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,18 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
items: MatchingModel[] = []
|
items: MatchingModel[] = []
|
||||||
|
|
||||||
|
get itemsSorted(): MatchingModel[] {
|
||||||
|
return this.items.sort((a,b) => {
|
||||||
|
if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) {
|
||||||
|
return 1
|
||||||
|
} else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private selectionStates = new Map<number, ToggleableItemState>()
|
private selectionStates = new Map<number, ToggleableItemState>()
|
||||||
|
|
||||||
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
private temporarySelectionStates = new Map<number, ToggleableItemState>()
|
||||||
@ -69,6 +81,10 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getNonTemporary(id: number) {
|
||||||
|
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
|
||||||
get(id: number) {
|
get(id: number) {
|
||||||
return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
@ -142,7 +158,7 @@ export class FilterableDropdownComponent {
|
|||||||
if (items) {
|
if (items) {
|
||||||
this._selectionModel.items = Array.from(items)
|
this._selectionModel.items = Array.from(items)
|
||||||
this._selectionModel.items.unshift({
|
this._selectionModel.items.unshift({
|
||||||
name: $localize`Not assigned`,
|
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
|
||||||
id: null
|
id: null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -186,6 +202,9 @@ export class FilterableDropdownComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
filterPlaceholder: string = ""
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
icon: string
|
icon: string
|
||||||
|
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
|
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
[closeOnSelect]="false"
|
[closeOnSelect]="false"
|
||||||
|
[clearSearchOnAdd]="true"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
|
[hideSelected]="true"
|
||||||
(change)="ngSelectChange()">
|
(change)="ngSelectChange()">
|
||||||
|
|
||||||
<ng-template ng-label-tmp let-item="item">
|
<ng-template ng-label-tmp let-item="item">
|
||||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,7 @@
|
|||||||
|
table {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:first-child {
|
||||||
|
min-width: 5rem;
|
||||||
|
}
|
@ -10,7 +10,7 @@
|
|||||||
</ngx-file-drop>
|
</ngx-file-drop>
|
||||||
</form>
|
</form>
|
||||||
<div *ngIf="uploadVisible" class="mt-3">
|
<div *ngIf="uploadVisible" class="mt-3">
|
||||||
<p i18n>Uploading {{uploadStatus.length}} file(s)</p>
|
<p i18n>{uploadStatus.length, plural, =1 {Uploading file...} =other {Uploading {{uploadStatus.length}} files...}}</p>
|
||||||
<ngb-progressbar [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
|
<ngb-progressbar [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
|
||||||
</ngb-progressbar>
|
</ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
<img src="assets/save-filter.png" class="float-right">
|
<img src="assets/save-filter.png" class="float-right">
|
||||||
<p i18n>Paperless is running! :)</p>
|
<p i18n>Paperless is running! :)</p>
|
||||||
<p i18n>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
|
<p i18n>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
|
||||||
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and have them displayed on the dashboard instead of this message.</p>
|
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and they will appear on the dashboard instead of this message.</p>
|
||||||
<p i18n>Paperless offers some more features that try to make your life easier, such as:</p>
|
<p i18n>Paperless offers some more features that try to make your life easier:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li i18n>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
|
<li i18n>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
|
||||||
<li i18n>You can configure paperless to read your mails and add documents from attached files.</li>
|
<li i18n>You can configure paperless to read your mails and add documents from attached files.</li>
|
||||||
|
@ -159,7 +159,7 @@ export class DocumentDetailComponent implements OnInit {
|
|||||||
delete() {
|
delete() {
|
||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
modal.componentInstance.title = $localize`Confirm delete`
|
modal.componentInstance.title = $localize`Confirm delete`
|
||||||
modal.componentInstance.messageBold = $localize`Do you really want to delete document '${this.document.title}'?`
|
modal.componentInstance.messageBold = $localize`Do you really want to delete document "${this.document.title}"?`
|
||||||
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
|
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
|
||||||
modal.componentInstance.btnClass = "btn-danger"
|
modal.componentInstance.btnClass = "btn-danger"
|
||||||
modal.componentInstance.btnCaption = $localize`Delete document`
|
modal.componentInstance.btnCaption = $localize`Delete document`
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let m of metadata">
|
<tr *ngFor="let m of metadata">
|
||||||
<td>{{m.prefix}}:{{m.key}}</td>
|
<td>{{m.prefix}}:{{m.key}}</td>
|
||||||
<td>{{m.value}}</td>
|
<td class="metadata-column">{{m.value}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.metadata-column {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
@ -26,7 +26,8 @@
|
|||||||
<div class="col-auto mb-2 mb-xl-0">
|
<div class="col-auto mb-2 mb-xl-0">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<label class="ml-auto mt-1 mb-0 mr-2" i18n>Edit:</label>
|
<label class="ml-auto mt-1 mb-0 mr-2" i18n>Edit:</label>
|
||||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill"
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||||
[items]="tags"
|
[items]="tags"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[multiple]="true"
|
[multiple]="true"
|
||||||
@ -35,7 +36,8 @@
|
|||||||
[(selectionModel)]="tagSelectionModel"
|
[(selectionModel)]="tagSelectionModel"
|
||||||
(apply)="setTags($event)">
|
(apply)="setTags($event)">
|
||||||
</app-filterable-dropdown>
|
</app-filterable-dropdown>
|
||||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill"
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||||
[items]="correspondents"
|
[items]="correspondents"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
@ -43,7 +45,8 @@
|
|||||||
[(selectionModel)]="correspondentSelectionModel"
|
[(selectionModel)]="correspondentSelectionModel"
|
||||||
(apply)="setCorrespondents($event)">
|
(apply)="setCorrespondents($event)">
|
||||||
</app-filterable-dropdown>
|
</app-filterable-dropdown>
|
||||||
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document Type" icon="file-earmark-fill"
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||||
[items]="documentTypes"
|
[items]="documentTypes"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
|
@ -100,10 +100,10 @@ export class BulkEditorComponent {
|
|||||||
} else if (items.length == 1) {
|
} else if (items.length == 1) {
|
||||||
return items[0].name
|
return items[0].name
|
||||||
} else if (items.length == 2) {
|
} else if (items.length == 2) {
|
||||||
return $localize`${items[0].name} and ${items[1].name}`
|
return $localize`:This is for messages like 'modify "tag1" and "tag2"':"${items[0].name}" and "${items[1].name}"`
|
||||||
} else {
|
} else {
|
||||||
let list = items.slice(0, items.length - 1).map(i => i.name).join($localize`, `)
|
let list = items.slice(0, items.length - 1).map(i => $localize`"${i.name}"`).join($localize`:this is used to separate enumerations and should probably be a comma and a whitespace in most languages:, `)
|
||||||
return $localize`${list} and ${items[items.length - 1].name}`
|
return $localize`:this is for messages like 'modify "tag1", "tag2" and "tag3"':${list} and "${items[items.length - 1].name}"`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,16 +115,16 @@ export class BulkEditorComponent {
|
|||||||
modal.componentInstance.title = $localize`Confirm tags assignment`
|
modal.componentInstance.title = $localize`Confirm tags assignment`
|
||||||
if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) {
|
if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) {
|
||||||
let tag = changedTags.itemsToAdd[0]
|
let tag = changedTags.itemsToAdd[0]
|
||||||
modal.componentInstance.message = $localize`This operation will add the tag ${tag.name} to all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.message = $localize`This operation will add the tag "${tag.name}" to ${this.list.selected.size} selected document(s).`
|
||||||
} else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) {
|
} else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) {
|
||||||
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to ${this.list.selected.size} selected document(s).`
|
||||||
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) {
|
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) {
|
||||||
let tag = changedTags.itemsToRemove[0]
|
let tag = changedTags.itemsToRemove[0]
|
||||||
modal.componentInstance.message = $localize`This operation will remove the tag ${tag.name} from all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.message = $localize`This operation will remove the tag "${tag.name}" from ${this.list.selected.size} selected document(s).`
|
||||||
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) {
|
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) {
|
||||||
modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from ${this.list.selected.size} selected document(s).`
|
||||||
} else {
|
} else {
|
||||||
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on all ${this.list.selected.size} selected document(s).`
|
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.btnClass = "btn-warning"
|
||||||
@ -156,9 +156,9 @@ export class BulkEditorComponent {
|
|||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
modal.componentInstance.title = $localize`Confirm correspondent assignment`
|
modal.componentInstance.title = $localize`Confirm correspondent assignment`
|
||||||
if (correspondent) {
|
if (correspondent) {
|
||||||
modal.componentInstance.message = $localize`This operation will assign the correspondent ${correspondent.name} to all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.message = $localize`This operation will assign the correspondent "${correspondent.name}" to ${this.list.selected.size} selected document(s).`
|
||||||
} else {
|
} else {
|
||||||
modal.componentInstance.message = $localize`This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.message = $localize`This operation will remove the correspondent from ${this.list.selected.size} selected document(s).`
|
||||||
}
|
}
|
||||||
modal.componentInstance.btnClass = "btn-warning"
|
modal.componentInstance.btnClass = "btn-warning"
|
||||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
@ -189,9 +189,9 @@ export class BulkEditorComponent {
|
|||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
modal.componentInstance.title = $localize`Confirm document type assignment`
|
modal.componentInstance.title = $localize`Confirm document type assignment`
|
||||||
if (documentType) {
|
if (documentType) {
|
||||||
modal.componentInstance.message = $localize`This operation will assign the document type ${documentType.name} to all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.message = $localize`This operation will assign the document type "${documentType.name}" to ${this.list.selected.size} selected document(s).`
|
||||||
} else {
|
} else {
|
||||||
modal.componentInstance.message = $localize`This operation will remove the document type from all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.message = $localize`This operation will remove the document type from ${this.list.selected.size} selected document(s).`
|
||||||
}
|
}
|
||||||
modal.componentInstance.btnClass = "btn-warning"
|
modal.componentInstance.btnClass = "btn-warning"
|
||||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
@ -217,7 +217,7 @@ export class BulkEditorComponent {
|
|||||||
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
|
||||||
modal.componentInstance.delayConfirm(5)
|
modal.componentInstance.delayConfirm(5)
|
||||||
modal.componentInstance.title = $localize`Delete confirm`
|
modal.componentInstance.title = $localize`Delete confirm`
|
||||||
modal.componentInstance.messageBold = $localize`This operation will permanently delete all ${this.list.selected.size} selected document(s).`
|
modal.componentInstance.messageBold = $localize`This operation will permanently delete ${this.list.selected.size} selected document(s).`
|
||||||
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
modal.componentInstance.message = $localize`This operation cannot be undone.`
|
||||||
modal.componentInstance.btnClass = "btn-danger"
|
modal.componentInstance.btnClass = "btn-danger"
|
||||||
modal.componentInstance.btnCaption = $localize`Delete document(s)`
|
modal.componentInstance.btnCaption = $localize`Delete document(s)`
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="card mb-3 bg-light shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<div class="col-md-2 d-none d-lg-block doc-img-background" [class.doc-img-background-selected]="selected">
|
<div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected">
|
||||||
<img [src]="getThumbUrl()" class="card-img doc-img border-right" (click)="setSelected(selectable ? !selected : false)">
|
<img [src]="getThumbUrl()" class="card-img doc-img border-right rounded-left" (click)="setSelected(selectable ? !selected : false)">
|
||||||
|
|
||||||
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
|
||||||
<div class="custom-control custom-checkbox">
|
<div class="custom-control custom-checkbox">
|
||||||
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card-body">
|
<div class="card-body bg-light">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
@ -58,11 +58,11 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="text-muted ml-auto" i18n>Score:</small>
|
<small *ngIf="searchScore" class="text-muted ml-auto" i18n>Score:</small>
|
||||||
|
|
||||||
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
|
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
|
||||||
|
|
||||||
<small class="text-muted" i18n>Created: {{document.created | date}}</small>
|
<small class="text-muted" [class.ml-auto]="!searchScore" i18n>Created: {{document.created | date}}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,10 +30,6 @@
|
|||||||
border-color: $primary;
|
border-color: $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-img-background {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-img-background-selected {
|
.doc-img-background-selected {
|
||||||
background-color: $primaryFaded;
|
background-color: $primaryFaded;
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
<div class="col p-2 h-100">
|
<div class="col p-2 h-100">
|
||||||
<div class="card h-100 shadow-sm" [class.card-selected]="selected">
|
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
|
||||||
<div class="border-bottom" [class.doc-img-background-selected]="selected">
|
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected">
|
||||||
<img class="card-img doc-img" [src]="getThumbUrl()" (click)="setSelected(!selected)">
|
<img class="card-img doc-img rounded-top" [src]="getThumbUrl()" (click)="setSelected(!selected)">
|
||||||
|
|
||||||
<div class="border-right border-bottom bg-light p-1 rounded document-card-check">
|
<div class="border-right border-bottom bg-light p-1 rounded document-card-check">
|
||||||
<div class="custom-control custom-checkbox">
|
<div class="custom-control custom-checkbox">
|
||||||
|
@ -78,13 +78,13 @@
|
|||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<div class="w-100 mb-2 mb-sm-4">
|
<div class="w-100 mb-2 mb-sm-4">
|
||||||
<app-filter-editor *ngIf="!isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
|
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
|
||||||
<app-bulk-editor *ngIf="isBulkEditing"></app-bulk-editor>
|
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<p i18n *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of {{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}</p>
|
<p i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</p>
|
||||||
<p *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {1 document} other {{{list.collectionSize || 0}} documents}}</p>
|
<p i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</p>
|
||||||
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
<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" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,11 +8,35 @@
|
|||||||
<div class="w-100 d-xl-none"></div>
|
<div class="w-100 d-xl-none"></div>
|
||||||
<div class="col col-xl-auto mb-2 mb-xl-0">
|
<div class="col col-xl-auto mb-2 mb-xl-0">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<app-filterable-dropdown class="mr-2 mr-md-3" [items]="tags" [(selectionModel)]="tagSelectionModel" (selectionModelChange)="updateRules()" [multiple]="true" [allowSelectNone]="true" title="Tags" icon="tag-fill" i18n-title></app-filterable-dropdown>
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" i18n-title
|
||||||
<app-filterable-dropdown class="mr-2 mr-md-3" [items]="correspondents" [(selectionModel)]="correspondentSelectionModel" (selectionModelChange)="updateRules()" [allowSelectNone]="true" title="Correspondents" icon="person-fill" i18n-title></app-filterable-dropdown>
|
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||||
<app-filterable-dropdown class="mr-2 mr-md-3" [items]="documentTypes" [(selectionModel)]="documentTypeSelectionModel" (selectionModelChange)="updateRules()" [allowSelectNone]="true" title="Document types" icon="file-earmark-fill" i18n-title></app-filterable-dropdown>
|
[items]="tags"
|
||||||
<app-date-dropdown class="mr-2 mr-md-3" [(dateBefore)]="dateCreatedBefore" [(dateAfter)]="dateCreatedAfter" title="Created" (datesSet)="updateRules()" i18n-title></app-date-dropdown>
|
[(selectionModel)]="tagSelectionModel"
|
||||||
<app-date-dropdown [(dateBefore)]="dateAddedBefore" [(dateAfter)]="dateAddedAfter" title="Added" (datesSet)="updateRules()" i18n-title></app-date-dropdown>
|
(selectionModelChange)="updateRules()"
|
||||||
|
[multiple]="true"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||||
|
[items]="correspondents"
|
||||||
|
[(selectionModel)]="correspondentSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-filterable-dropdown class="mr-2 mr-md-3" title="Document type" icon="file-earmark-fill" i18n-title
|
||||||
|
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||||
|
[items]="documentTypes"
|
||||||
|
[(selectionModel)]="documentTypeSelectionModel"
|
||||||
|
(selectionModelChange)="updateRules()"
|
||||||
|
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||||
|
<app-date-dropdown class="mr-2 mr-md-3"
|
||||||
|
title="Created" i18n-title
|
||||||
|
(datesSet)="updateRules()"
|
||||||
|
[(dateBefore)]="dateCreatedBefore"
|
||||||
|
[(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
|
||||||
|
<app-date-dropdown
|
||||||
|
[(dateBefore)]="dateAddedBefore"
|
||||||
|
[(dateAfter)]="dateAddedAfter"
|
||||||
|
title="Added" i18n-title
|
||||||
|
(datesSet)="updateRules()"></app-date-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100 d-xl-none"></div>
|
<div class="w-100 d-xl-none"></div>
|
||||||
|
@ -25,14 +25,27 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
switch(this.filterRules[0].rule_type) {
|
switch(this.filterRules[0].rule_type) {
|
||||||
|
|
||||||
case FILTER_CORRESPONDENT:
|
case FILTER_CORRESPONDENT:
|
||||||
|
if (rule.value) {
|
||||||
return $localize`Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
|
return $localize`Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
|
||||||
|
} else {
|
||||||
|
return $localize`Without correspondent`
|
||||||
|
}
|
||||||
|
|
||||||
case FILTER_DOCUMENT_TYPE:
|
case FILTER_DOCUMENT_TYPE:
|
||||||
|
if (rule.value) {
|
||||||
return $localize`Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
|
return $localize`Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
|
||||||
|
} else {
|
||||||
|
return $localize`Without document type`
|
||||||
|
}
|
||||||
|
|
||||||
case FILTER_HAS_TAG:
|
case FILTER_HAS_TAG:
|
||||||
return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
|
return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
|
||||||
|
|
||||||
|
case FILTER_HAS_ANY_TAG:
|
||||||
|
if (rule.value == "false") {
|
||||||
|
return $localize`Without any tag`
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +78,11 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
|||||||
this.documentTypeSelectionModel.clear(false)
|
this.documentTypeSelectionModel.clear(false)
|
||||||
this.tagSelectionModel.clear(false)
|
this.tagSelectionModel.clear(false)
|
||||||
this.correspondentSelectionModel.clear(false)
|
this.correspondentSelectionModel.clear(false)
|
||||||
|
this._titleFilter = null
|
||||||
|
this.dateAddedBefore = null
|
||||||
|
this.dateAddedAfter = null
|
||||||
|
this.dateCreatedBefore = null
|
||||||
|
this.dateCreatedAfter = null
|
||||||
|
|
||||||
value.forEach(rule => {
|
value.forEach(rule => {
|
||||||
switch (rule.rule_type) {
|
switch (rule.rule_type) {
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
|
||||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text>
|
||||||
<app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type';
|
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type';
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||||
@ -16,20 +15,16 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
|
|||||||
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
|
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
|
||||||
|
|
||||||
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
|
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
|
||||||
private router: Router,
|
|
||||||
private list: DocumentListViewService
|
private list: DocumentListViewService
|
||||||
) {
|
) {
|
||||||
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
|
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDeleteMessage(object: PaperlessCorrespondent) {
|
getDeleteMessage(object: PaperlessCorrespondent) {
|
||||||
return $localize`Do you really want to delete the correspondent ${object.name}?`
|
return $localize`Do you really want to delete the correspondent "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
filterDocuments(object: PaperlessCorrespondent) {
|
filterDocuments(object: PaperlessCorrespondent) {
|
||||||
this.list.documentListView.filter_rules = [
|
this.list.quickFilter([{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}])
|
||||||
{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}
|
|
||||||
]
|
|
||||||
this.router.navigate(["documents"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
|
<app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
|
||||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text>
|
||||||
<app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type';
|
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type';
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||||
@ -16,21 +15,17 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
|
|||||||
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
|
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
|
||||||
|
|
||||||
constructor(service: DocumentTypeService, modalService: NgbModal,
|
constructor(service: DocumentTypeService, modalService: NgbModal,
|
||||||
private router: Router,
|
|
||||||
private list: DocumentListViewService
|
private list: DocumentListViewService
|
||||||
) {
|
) {
|
||||||
super(service, modalService, DocumentTypeEditDialogComponent)
|
super(service, modalService, DocumentTypeEditDialogComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDeleteMessage(object: PaperlessDocumentType) {
|
getDeleteMessage(object: PaperlessDocumentType) {
|
||||||
return $localize`Do you really want to delete the document type ${object.name}?`
|
return $localize`Do you really want to delete the document type "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
filterDocuments(object: PaperlessDocumentType) {
|
filterDocuments(object: PaperlessDocumentType) {
|
||||||
this.list.documentListView.filter_rules = [
|
this.list.quickFilter([{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}])
|
||||||
{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}
|
|
||||||
]
|
|
||||||
this.router.navigate(["documents"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
if (o.matching_algorithm == MATCH_AUTO) {
|
if (o.matching_algorithm == MATCH_AUTO) {
|
||||||
return $localize`Automatic`
|
return $localize`Automatic`
|
||||||
} else if (o.match && o.match.length > 0) {
|
} else if (o.match && o.match.length > 0) {
|
||||||
return `${o.match} (${MATCHING_ALGORITHMS.find(a => a.id == o.matching_algorithm).name})`
|
return `${MATCHING_ALGORITHMS.find(a => a.id == o.matching_algorithm).shortName}: ${o.match}`
|
||||||
} else {
|
} else {
|
||||||
return "-"
|
return "-"
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<app-page-header title="Settings">
|
<app-page-header title="Settings" i18n-title>
|
||||||
|
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<a ngbNavLink i18n>General settings</a>
|
<a ngbNavLink i18n>General settings</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
<h4 i18n>Document list</h4>
|
<h4 i18n>Appearance</h4>
|
||||||
|
|
||||||
<div class="form-row form-group">
|
<div class="form-row form-group">
|
||||||
<div class="col-md-3 col-form-label">
|
<div class="col-md-3 col-form-label">
|
||||||
@ -26,14 +26,29 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 i18n>Bulk editing</h4>
|
<div class="form-row form-group">
|
||||||
|
<div class="col-md-3 col-form-label">
|
||||||
|
<span i18n>Dark mode</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<app-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem" (change)="toggleDarkModeSetting()"></app-input-check>
|
||||||
|
<div class="custom-control custom-switch" *ngIf="!settingsForm.value.darkModeUseSystem">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="darkModeEnabled" formControlName="darkModeEnabled" [checked]="settingsForm.value.darkModeEnabled">
|
||||||
|
<label class="custom-control-label" for="darkModeEnabled">Enabled</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="mt-4" i18n>Bulk editing</h4>
|
||||||
|
|
||||||
|
<div class="form-row form-group">
|
||||||
|
<div class="offset-md-3 col">
|
||||||
<app-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></app-input-check>
|
<app-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></app-input-check>
|
||||||
<app-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></app-input-check>
|
<app-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></app-input-check>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, Renderer2 } from '@angular/core';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||||
@ -19,9 +19,13 @@ export class SettingsComponent implements OnInit {
|
|||||||
'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)),
|
'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)),
|
||||||
'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)),
|
'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)),
|
||||||
'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)),
|
'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)),
|
||||||
|
'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)),
|
||||||
|
'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)),
|
||||||
'savedViews': this.savedViewGroup
|
'savedViews': this.savedViewGroup
|
||||||
})
|
})
|
||||||
|
|
||||||
|
savedViews: PaperlessSavedView[]
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public savedViewService: SavedViewService,
|
public savedViewService: SavedViewService,
|
||||||
private documentListViewService: DocumentListViewService,
|
private documentListViewService: DocumentListViewService,
|
||||||
@ -29,8 +33,6 @@ export class SettingsComponent implements OnInit {
|
|||||||
private settings: SettingsService
|
private settings: SettingsService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
savedViews: PaperlessSavedView[]
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.savedViewService.listAll().subscribe(r => {
|
this.savedViewService.listAll().subscribe(r => {
|
||||||
this.savedViews = r.results
|
this.savedViews = r.results
|
||||||
@ -49,15 +51,26 @@ export class SettingsComponent implements OnInit {
|
|||||||
this.savedViewService.delete(savedView).subscribe(() => {
|
this.savedViewService.delete(savedView).subscribe(() => {
|
||||||
this.savedViewGroup.removeControl(savedView.id.toString())
|
this.savedViewGroup.removeControl(savedView.id.toString())
|
||||||
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
|
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
|
||||||
this.toastService.showInfo($localize`Saved view "${savedView.name} deleted.`)
|
this.toastService.showInfo($localize`Saved view "${savedView.name}" deleted.`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleDarkModeSetting() {
|
||||||
|
if (this.settingsForm.value.darkModeUseSystem) {
|
||||||
|
(this.settingsForm.controls.darkModeEnabled as FormControl).disable()
|
||||||
|
} else {
|
||||||
|
(this.settingsForm.controls.darkModeEnabled as FormControl).enable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private saveLocalSettings() {
|
private saveLocalSettings() {
|
||||||
this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose)
|
this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose)
|
||||||
this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs)
|
this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs)
|
||||||
this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
|
this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
|
||||||
|
this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem)
|
||||||
|
this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString())
|
||||||
this.documentListViewService.updatePageSize()
|
this.documentListViewService.updatePageSize()
|
||||||
|
this.settings.updateDarkModeSettings()
|
||||||
this.toastService.showInfo($localize`Settings saved successfully.`)
|
this.toastService.showInfo($localize`Settings saved successfully.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
<app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
|
<app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
|
||||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||||
<app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
|
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text>
|
||||||
<app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
|
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
|
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type';
|
import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type';
|
||||||
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
|
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
|
||||||
@ -16,7 +15,6 @@ import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.compon
|
|||||||
export class TagListComponent extends GenericListComponent<PaperlessTag> {
|
export class TagListComponent extends GenericListComponent<PaperlessTag> {
|
||||||
|
|
||||||
constructor(tagService: TagService, modalService: NgbModal,
|
constructor(tagService: TagService, modalService: NgbModal,
|
||||||
private router: Router,
|
|
||||||
private list: DocumentListViewService
|
private list: DocumentListViewService
|
||||||
) {
|
) {
|
||||||
super(tagService, modalService, TagEditDialogComponent)
|
super(tagService, modalService, TagEditDialogComponent)
|
||||||
@ -27,13 +25,11 @@ export class TagListComponent extends GenericListComponent<PaperlessTag> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDeleteMessage(object: PaperlessTag) {
|
getDeleteMessage(object: PaperlessTag) {
|
||||||
return $localize`Do you really want to delete the tag ${object.name}?`
|
return $localize`Do you really want to delete the tag "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
filterDocuments(object: PaperlessTag) {
|
filterDocuments(object: PaperlessTag) {
|
||||||
this.list.documentListView.filter_rules = [
|
this.list.quickFilter([{rule_type: FILTER_HAS_TAG, value: object.id.toString()}])
|
||||||
{rule_type: FILTER_HAS_TAG, value: object.id.toString()}
|
|
||||||
]
|
|
||||||
this.router.navigate(["documents"])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,12 @@ export const MATCH_FUZZY = 5
|
|||||||
export const MATCH_AUTO = 6
|
export const MATCH_AUTO = 6
|
||||||
|
|
||||||
export const MATCHING_ALGORITHMS = [
|
export const MATCHING_ALGORITHMS = [
|
||||||
{id: MATCH_ANY, name: $localize`Any`},
|
{id: MATCH_ANY, shortName: $localize`Any word`, name: $localize`Any: Document contains any of these words (space separated)`},
|
||||||
{id: MATCH_ALL, name: $localize`All`},
|
{id: MATCH_ALL, shortName: $localize`All words`, name: $localize`All: Document contains all of these words (space separated)`},
|
||||||
{id: MATCH_LITERAL, name: $localize`Literal`},
|
{id: MATCH_LITERAL, shortName: $localize`Exact match`, name: $localize`Exact: Document contains this string`},
|
||||||
{id: MATCH_REGEX, name: $localize`Regular expression`},
|
{id: MATCH_REGEX, shortName: $localize`Regular expression`, name: $localize`Regular expression: Document matches this regular expression`},
|
||||||
{id: MATCH_FUZZY, name: $localize`Fuzzy match`},
|
{id: MATCH_FUZZY, shortName: $localize`Fuzzy word`, name: $localize`Fuzzy: Document contains a word similar to this word`},
|
||||||
{id: MATCH_AUTO, name: $localize`Auto`},
|
{id: MATCH_AUTO, shortName: $localize`Automatic`, name: $localize`Auto: Learn matching automatically`},
|
||||||
]
|
]
|
||||||
|
|
||||||
export interface MatchingModel extends ObjectWithId {
|
export interface MatchingModel extends ObjectWithId {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { cloneFilterRules, FilterRule } from '../data/filter-rule';
|
import { cloneFilterRules, FilterRule } from '../data/filter-rule';
|
||||||
import { PaperlessDocument } from '../data/paperless-document';
|
import { PaperlessDocument } from '../data/paperless-document';
|
||||||
@ -155,6 +156,14 @@ export class DocumentListViewService {
|
|||||||
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
|
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quickFilter(filterRules: FilterRule[]) {
|
||||||
|
this.savedView = null
|
||||||
|
this.view.filter_rules = filterRules
|
||||||
|
this.reduceSelectionToFilter()
|
||||||
|
this.saveDocumentListView()
|
||||||
|
this.router.navigate(["documents"])
|
||||||
|
}
|
||||||
|
|
||||||
getLastPage(): number {
|
getLastPage(): number {
|
||||||
return Math.ceil(this.collectionSize / this.currentPageSize)
|
return Math.ceil(this.collectionSize / this.currentPageSize)
|
||||||
}
|
}
|
||||||
@ -240,7 +249,7 @@ export class DocumentListViewService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private documentService: DocumentService, private settings: SettingsService) {
|
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) {
|
if (documentListViewConfigJson) {
|
||||||
try {
|
try {
|
||||||
|
@ -28,6 +28,9 @@ export class OpenDocumentsService {
|
|||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.documentService.get(id).subscribe(doc => {
|
this.documentService.get(id).subscribe(doc => {
|
||||||
this.openDocuments[index] = doc
|
this.openDocuments[index] = doc
|
||||||
|
}, error => {
|
||||||
|
this.openDocuments.splice(index, 1)
|
||||||
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
|
||||||
|
|
||||||
export interface PaperlessSettings {
|
export interface PaperlessSettings {
|
||||||
key: string
|
key: string
|
||||||
@ -10,12 +11,16 @@ export const SETTINGS_KEYS = {
|
|||||||
BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs',
|
BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs',
|
||||||
BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close',
|
BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close',
|
||||||
DOCUMENT_LIST_SIZE: 'general-settings:documentListSize',
|
DOCUMENT_LIST_SIZE: 'general-settings:documentListSize',
|
||||||
|
DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system',
|
||||||
|
DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled'
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS: PaperlessSettings[] = [
|
const SETTINGS: PaperlessSettings[] = [
|
||||||
{key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: "boolean", default: true},
|
{key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: "boolean", default: true},
|
||||||
{key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, type: "boolean", default: false},
|
{key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, type: "boolean", default: false},
|
||||||
{key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50}
|
{key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50},
|
||||||
|
{key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: "boolean", default: true},
|
||||||
|
{key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: "boolean", default: false}
|
||||||
]
|
]
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@ -23,7 +28,30 @@ const SETTINGS: PaperlessSettings[] = [
|
|||||||
})
|
})
|
||||||
export class SettingsService {
|
export class SettingsService {
|
||||||
|
|
||||||
constructor() { }
|
private renderer: Renderer2;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private rendererFactory: RendererFactory2,
|
||||||
|
@Inject(DOCUMENT) private document
|
||||||
|
) {
|
||||||
|
this.renderer = rendererFactory.createRenderer(null, null);
|
||||||
|
|
||||||
|
this.updateDarkModeSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDarkModeSettings(): void {
|
||||||
|
let darkModeUseSystem = this.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)
|
||||||
|
let darkModeEnabled = this.get(SETTINGS_KEYS.DARK_MODE_ENABLED)
|
||||||
|
|
||||||
|
if (darkModeUseSystem) {
|
||||||
|
this.renderer.addClass(this.document.body, 'color-scheme-system')
|
||||||
|
this.renderer.removeClass(this.document.body, 'color-scheme-dark')
|
||||||
|
} else {
|
||||||
|
this.renderer.removeClass(this.document.body, 'color-scheme-system')
|
||||||
|
darkModeEnabled ? this.renderer.addClass(this.document.body, 'color-scheme-dark') : this.renderer.removeClass(this.document.body, 'color-scheme-dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
get(key: string): any {
|
get(key: string): any {
|
||||||
let setting = SETTINGS.find(s => s.key == key)
|
let setting = SETTINGS.find(s => s.key == key)
|
||||||
|
@ -1,69 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<svg
|
<!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
<svg version="1.1"
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
id="svg4812" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="logo-dark-notext.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 198.4 238.9"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
style="enable-background:new 0 0 198.4 238.9;" xml:space="preserve">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="SvgjsG1020" inkscape:cx="328.04904" inkscape:cy="330.33332" inkscape:document-rotation="0" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1280" inkscape:window-y="27" inkscape:zoom="0.98994949" pagecolor="#ffffff" showgrid="false">
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
</sodipodi:namedview>
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
<g id="layer1" transform="translate(-9.9999792,-10.000082)" inkscape:groupmode="layer" inkscape:label="Layer 1">
|
||||||
width="69.999977mm"
|
<g id="SvgjsG1020" transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)">
|
||||||
height="84.283669mm"
|
<path id="path57" d="M1967.5,16C1672.7,702,255.4,787,709,1892.5c5.7,14.2-104.9,164.4-178.6,289.1c-17-62.4-36.9-130.4-34-136.1
|
||||||
viewBox="0 0 69.999977 84.283669"
|
c368.5-436.5-263.6-683.1-297.6-1040.3C40,1288.7-16.7,1784.8,462.3,2071.1c2.8,0,25.5,107.7,36.9,161.6
|
||||||
version="1.1"
|
c-11.3,22.7-22.7,45.4-28.3,62.4c-11.3,28.3,73.7,25.5,73.7,31.2c8.5-2.8,209.8-357.2,215.4-360
|
||||||
id="svg4812"
|
C1899.5,1705.4,2100.8,679.3,1967.5,16z M1386.4,738.8C853.5,1215,762.8,1569.4,779.8,1742.3
|
||||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
C601.2,1319.9,1125.7,855,1386.4,738.8z M357.5,1419.1c102,93.5,272.1,379.8,127.6,547.1C519,1889.7,530.4,1716.8,357.5,1419.1z"
|
||||||
sodipodi:docname="logo-dark-notext.svg">
|
/>
|
||||||
<defs
|
|
||||||
id="defs4806" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="0.98994949"
|
|
||||||
inkscape:cx="328.04904"
|
|
||||||
inkscape:cy="330.33332"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="SvgjsG1020"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1016"
|
|
||||||
inkscape:window-x="1280"
|
|
||||||
inkscape:window-y="27"
|
|
||||||
inkscape:window-maximized="1" />
|
|
||||||
<metadata
|
|
||||||
id="metadata4809">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-9.9999792,-10.000082)">
|
|
||||||
<g
|
|
||||||
id="SvgjsG1020"
|
|
||||||
featureKey="symbol1"
|
|
||||||
fill="#ffffff"
|
|
||||||
transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)">
|
|
||||||
<path
|
|
||||||
id="path57"
|
|
||||||
style="fill:#ffffff;stroke-width:1.10017"
|
|
||||||
d="M 752.4375,82.365234 C 638.02019,348.60552 87.938206,381.6089 263.96484,810.67383 c 2.20034,5.50083 -40.70621,63.80947 -69.31054,112.21679 -6.601,-24.20366 -14.30329,-50.6063 -13.20313,-52.80664 C 324.47281,700.65835 79.135592,604.94324 65.933594,466.32227 4.3242706,576.33891 -17.678136,768.86756 168.25,879.98438 c 1.10017,-10e-6 9.90207,41.80777 14.30273,62.71093 -4.40066,8.80133 -8.80162,17.60213 -11.00195,24.20313 -4.40066,11.00166 28.60352,9.90123 28.60352,12.10156 3.3005,-1.10017 81.41295,-138.62054 83.61328,-139.7207 C 726.0345,738.06398 804.14532,339.80419 752.4375,82.365234 Z M 526.9043,362.90625 C 320.073,547.73422 284.86775,685.25508 291.46875,752.36523 222.15826,588.44043 425.68898,408.01308 526.9043,362.90625 Z M 127.54297,626.94727 c 39.60599,36.30549 105.6163,147.4222 49.50781,212.33203 13.202,-29.7045 17.60234,-96.81455 -49.50781,-212.33203 z"
|
|
||||||
transform="matrix(0.90895334,0,0,0.90895334,65.06894,-58.865357)" />
|
|
||||||
<defs
|
|
||||||
id="defs14302" />
|
|
||||||
</g>
|
|
||||||
</g>
|
</g>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.0 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.8 KiB |
69
src-ui/src/assets/logo-white-notext.svg
Normal file
69
src-ui/src/assets/logo-white-notext.svg
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="69.999977mm"
|
||||||
|
height="84.283669mm"
|
||||||
|
viewBox="0 0 69.999977 84.283669"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4812"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||||
|
sodipodi:docname="logo-dark-notext.svg">
|
||||||
|
<defs
|
||||||
|
id="defs4806" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.98994949"
|
||||||
|
inkscape:cx="328.04904"
|
||||||
|
inkscape:cy="330.33332"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="SvgjsG1020"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1016"
|
||||||
|
inkscape:window-x="1280"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata4809">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(-9.9999792,-10.000082)">
|
||||||
|
<g
|
||||||
|
id="SvgjsG1020"
|
||||||
|
featureKey="symbol1"
|
||||||
|
fill="#ffffff"
|
||||||
|
transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)">
|
||||||
|
<path
|
||||||
|
id="path57"
|
||||||
|
style="fill:#ffffff;stroke-width:1.10017"
|
||||||
|
d="M 752.4375,82.365234 C 638.02019,348.60552 87.938206,381.6089 263.96484,810.67383 c 2.20034,5.50083 -40.70621,63.80947 -69.31054,112.21679 -6.601,-24.20366 -14.30329,-50.6063 -13.20313,-52.80664 C 324.47281,700.65835 79.135592,604.94324 65.933594,466.32227 4.3242706,576.33891 -17.678136,768.86756 168.25,879.98438 c 1.10017,-10e-6 9.90207,41.80777 14.30273,62.71093 -4.40066,8.80133 -8.80162,17.60213 -11.00195,24.20313 -4.40066,11.00166 28.60352,9.90123 28.60352,12.10156 3.3005,-1.10017 81.41295,-138.62054 83.61328,-139.7207 C 726.0345,738.06398 804.14532,339.80419 752.4375,82.365234 Z M 526.9043,362.90625 C 320.073,547.73422 284.86775,685.25508 291.46875,752.36523 222.15826,588.44043 425.68898,408.01308 526.9043,362.90625 Z M 127.54297,626.94727 c 39.60599,36.30549 105.6163,147.4222 49.50781,212.33203 13.202,-29.7045 17.60234,-96.81455 -49.50781,-212.33203 z"
|
||||||
|
transform="matrix(0.90895334,0,0,0.90895334,65.06894,-58.865357)" />
|
||||||
|
<defs
|
||||||
|
id="defs14302" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
@ -2,5 +2,5 @@ export const environment = {
|
|||||||
production: true,
|
production: true,
|
||||||
apiBaseUrl: "/api/",
|
apiBaseUrl: "/api/",
|
||||||
appTitle: "Paperless-ng",
|
appTitle: "Paperless-ng",
|
||||||
version: "0.9.10"
|
version: "0.9.11"
|
||||||
};
|
};
|
||||||
|
@ -5,9 +5,12 @@
|
|||||||
<title>Paperless-ng</title>
|
<title>Paperless-ng</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="color-scheme" content="dark light">
|
||||||
|
<meta name="theme-color" content="#17541f" />
|
||||||
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="color-scheme-system">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
1907
src-ui/src/locale/messages.de.xlf
Normal file
1907
src-ui/src/locale/messages.de.xlf
Normal file
File diff suppressed because it is too large
Load Diff
14
src-ui/src/manifest.webmanifest
Normal file
14
src-ui/src/manifest.webmanifest
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"background_color": "white",
|
||||||
|
"description": "A supercharged version of paperless: scan, index and archive all your physical documents",
|
||||||
|
"display": "fullscreen",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "128x128"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Paperless NG",
|
||||||
|
"short_name": "Paperless NG",
|
||||||
|
"start_url": "/"
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
@import "theme";
|
@import "theme";
|
||||||
|
@import "theme_dark";
|
||||||
@import "node_modules/bootstrap/scss/bootstrap";
|
@import "node_modules/bootstrap/scss/bootstrap";
|
||||||
@import "~@ng-select/ng-select/themes/default.theme.css";
|
@import "~@ng-select/ng-select/themes/default.theme.css";
|
||||||
|
|
||||||
|
337
src-ui/src/theme_dark.scss
Normal file
337
src-ui/src/theme_dark.scss
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
$primary-dark-mode: #45973a;
|
||||||
|
$danger-dark-mode: #b71631;
|
||||||
|
$bg-dark-mode: #161618;
|
||||||
|
$bg-light-dark-mode: #1c1c1f;
|
||||||
|
$text-color-dark-mode: #abb2bf;
|
||||||
|
$text-color-dark-mode-accent: lighten($text-color-dark-mode, 10%);
|
||||||
|
$border-color-dark-mode: #47494f;
|
||||||
|
|
||||||
|
* {
|
||||||
|
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark-mode {
|
||||||
|
background-color: $bg-dark-mode !important;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.logo {
|
||||||
|
.leaf {
|
||||||
|
color: $primary-dark-mode !important;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
fill: $text-color-dark-mode !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-light {
|
||||||
|
background-color: $bg-light-dark-mode !important;
|
||||||
|
|
||||||
|
a,
|
||||||
|
div {
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-light {
|
||||||
|
color: $text-color-dark-mode !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border-color: $border-color-dark-mode !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-right {
|
||||||
|
border-right: 1px solid $border-color-dark-mode !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-left {
|
||||||
|
border-left: 1px solid $border-color-dark-mode !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-bottom {
|
||||||
|
border-bottom: 1px solid $border-color-dark-mode !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: $text-color-dark-mode !important;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $bg-dark-mode;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode $border-color-dark-mode $bg-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $text-color-dark-mode-accent !important;
|
||||||
|
border-color: $border-color-dark-mode $border-color-dark-mode $bg-dark-mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: $primary-dark-mode !important;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $text-color-dark-mode !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: $bg-dark-mode;
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.disabled {
|
||||||
|
color: darken($text-color-dark-mode, 20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dark {
|
||||||
|
color: $text-color-dark-mode !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content, .modal-header, .modal-body, .modal-footer {
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-tag .badge {
|
||||||
|
filter: brightness(.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-light {
|
||||||
|
background-color: darken($bg-dark-mode, 20%);
|
||||||
|
color: $text-color-dark-mode-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-img-container {
|
||||||
|
border: none !important;
|
||||||
|
border-top-left-radius: .25rem;
|
||||||
|
border-top-right-radius: .25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-img {
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
filter: invert(95%) hue-rotate(180deg);
|
||||||
|
border-radius: 0;
|
||||||
|
border-color: $bg-dark-mode;
|
||||||
|
|
||||||
|
&.border-right {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-selected .doc-img {
|
||||||
|
mix-blend-mode: luminosity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background-color: opacify($bg-light-dark-mode, .85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-header {
|
||||||
|
background-color: opacify($bg-dark-mode, .85);
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.card-title a {
|
||||||
|
color: $primary-dark-mode;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: lighten($primary, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
background-color: $bg-dark-mode;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
color: $text-color-dark-mode-accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td,
|
||||||
|
.table th {
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-selected {
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
text-shadow: 0 1px 0 #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary {
|
||||||
|
border-color: $primary-dark-mode;
|
||||||
|
color: $primary-dark-mode;
|
||||||
|
|
||||||
|
&:not(:disabled):not(.disabled).active,
|
||||||
|
&:not(:disabled):not(.disabled):hover {
|
||||||
|
background-color: darken($primary-dark-mode, 10%);
|
||||||
|
border-color: darken($primary-dark-mode, 10%);
|
||||||
|
color: $text-color-dark-mode-accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary {
|
||||||
|
border-color: $text-color-dark-mode;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
|
||||||
|
&:not(:disabled):not(.disabled):hover {
|
||||||
|
background-color: $bg-dark-mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-danger {
|
||||||
|
border-color: $danger-dark-mode;
|
||||||
|
color: $danger-dark-mode;
|
||||||
|
|
||||||
|
&:not(:disabled):not(.disabled):hover {
|
||||||
|
background-color: darken($danger-dark-mode, 10%);
|
||||||
|
border-color: darken($danger-dark-mode, 10%);
|
||||||
|
color: $text-color-dark-mode-accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-dark {
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
|
||||||
|
&:not(:disabled):not(.disabled):hover {
|
||||||
|
color: $text-color-dark-mode-accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link:not(:disabled):not(.disabled) {
|
||||||
|
color: $primary-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link:hover,
|
||||||
|
.btn-outline-primary:not(:disabled):not(.disabled).active,
|
||||||
|
.btn-outline-primary:not(:disabled):not(.disabled):active,
|
||||||
|
.show > .btn-outline-primary.dropdown-toggle {
|
||||||
|
color: $text-color-dark-mode-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.bg-light:hover {
|
||||||
|
background-color: $bg-dark-mode !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
background-color: $bg-dark-mode;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: $bg-light-dark-mode !important;
|
||||||
|
color: darken($text-color-dark-mode, 10%) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ng-select-container,
|
||||||
|
.ng-select.ng-select-opened > .ng-select-container,
|
||||||
|
.ng-dropdown-panel,
|
||||||
|
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
|
||||||
|
background-color: $bg-dark-mode;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option:hover {
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-control-label:before {
|
||||||
|
background-color: $bg-dark-mode;
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-control-input:checked ~ .custom-control-label::before {
|
||||||
|
color: $text-color-dark-mode-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
color: $text-color-dark-mode;
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.disabled .page-link {
|
||||||
|
background-color: $bg-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item,
|
||||||
|
.page-link {
|
||||||
|
background-color: $bg-light-dark-mode;
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.active .page-link {
|
||||||
|
border-color: $border-color-dark-mode;
|
||||||
|
color: $text-color-dark-mode-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background-color: $border-color-dark-mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.color-scheme-dark {
|
||||||
|
@include dark-mode;
|
||||||
|
}
|
||||||
|
body.color-scheme-system {
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
@include dark-mode;
|
||||||
|
}
|
||||||
|
}
|
@ -1,34 +1,30 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class DocumentsConfig(AppConfig):
|
class DocumentsConfig(AppConfig):
|
||||||
|
|
||||||
name = "documents"
|
name = "documents"
|
||||||
|
|
||||||
def ready(self):
|
verbose_name = _("Documents")
|
||||||
|
|
||||||
from .signals import document_consumption_started
|
def ready(self):
|
||||||
from .signals import document_consumption_finished
|
from .signals import document_consumption_finished
|
||||||
from .signals.handlers import (
|
from .signals.handlers import (
|
||||||
add_inbox_tags,
|
add_inbox_tags,
|
||||||
run_pre_consume_script,
|
|
||||||
run_post_consume_script,
|
|
||||||
set_log_entry,
|
set_log_entry,
|
||||||
set_correspondent,
|
set_correspondent,
|
||||||
set_document_type,
|
set_document_type,
|
||||||
set_tags,
|
set_tags,
|
||||||
add_to_index
|
add_to_index
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
document_consumption_started.connect(run_pre_consume_script)
|
|
||||||
|
|
||||||
document_consumption_finished.connect(add_inbox_tags)
|
document_consumption_finished.connect(add_inbox_tags)
|
||||||
document_consumption_finished.connect(set_correspondent)
|
document_consumption_finished.connect(set_correspondent)
|
||||||
document_consumption_finished.connect(set_document_type)
|
document_consumption_finished.connect(set_document_type)
|
||||||
document_consumption_finished.connect(set_tags)
|
document_consumption_finished.connect(set_tags)
|
||||||
document_consumption_finished.connect(set_log_entry)
|
document_consumption_finished.connect(set_log_entry)
|
||||||
document_consumption_finished.connect(add_to_index)
|
document_consumption_finished.connect(add_to_index)
|
||||||
document_consumption_finished.connect(run_post_consume_script)
|
|
||||||
|
|
||||||
AppConfig.ready(self)
|
AppConfig.ready(self)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
import magic
|
import magic
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -9,6 +9,7 @@ from django.db import transaction
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from .classifier import DocumentClassifier, IncompatibleClassifierVersionError
|
from .classifier import DocumentClassifier, IncompatibleClassifierVersionError
|
||||||
from .file_handling import create_source_path_directory, \
|
from .file_handling import create_source_path_directory, \
|
||||||
@ -66,6 +67,39 @@ class Consumer(LoggingMixin):
|
|||||||
os.makedirs(settings.ORIGINALS_DIR, exist_ok=True)
|
os.makedirs(settings.ORIGINALS_DIR, exist_ok=True)
|
||||||
os.makedirs(settings.ARCHIVE_DIR, exist_ok=True)
|
os.makedirs(settings.ARCHIVE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
def run_pre_consume_script(self):
|
||||||
|
if not settings.PRE_CONSUME_SCRIPT:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
Popen((settings.PRE_CONSUME_SCRIPT, self.path)).wait()
|
||||||
|
except Exception as e:
|
||||||
|
raise ConsumerError(
|
||||||
|
f"Error while executing pre-consume script: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_post_consume_script(self, document):
|
||||||
|
if not settings.POST_CONSUME_SCRIPT:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
Popen((
|
||||||
|
settings.POST_CONSUME_SCRIPT,
|
||||||
|
str(document.pk),
|
||||||
|
document.get_public_filename(),
|
||||||
|
os.path.normpath(document.source_path),
|
||||||
|
os.path.normpath(document.thumbnail_path),
|
||||||
|
reverse("document-download", kwargs={"pk": document.pk}),
|
||||||
|
reverse("document-thumb", kwargs={"pk": document.pk}),
|
||||||
|
str(document.correspondent),
|
||||||
|
str(",".join(document.tags.all().values_list(
|
||||||
|
"name", flat=True)))
|
||||||
|
)).wait()
|
||||||
|
except Exception as e:
|
||||||
|
raise ConsumerError(
|
||||||
|
f"Error while executing pre-consume script: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
def try_consume_file(self,
|
def try_consume_file(self,
|
||||||
path,
|
path,
|
||||||
override_filename=None,
|
override_filename=None,
|
||||||
@ -119,6 +153,8 @@ class Consumer(LoggingMixin):
|
|||||||
logging_group=self.logging_group
|
logging_group=self.logging_group
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.run_pre_consume_script()
|
||||||
|
|
||||||
# This doesn't parse the document yet, but gives us a parser.
|
# This doesn't parse the document yet, but gives us a parser.
|
||||||
|
|
||||||
document_parser = parser_class(self.logging_group)
|
document_parser = parser_class(self.logging_group)
|
||||||
@ -130,7 +166,7 @@ class Consumer(LoggingMixin):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self.log("debug", "Parsing {}...".format(self.filename))
|
self.log("debug", "Parsing {}...".format(self.filename))
|
||||||
document_parser.parse(self.path, mime_type)
|
document_parser.parse(self.path, mime_type, self.filename)
|
||||||
|
|
||||||
self.log("debug", f"Generating thumbnail for {self.filename}...")
|
self.log("debug", f"Generating thumbnail for {self.filename}...")
|
||||||
thumbnail = document_parser.get_optimised_thumbnail(
|
thumbnail = document_parser.get_optimised_thumbnail(
|
||||||
@ -158,7 +194,7 @@ class Consumer(LoggingMixin):
|
|||||||
try:
|
try:
|
||||||
classifier = DocumentClassifier()
|
classifier = DocumentClassifier()
|
||||||
classifier.reload()
|
classifier.reload()
|
||||||
except (FileNotFoundError, IncompatibleClassifierVersionError) as e:
|
except (OSError, EOFError, IncompatibleClassifierVersionError) as e:
|
||||||
self.log(
|
self.log(
|
||||||
"warning",
|
"warning",
|
||||||
f"Cannot classify documents: {e}.")
|
f"Cannot classify documents: {e}.")
|
||||||
@ -215,6 +251,9 @@ class Consumer(LoggingMixin):
|
|||||||
# Delete the file only if it was successfully consumed
|
# Delete the file only if it was successfully consumed
|
||||||
self.log("debug", "Deleting file {}".format(self.path))
|
self.log("debug", "Deleting file {}".format(self.path))
|
||||||
os.unlink(self.path)
|
os.unlink(self.path)
|
||||||
|
|
||||||
|
self.run_post_consume_script(document)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(
|
self.log(
|
||||||
"error",
|
"error",
|
||||||
|
@ -100,7 +100,9 @@ def generate_filename(doc, counter=0):
|
|||||||
many_to_dictionary(doc.tags))
|
many_to_dictionary(doc.tags))
|
||||||
|
|
||||||
tag_list = pathvalidate.sanitize_filename(
|
tag_list = pathvalidate.sanitize_filename(
|
||||||
",".join([tag.name for tag in doc.tags.all()]),
|
",".join(sorted(
|
||||||
|
[tag.name for tag in doc.tags.all()]
|
||||||
|
)),
|
||||||
replacement_text="-"
|
replacement_text="-"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ class Command(Renderable, BaseCommand):
|
|||||||
classifier = DocumentClassifier()
|
classifier = DocumentClassifier()
|
||||||
try:
|
try:
|
||||||
classifier.reload()
|
classifier.reload()
|
||||||
except (FileNotFoundError, IncompatibleClassifierVersionError) as e:
|
except (OSError, EOFError, IncompatibleClassifierVersionError) as e:
|
||||||
logging.getLogger(__name__).warning(
|
logging.getLogger(__name__).warning(
|
||||||
f"Cannot classify documents: {e}.")
|
f"Cannot classify documents: {e}.")
|
||||||
classifier = None
|
classifier = None
|
||||||
|
68
src/documents/management/commands/document_thumbnails.py
Normal file
68
src/documents/management/commands/document_thumbnails.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import logging
|
||||||
|
import multiprocessing
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import tqdm
|
||||||
|
from django import db
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from documents.models import Document
|
||||||
|
from ...mixins import Renderable
|
||||||
|
from ...parsers import get_parser_class_for_mime_type
|
||||||
|
|
||||||
|
|
||||||
|
def _process_document(doc_in):
|
||||||
|
document = Document.objects.get(id=doc_in)
|
||||||
|
parser = get_parser_class_for_mime_type(document.mime_type)(
|
||||||
|
logging_group=None)
|
||||||
|
try:
|
||||||
|
thumb = parser.get_optimised_thumbnail(
|
||||||
|
document.source_path, document.mime_type)
|
||||||
|
|
||||||
|
shutil.move(thumb, document.thumbnail_path)
|
||||||
|
finally:
|
||||||
|
parser.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(Renderable, BaseCommand):
|
||||||
|
|
||||||
|
help = """
|
||||||
|
This will regenerate the thumbnails for all documents.
|
||||||
|
""".replace(" ", "")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.verbosity = 0
|
||||||
|
BaseCommand.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"-d", "--document",
|
||||||
|
default=None,
|
||||||
|
type=int,
|
||||||
|
required=False,
|
||||||
|
help="Specify the ID of a document, and this command will only "
|
||||||
|
"run on this specific document."
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
|
self.verbosity = options["verbosity"]
|
||||||
|
|
||||||
|
logging.getLogger().handlers[0].level = logging.ERROR
|
||||||
|
|
||||||
|
if options['document']:
|
||||||
|
documents = Document.objects.filter(pk=options['document'])
|
||||||
|
else:
|
||||||
|
documents = Document.objects.all()
|
||||||
|
|
||||||
|
ids = [doc.id for doc in documents]
|
||||||
|
|
||||||
|
# Note to future self: this prevents django from reusing database
|
||||||
|
# conncetions between processes, which is bad and does not work
|
||||||
|
# with postgres.
|
||||||
|
db.connections.close_all()
|
||||||
|
|
||||||
|
with multiprocessing.Pool() as pool:
|
||||||
|
list(tqdm.tqdm(
|
||||||
|
pool.imap_unordered(_process_document, ids), total=len(ids)
|
||||||
|
))
|
18
src/documents/migrations/1010_auto_20210101_2159.py
Normal file
18
src/documents/migrations/1010_auto_20210101_2159.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-01-01 21:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '1009_auto_20201216_2005'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedviewfilterrule',
|
||||||
|
name='value',
|
||||||
|
field=models.CharField(blank=True, max_length=128, null=True),
|
||||||
|
),
|
||||||
|
]
|
250
src/documents/migrations/1011_auto_20210101_2340.py
Normal file
250
src/documents/migrations/1011_auto_20210101_2340.py
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-01-01 23:40
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('documents', '1010_auto_20210101_2159'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='correspondent',
|
||||||
|
options={'ordering': ('name',), 'verbose_name': 'correspondent', 'verbose_name_plural': 'correspondents'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='document',
|
||||||
|
options={'ordering': ('-created',), 'verbose_name': 'document', 'verbose_name_plural': 'documents'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='documenttype',
|
||||||
|
options={'verbose_name': 'document type', 'verbose_name_plural': 'document types'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='log',
|
||||||
|
options={'ordering': ('-created',), 'verbose_name': 'log', 'verbose_name_plural': 'logs'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='savedview',
|
||||||
|
options={'ordering': ('name',), 'verbose_name': 'saved view', 'verbose_name_plural': 'saved views'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='savedviewfilterrule',
|
||||||
|
options={'verbose_name': 'filter rule', 'verbose_name_plural': 'filter rules'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='tag',
|
||||||
|
options={'verbose_name': 'tag', 'verbose_name_plural': 'tags'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='correspondent',
|
||||||
|
name='is_insensitive',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='is insensitive'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='correspondent',
|
||||||
|
name='match',
|
||||||
|
field=models.CharField(blank=True, max_length=256, verbose_name='match'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='correspondent',
|
||||||
|
name='matching_algorithm',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, 'Any word'), (2, 'All words'), (3, 'Exact match'), (4, 'Regular expression'), (5, 'Fuzzy word'), (6, 'Automatic')], default=1, verbose_name='matching algorithm'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='correspondent',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, unique=True, verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='added',
|
||||||
|
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, editable=False, verbose_name='added'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='archive_checksum',
|
||||||
|
field=models.CharField(blank=True, editable=False, help_text='The checksum of the archived document.', max_length=32, null=True, verbose_name='archive checksum'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='archive_serial_number',
|
||||||
|
field=models.IntegerField(blank=True, db_index=True, help_text='The position of this document in your physical document archive.', null=True, unique=True, verbose_name='archive serial number'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='checksum',
|
||||||
|
field=models.CharField(editable=False, help_text='The checksum of the original document.', max_length=32, unique=True, verbose_name='checksum'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='content',
|
||||||
|
field=models.TextField(blank=True, help_text='The raw, text-only data of the document. This field is primarily used for searching.', verbose_name='content'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='correspondent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='documents.correspondent', verbose_name='correspondent'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='created',
|
||||||
|
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='created'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='document_type',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='documents.documenttype', verbose_name='document type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='filename',
|
||||||
|
field=models.FilePathField(default=None, editable=False, help_text='Current filename in storage', max_length=1024, null=True, verbose_name='filename'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='mime_type',
|
||||||
|
field=models.CharField(editable=False, max_length=256, verbose_name='mime type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='modified',
|
||||||
|
field=models.DateTimeField(auto_now=True, db_index=True, verbose_name='modified'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='storage_type',
|
||||||
|
field=models.CharField(choices=[('unencrypted', 'Unencrypted'), ('gpg', 'Encrypted with GNU Privacy Guard')], default='unencrypted', editable=False, max_length=11, verbose_name='storage type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='tags',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='documents', to='documents.Tag', verbose_name='tags'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='document',
|
||||||
|
name='title',
|
||||||
|
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='title'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='documenttype',
|
||||||
|
name='is_insensitive',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='is insensitive'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='documenttype',
|
||||||
|
name='match',
|
||||||
|
field=models.CharField(blank=True, max_length=256, verbose_name='match'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='documenttype',
|
||||||
|
name='matching_algorithm',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, 'Any word'), (2, 'All words'), (3, 'Exact match'), (4, 'Regular expression'), (5, 'Fuzzy word'), (6, 'Automatic')], default=1, verbose_name='matching algorithm'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='documenttype',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, unique=True, verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='log',
|
||||||
|
name='created',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, verbose_name='created'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='log',
|
||||||
|
name='group',
|
||||||
|
field=models.UUIDField(blank=True, null=True, verbose_name='group'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='log',
|
||||||
|
name='level',
|
||||||
|
field=models.PositiveIntegerField(choices=[(10, 'debug'), (20, 'information'), (30, 'warning'), (40, 'error'), (50, 'critical')], default=20, verbose_name='level'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='log',
|
||||||
|
name='message',
|
||||||
|
field=models.TextField(verbose_name='message'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedview',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedview',
|
||||||
|
name='show_in_sidebar',
|
||||||
|
field=models.BooleanField(verbose_name='show in sidebar'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedview',
|
||||||
|
name='show_on_dashboard',
|
||||||
|
field=models.BooleanField(verbose_name='show on dashboard'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedview',
|
||||||
|
name='sort_field',
|
||||||
|
field=models.CharField(max_length=128, verbose_name='sort field'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedview',
|
||||||
|
name='sort_reverse',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='sort reverse'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedview',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedviewfilterrule',
|
||||||
|
name='rule_type',
|
||||||
|
field=models.PositiveIntegerField(choices=[(0, 'title contains'), (1, 'content contains'), (2, 'ASN is'), (3, 'correspondent is'), (4, 'document type is'), (5, 'is in inbox'), (6, 'has tag'), (7, 'has any tag'), (8, 'created before'), (9, 'created after'), (10, 'created year is'), (11, 'created month is'), (12, 'created day is'), (13, 'added before'), (14, 'added after'), (15, 'modified before'), (16, 'modified after'), (17, 'does not have tag')], verbose_name='rule type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedviewfilterrule',
|
||||||
|
name='saved_view',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='filter_rules', to='documents.savedview', verbose_name='saved view'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='savedviewfilterrule',
|
||||||
|
name='value',
|
||||||
|
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='value'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tag',
|
||||||
|
name='colour',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, '#a6cee3'), (2, '#1f78b4'), (3, '#b2df8a'), (4, '#33a02c'), (5, '#fb9a99'), (6, '#e31a1c'), (7, '#fdbf6f'), (8, '#ff7f00'), (9, '#cab2d6'), (10, '#6a3d9a'), (11, '#b15928'), (12, '#000000'), (13, '#cccccc')], default=1, verbose_name='color'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tag',
|
||||||
|
name='is_inbox_tag',
|
||||||
|
field=models.BooleanField(default=False, help_text='Marks this tag as an inbox tag: All newly consumed documents will be tagged with inbox tags.', verbose_name='is inbox tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tag',
|
||||||
|
name='is_insensitive',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='is insensitive'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tag',
|
||||||
|
name='match',
|
||||||
|
field=models.CharField(blank=True, max_length=256, verbose_name='match'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tag',
|
||||||
|
name='matching_algorithm',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, 'Any word'), (2, 'All words'), (3, 'Exact match'), (4, 'Regular expression'), (5, 'Fuzzy word'), (6, 'Automatic')], default=1, verbose_name='matching algorithm'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tag',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=128, unique=True, verbose_name='name'),
|
||||||
|
),
|
||||||
|
]
|
@ -13,6 +13,8 @@ from django.contrib.auth.models import User
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from documents.file_handling import archive_name_from_filename
|
from documents.file_handling import archive_name_from_filename
|
||||||
from documents.parsers import get_default_file_extension
|
from documents.parsers import get_default_file_extension
|
||||||
|
|
||||||
@ -27,36 +29,31 @@ class MatchingModel(models.Model):
|
|||||||
MATCH_AUTO = 6
|
MATCH_AUTO = 6
|
||||||
|
|
||||||
MATCHING_ALGORITHMS = (
|
MATCHING_ALGORITHMS = (
|
||||||
(MATCH_ANY, "Any"),
|
(MATCH_ANY, _("Any word")),
|
||||||
(MATCH_ALL, "All"),
|
(MATCH_ALL, _("All words")),
|
||||||
(MATCH_LITERAL, "Literal"),
|
(MATCH_LITERAL, _("Exact match")),
|
||||||
(MATCH_REGEX, "Regular Expression"),
|
(MATCH_REGEX, _("Regular expression")),
|
||||||
(MATCH_FUZZY, "Fuzzy Match"),
|
(MATCH_FUZZY, _("Fuzzy word")),
|
||||||
(MATCH_AUTO, "Automatic Classification"),
|
(MATCH_AUTO, _("Automatic")),
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(
|
||||||
|
_("name"),
|
||||||
|
max_length=128, unique=True)
|
||||||
|
|
||||||
|
match = models.CharField(
|
||||||
|
_("match"),
|
||||||
|
max_length=256, blank=True)
|
||||||
|
|
||||||
match = models.CharField(max_length=256, blank=True)
|
|
||||||
matching_algorithm = models.PositiveIntegerField(
|
matching_algorithm = models.PositiveIntegerField(
|
||||||
|
_("matching algorithm"),
|
||||||
choices=MATCHING_ALGORITHMS,
|
choices=MATCHING_ALGORITHMS,
|
||||||
default=MATCH_ANY,
|
default=MATCH_ANY
|
||||||
help_text=(
|
|
||||||
"Which algorithm you want to use when matching text to the OCR'd "
|
|
||||||
"PDF. Here, \"any\" looks for any occurrence of any word "
|
|
||||||
"provided in the PDF, while \"all\" requires that every word "
|
|
||||||
"provided appear in the PDF, albeit not in the order provided. A "
|
|
||||||
"\"literal\" match means that the text you enter must appear in "
|
|
||||||
"the PDF exactly as you've entered it, and \"regular expression\" "
|
|
||||||
"uses a regex to match the PDF. (If you don't know what a regex "
|
|
||||||
"is, you probably don't want this option.) Finally, a \"fuzzy "
|
|
||||||
"match\" looks for words or phrases that are mostly—but not "
|
|
||||||
"exactly—the same, which can be useful for matching against "
|
|
||||||
"documents containg imperfections that foil accurate OCR."
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is_insensitive = models.BooleanField(default=True)
|
is_insensitive = models.BooleanField(
|
||||||
|
_("is insensitive"),
|
||||||
|
default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -80,6 +77,8 @@ class Correspondent(MatchingModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
verbose_name = _("correspondent")
|
||||||
|
verbose_name_plural = _("correspondents")
|
||||||
|
|
||||||
|
|
||||||
class Tag(MatchingModel):
|
class Tag(MatchingModel):
|
||||||
@ -100,18 +99,27 @@ class Tag(MatchingModel):
|
|||||||
(13, "#cccccc")
|
(13, "#cccccc")
|
||||||
)
|
)
|
||||||
|
|
||||||
colour = models.PositiveIntegerField(choices=COLOURS, default=1)
|
colour = models.PositiveIntegerField(
|
||||||
|
_("color"),
|
||||||
|
choices=COLOURS, default=1)
|
||||||
|
|
||||||
is_inbox_tag = models.BooleanField(
|
is_inbox_tag = models.BooleanField(
|
||||||
|
_("is inbox tag"),
|
||||||
default=False,
|
default=False,
|
||||||
help_text="Marks this tag as an inbox tag: All newly consumed "
|
help_text=_("Marks this tag as an inbox tag: All newly consumed "
|
||||||
"documents will be tagged with inbox tags."
|
"documents will be tagged with inbox tags.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("tag")
|
||||||
|
verbose_name_plural = _("tags")
|
||||||
|
|
||||||
|
|
||||||
class DocumentType(MatchingModel):
|
class DocumentType(MatchingModel):
|
||||||
|
|
||||||
pass
|
class Meta:
|
||||||
|
verbose_name = _("document type")
|
||||||
|
verbose_name_plural = _("document types")
|
||||||
|
|
||||||
|
|
||||||
class Document(models.Model):
|
class Document(models.Model):
|
||||||
@ -119,8 +127,8 @@ class Document(models.Model):
|
|||||||
STORAGE_TYPE_UNENCRYPTED = "unencrypted"
|
STORAGE_TYPE_UNENCRYPTED = "unencrypted"
|
||||||
STORAGE_TYPE_GPG = "gpg"
|
STORAGE_TYPE_GPG = "gpg"
|
||||||
STORAGE_TYPES = (
|
STORAGE_TYPES = (
|
||||||
(STORAGE_TYPE_UNENCRYPTED, "Unencrypted"),
|
(STORAGE_TYPE_UNENCRYPTED, _("Unencrypted")),
|
||||||
(STORAGE_TYPE_GPG, "Encrypted with GNU Privacy Guard")
|
(STORAGE_TYPE_GPG, _("Encrypted with GNU Privacy Guard"))
|
||||||
)
|
)
|
||||||
|
|
||||||
correspondent = models.ForeignKey(
|
correspondent = models.ForeignKey(
|
||||||
@ -128,55 +136,68 @@ class Document(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
related_name="documents",
|
related_name="documents",
|
||||||
on_delete=models.SET_NULL
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_("correspondent")
|
||||||
)
|
)
|
||||||
|
|
||||||
title = models.CharField(max_length=128, blank=True, db_index=True)
|
title = models.CharField(
|
||||||
|
_("title"),
|
||||||
|
max_length=128, blank=True, db_index=True)
|
||||||
|
|
||||||
document_type = models.ForeignKey(
|
document_type = models.ForeignKey(
|
||||||
DocumentType,
|
DocumentType,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
related_name="documents",
|
related_name="documents",
|
||||||
on_delete=models.SET_NULL
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_("document type")
|
||||||
)
|
)
|
||||||
|
|
||||||
content = models.TextField(
|
content = models.TextField(
|
||||||
|
_("content"),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="The raw, text-only data of the document. This field is "
|
help_text=_("The raw, text-only data of the document. This field is "
|
||||||
"primarily used for searching."
|
"primarily used for searching.")
|
||||||
)
|
)
|
||||||
|
|
||||||
mime_type = models.CharField(
|
mime_type = models.CharField(
|
||||||
|
_("mime type"),
|
||||||
max_length=256,
|
max_length=256,
|
||||||
editable=False
|
editable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = models.ManyToManyField(
|
tags = models.ManyToManyField(
|
||||||
Tag, related_name="documents", blank=True)
|
Tag, related_name="documents", blank=True,
|
||||||
|
verbose_name=_("tags")
|
||||||
|
)
|
||||||
|
|
||||||
checksum = models.CharField(
|
checksum = models.CharField(
|
||||||
|
_("checksum"),
|
||||||
max_length=32,
|
max_length=32,
|
||||||
editable=False,
|
editable=False,
|
||||||
unique=True,
|
unique=True,
|
||||||
help_text="The checksum of the original document."
|
help_text=_("The checksum of the original document.")
|
||||||
)
|
)
|
||||||
|
|
||||||
archive_checksum = models.CharField(
|
archive_checksum = models.CharField(
|
||||||
|
_("archive checksum"),
|
||||||
max_length=32,
|
max_length=32,
|
||||||
editable=False,
|
editable=False,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="The checksum of the archived document."
|
help_text=_("The checksum of the archived document.")
|
||||||
)
|
)
|
||||||
|
|
||||||
created = models.DateTimeField(
|
created = models.DateTimeField(
|
||||||
|
_("created"),
|
||||||
default=timezone.now, db_index=True)
|
default=timezone.now, db_index=True)
|
||||||
|
|
||||||
modified = models.DateTimeField(
|
modified = models.DateTimeField(
|
||||||
|
_("modified"),
|
||||||
auto_now=True, editable=False, db_index=True)
|
auto_now=True, editable=False, db_index=True)
|
||||||
|
|
||||||
storage_type = models.CharField(
|
storage_type = models.CharField(
|
||||||
|
_("storage type"),
|
||||||
max_length=11,
|
max_length=11,
|
||||||
choices=STORAGE_TYPES,
|
choices=STORAGE_TYPES,
|
||||||
default=STORAGE_TYPE_UNENCRYPTED,
|
default=STORAGE_TYPE_UNENCRYPTED,
|
||||||
@ -184,27 +205,32 @@ class Document(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
added = models.DateTimeField(
|
added = models.DateTimeField(
|
||||||
|
_("added"),
|
||||||
default=timezone.now, editable=False, db_index=True)
|
default=timezone.now, editable=False, db_index=True)
|
||||||
|
|
||||||
filename = models.FilePathField(
|
filename = models.FilePathField(
|
||||||
|
_("filename"),
|
||||||
max_length=1024,
|
max_length=1024,
|
||||||
editable=False,
|
editable=False,
|
||||||
default=None,
|
default=None,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="Current filename in storage"
|
help_text=_("Current filename in storage")
|
||||||
)
|
)
|
||||||
|
|
||||||
archive_serial_number = models.IntegerField(
|
archive_serial_number = models.IntegerField(
|
||||||
|
_("archive serial number"),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
unique=True,
|
unique=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
help_text="The position of this document in your physical document "
|
help_text=_("The position of this document in your physical document "
|
||||||
"archive."
|
"archive.")
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
|
verbose_name = _("document")
|
||||||
|
verbose_name_plural = _("documents")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
created = datetime.date.isoformat(self.created)
|
created = datetime.date.isoformat(self.created)
|
||||||
@ -286,20 +312,29 @@ class Document(models.Model):
|
|||||||
class Log(models.Model):
|
class Log(models.Model):
|
||||||
|
|
||||||
LEVELS = (
|
LEVELS = (
|
||||||
(logging.DEBUG, "Debugging"),
|
(logging.DEBUG, _("debug")),
|
||||||
(logging.INFO, "Informational"),
|
(logging.INFO, _("information")),
|
||||||
(logging.WARNING, "Warning"),
|
(logging.WARNING, _("warning")),
|
||||||
(logging.ERROR, "Error"),
|
(logging.ERROR, _("error")),
|
||||||
(logging.CRITICAL, "Critical"),
|
(logging.CRITICAL, _("critical")),
|
||||||
)
|
)
|
||||||
|
|
||||||
group = models.UUIDField(blank=True, null=True)
|
group = models.UUIDField(
|
||||||
message = models.TextField()
|
_("group"),
|
||||||
level = models.PositiveIntegerField(choices=LEVELS, default=logging.INFO)
|
blank=True, null=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
message = models.TextField(_("message"))
|
||||||
|
|
||||||
|
level = models.PositiveIntegerField(
|
||||||
|
_("level"),
|
||||||
|
choices=LEVELS, default=logging.INFO)
|
||||||
|
|
||||||
|
created = models.DateTimeField(_("created"), auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
|
verbose_name = _("log")
|
||||||
|
verbose_name_plural = _("logs")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.message
|
return self.message
|
||||||
@ -310,48 +345,72 @@ class SavedView(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
|
verbose_name = _("saved view")
|
||||||
|
verbose_name_plural = _("saved views")
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE,
|
||||||
name = models.CharField(max_length=128)
|
verbose_name=_("user"))
|
||||||
|
name = models.CharField(
|
||||||
|
_("name"),
|
||||||
|
max_length=128)
|
||||||
|
|
||||||
show_on_dashboard = models.BooleanField()
|
show_on_dashboard = models.BooleanField(
|
||||||
show_in_sidebar = models.BooleanField()
|
_("show on dashboard"),
|
||||||
|
)
|
||||||
|
show_in_sidebar = models.BooleanField(
|
||||||
|
_("show in sidebar"),
|
||||||
|
)
|
||||||
|
|
||||||
sort_field = models.CharField(max_length=128)
|
sort_field = models.CharField(
|
||||||
sort_reverse = models.BooleanField(default=False)
|
_("sort field"),
|
||||||
|
max_length=128)
|
||||||
|
sort_reverse = models.BooleanField(
|
||||||
|
_("sort reverse"),
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
|
||||||
class SavedViewFilterRule(models.Model):
|
class SavedViewFilterRule(models.Model):
|
||||||
RULE_TYPES = [
|
RULE_TYPES = [
|
||||||
(0, "Title contains"),
|
(0, _("title contains")),
|
||||||
(1, "Content contains"),
|
(1, _("content contains")),
|
||||||
(2, "ASN is"),
|
(2, _("ASN is")),
|
||||||
(3, "Correspondent is"),
|
(3, _("correspondent is")),
|
||||||
(4, "Document type is"),
|
(4, _("document type is")),
|
||||||
(5, "Is in inbox"),
|
(5, _("is in inbox")),
|
||||||
(6, "Has tag"),
|
(6, _("has tag")),
|
||||||
(7, "Has any tag"),
|
(7, _("has any tag")),
|
||||||
(8, "Created before"),
|
(8, _("created before")),
|
||||||
(9, "Created after"),
|
(9, _("created after")),
|
||||||
(10, "Created year is"),
|
(10, _("created year is")),
|
||||||
(11, "Created month is"),
|
(11, _("created month is")),
|
||||||
(12, "Created day is"),
|
(12, _("created day is")),
|
||||||
(13, "Added before"),
|
(13, _("added before")),
|
||||||
(14, "Added after"),
|
(14, _("added after")),
|
||||||
(15, "Modified before"),
|
(15, _("modified before")),
|
||||||
(16, "Modified after"),
|
(16, _("modified after")),
|
||||||
(17, "Does not have tag"),
|
(17, _("does not have tag")),
|
||||||
]
|
]
|
||||||
|
|
||||||
saved_view = models.ForeignKey(
|
saved_view = models.ForeignKey(
|
||||||
SavedView,
|
SavedView,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="filter_rules"
|
related_name="filter_rules",
|
||||||
|
verbose_name=_("saved view")
|
||||||
)
|
)
|
||||||
|
|
||||||
rule_type = models.PositiveIntegerField(choices=RULE_TYPES)
|
rule_type = models.PositiveIntegerField(
|
||||||
|
_("rule type"),
|
||||||
|
choices=RULE_TYPES)
|
||||||
|
|
||||||
value = models.CharField(max_length=128)
|
value = models.CharField(
|
||||||
|
_("value"),
|
||||||
|
max_length=128,
|
||||||
|
blank=True,
|
||||||
|
null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("filter rule")
|
||||||
|
verbose_name_plural = _("filter rules")
|
||||||
|
|
||||||
|
|
||||||
# TODO: why is this in the models file?
|
# TODO: why is this in the models file?
|
||||||
|
@ -117,6 +117,7 @@ def run_convert(input_file,
|
|||||||
trim=False,
|
trim=False,
|
||||||
type=None,
|
type=None,
|
||||||
depth=None,
|
depth=None,
|
||||||
|
auto_orient=False,
|
||||||
extra=None,
|
extra=None,
|
||||||
logging_group=None):
|
logging_group=None):
|
||||||
|
|
||||||
@ -134,6 +135,7 @@ def run_convert(input_file,
|
|||||||
args += ['-trim'] if trim else []
|
args += ['-trim'] if trim else []
|
||||||
args += ['-type', str(type)] if type else []
|
args += ['-type', str(type)] if type else []
|
||||||
args += ['-depth', str(depth)] if depth else []
|
args += ['-depth', str(depth)] if depth else []
|
||||||
|
args += ['-auto-orient'] if auto_orient else []
|
||||||
args += [input_file, output_file]
|
args += [input_file, output_file]
|
||||||
|
|
||||||
logger.debug("Execute: " + " ".join(args), extra={'group': logging_group})
|
logger.debug("Execute: " + " ".join(args), extra={'group': logging_group})
|
||||||
@ -142,6 +144,53 @@ def run_convert(input_file,
|
|||||||
raise ParseError("Convert failed at {}".format(args))
|
raise ParseError("Convert failed at {}".format(args))
|
||||||
|
|
||||||
|
|
||||||
|
def make_thumbnail_from_pdf(in_path, temp_dir, logging_group=None):
|
||||||
|
"""
|
||||||
|
The thumbnail of a PDF is just a 500px wide image of the first page.
|
||||||
|
"""
|
||||||
|
out_path = os.path.join(temp_dir, "convert.png")
|
||||||
|
|
||||||
|
# Run convert to get a decent thumbnail
|
||||||
|
try:
|
||||||
|
run_convert(density=300,
|
||||||
|
scale="500x5000>",
|
||||||
|
alpha="remove",
|
||||||
|
strip=True,
|
||||||
|
trim=False,
|
||||||
|
auto_orient=True,
|
||||||
|
input_file="{}[0]".format(in_path),
|
||||||
|
output_file=out_path,
|
||||||
|
logging_group=logging_group)
|
||||||
|
except ParseError:
|
||||||
|
# if convert fails, fall back to extracting
|
||||||
|
# the first PDF page as a PNG using Ghostscript
|
||||||
|
logger.warning(
|
||||||
|
"Thumbnail generation with ImageMagick failed, falling back "
|
||||||
|
"to ghostscript. Check your /etc/ImageMagick-x/policy.xml!",
|
||||||
|
extra={'group': logging_group}
|
||||||
|
)
|
||||||
|
gs_out_path = os.path.join(temp_dir, "gs_out.png")
|
||||||
|
cmd = [settings.GS_BINARY,
|
||||||
|
"-q",
|
||||||
|
"-sDEVICE=pngalpha",
|
||||||
|
"-o", gs_out_path,
|
||||||
|
in_path]
|
||||||
|
if not subprocess.Popen(cmd).wait() == 0:
|
||||||
|
raise ParseError("Thumbnail (gs) failed at {}".format(cmd))
|
||||||
|
# then run convert on the output from gs
|
||||||
|
run_convert(density=300,
|
||||||
|
scale="500x5000>",
|
||||||
|
alpha="remove",
|
||||||
|
strip=True,
|
||||||
|
trim=False,
|
||||||
|
auto_orient=True,
|
||||||
|
input_file=gs_out_path,
|
||||||
|
output_file=out_path,
|
||||||
|
logging_group=logging_group)
|
||||||
|
|
||||||
|
return out_path
|
||||||
|
|
||||||
|
|
||||||
def parse_date(filename, text):
|
def parse_date(filename, text):
|
||||||
"""
|
"""
|
||||||
Returns the date of the document.
|
Returns the date of the document.
|
||||||
@ -219,7 +268,7 @@ class DocumentParser(LoggingMixin):
|
|||||||
def extract_metadata(self, document_path, mime_type):
|
def extract_metadata(self, document_path, mime_type):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def parse(self, document_path, mime_type):
|
def parse(self, document_path, mime_type, file_name=None):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_archive_path(self):
|
def get_archive_path(self):
|
||||||
|
@ -11,7 +11,6 @@ from django.db.models import Q
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
from rest_framework.reverse import reverse
|
|
||||||
|
|
||||||
from .. import index, matching
|
from .. import index, matching
|
||||||
from ..file_handling import delete_empty_directories, \
|
from ..file_handling import delete_empty_directories, \
|
||||||
@ -147,32 +146,6 @@ def set_tags(sender,
|
|||||||
document.tags.add(*relevant_tags)
|
document.tags.add(*relevant_tags)
|
||||||
|
|
||||||
|
|
||||||
def run_pre_consume_script(sender, filename, **kwargs):
|
|
||||||
|
|
||||||
if not settings.PRE_CONSUME_SCRIPT:
|
|
||||||
return
|
|
||||||
|
|
||||||
Popen((settings.PRE_CONSUME_SCRIPT, filename)).wait()
|
|
||||||
|
|
||||||
|
|
||||||
def run_post_consume_script(sender, document, **kwargs):
|
|
||||||
|
|
||||||
if not settings.POST_CONSUME_SCRIPT:
|
|
||||||
return
|
|
||||||
|
|
||||||
Popen((
|
|
||||||
settings.POST_CONSUME_SCRIPT,
|
|
||||||
str(document.pk),
|
|
||||||
document.get_public_filename(),
|
|
||||||
os.path.normpath(document.source_path),
|
|
||||||
os.path.normpath(document.thumbnail_path),
|
|
||||||
reverse("document-download", kwargs={"pk": document.pk}),
|
|
||||||
reverse("document-thumb", kwargs={"pk": document.pk}),
|
|
||||||
str(document.correspondent),
|
|
||||||
str(",".join(document.tags.all().values_list("name", flat=True)))
|
|
||||||
)).wait()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(models.signals.post_delete, sender=Document)
|
@receiver(models.signals.post_delete, sender=Document)
|
||||||
def cleanup_document_deletion(sender, instance, using, **kwargs):
|
def cleanup_document_deletion(sender, instance, using, **kwargs):
|
||||||
with FileLock(settings.MEDIA_LOCK):
|
with FileLock(settings.MEDIA_LOCK):
|
||||||
@ -276,13 +249,6 @@ def update_filename_and_move_files(sender, instance, **kwargs):
|
|||||||
Document.objects.filter(pk=instance.pk).update(
|
Document.objects.filter(pk=instance.pk).update(
|
||||||
filename=new_filename)
|
filename=new_filename)
|
||||||
|
|
||||||
logging.getLogger(__name__).debug(
|
|
||||||
f"Moved file {old_source_path} to {new_source_path}.")
|
|
||||||
|
|
||||||
if instance.archive_checksum:
|
|
||||||
logging.getLogger(__name__).debug(
|
|
||||||
f"Moved file {old_archive_path} to {new_archive_path}.")
|
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
instance.filename = old_filename
|
instance.filename = old_filename
|
||||||
# this happens when we can't move a file. If that's the case for
|
# this happens when we can't move a file. If that's the case for
|
||||||
|
@ -35,9 +35,9 @@ def train_classifier():
|
|||||||
try:
|
try:
|
||||||
# load the classifier, since we might not have to train it again.
|
# load the classifier, since we might not have to train it again.
|
||||||
classifier.reload()
|
classifier.reload()
|
||||||
except (FileNotFoundError, IncompatibleClassifierVersionError):
|
except (OSError, EOFError, IncompatibleClassifierVersionError):
|
||||||
# This is what we're going to fix here.
|
# This is what we're going to fix here.
|
||||||
pass
|
classifier = DocumentClassifier()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if classifier.train():
|
if classifier.train():
|
||||||
@ -94,7 +94,10 @@ def bulk_update_documents(document_ids):
|
|||||||
documents = Document.objects.filter(id__in=document_ids)
|
documents = Document.objects.filter(id__in=document_ids)
|
||||||
|
|
||||||
ix = index.open_index()
|
ix = index.open_index()
|
||||||
|
|
||||||
|
for doc in documents:
|
||||||
|
post_save.send(Document, instance=doc, created=False)
|
||||||
|
|
||||||
with AsyncWriter(ix) as writer:
|
with AsyncWriter(ix) as writer:
|
||||||
for doc in documents:
|
for doc in documents:
|
||||||
index.update_document(writer, doc)
|
index.update_document(writer, doc)
|
||||||
post_save.send(Document, instance=doc, created=False)
|
|
||||||
|
@ -12,11 +12,13 @@
|
|||||||
<meta name="full_name" content="{{full_name}}">
|
<meta name="full_name" content="{{full_name}}">
|
||||||
<meta name="cookie_prefix" content="{{cookie_prefix}}">
|
<meta name="cookie_prefix" content="{{cookie_prefix}}">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<link rel="stylesheet" href="{% static 'frontend/styles.css' %}"></head>
|
<link rel="manifest" href="{% static webmanifest %}">
|
||||||
|
<link rel="stylesheet" href="{% static styles_css %}">
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root>Loading...</app-root>
|
<app-root>Loading...</app-root>
|
||||||
<script src="{% static 'frontend/runtime.js' %}" defer></script>
|
<script src="{% static runtime_js %}" defer></script>
|
||||||
<script src="{% static 'frontend/polyfills.js' %}" defer></script>
|
<script src="{% static polyfills_js %}" defer></script>
|
||||||
<script src="{% static 'frontend/main.js' %}" defer></script>
|
<script src="{% static main_js %}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -177,7 +177,7 @@ class DummyParser(DocumentParser):
|
|||||||
def get_optimised_thumbnail(self, document_path, mime_type):
|
def get_optimised_thumbnail(self, document_path, mime_type):
|
||||||
return self.fake_thumb
|
return self.fake_thumb
|
||||||
|
|
||||||
def parse(self, document_path, mime_type):
|
def parse(self, document_path, mime_type, file_name=None):
|
||||||
self.text = "The Text"
|
self.text = "The Text"
|
||||||
|
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ class FaultyParser(DocumentParser):
|
|||||||
def get_optimised_thumbnail(self, document_path, mime_type):
|
def get_optimised_thumbnail(self, document_path, mime_type):
|
||||||
return self.fake_thumb
|
return self.fake_thumb
|
||||||
|
|
||||||
def parse(self, document_path, mime_type):
|
def parse(self, document_path, mime_type, file_name=None):
|
||||||
raise ParseError("Does not compute.")
|
raise ParseError("Does not compute.")
|
||||||
|
|
||||||
|
|
||||||
@ -466,3 +466,53 @@ class TestConsumer(DirectoriesMixin, TestCase):
|
|||||||
self.assertTrue(os.path.isfile(dst))
|
self.assertTrue(os.path.isfile(dst))
|
||||||
self.assertRaises(ConsumerError, self.consumer.try_consume_file, dst)
|
self.assertRaises(ConsumerError, self.consumer.try_consume_file, dst)
|
||||||
self.assertTrue(os.path.isfile(dst))
|
self.assertTrue(os.path.isfile(dst))
|
||||||
|
|
||||||
|
|
||||||
|
class PostConsumeTestCase(TestCase):
|
||||||
|
|
||||||
|
@mock.patch("documents.consumer.Popen")
|
||||||
|
@override_settings(POST_CONSUME_SCRIPT=None)
|
||||||
|
def test_no_post_consume_script(self, m):
|
||||||
|
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
||||||
|
tag1 = Tag.objects.create(name="a")
|
||||||
|
tag2 = Tag.objects.create(name="b")
|
||||||
|
doc.tags.add(tag1)
|
||||||
|
doc.tags.add(tag2)
|
||||||
|
|
||||||
|
Consumer().run_post_consume_script(doc)
|
||||||
|
|
||||||
|
m.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.consumer.Popen")
|
||||||
|
@override_settings(POST_CONSUME_SCRIPT="script")
|
||||||
|
def test_post_consume_script_simple(self, m):
|
||||||
|
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
||||||
|
|
||||||
|
Consumer().run_post_consume_script(doc)
|
||||||
|
|
||||||
|
m.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.consumer.Popen")
|
||||||
|
@override_settings(POST_CONSUME_SCRIPT="script")
|
||||||
|
def test_post_consume_script_with_correspondent(self, m):
|
||||||
|
c = Correspondent.objects.create(name="my_bank")
|
||||||
|
doc = Document.objects.create(title="Test", mime_type="application/pdf", correspondent=c)
|
||||||
|
tag1 = Tag.objects.create(name="a")
|
||||||
|
tag2 = Tag.objects.create(name="b")
|
||||||
|
doc.tags.add(tag1)
|
||||||
|
doc.tags.add(tag2)
|
||||||
|
|
||||||
|
Consumer().run_post_consume_script(doc)
|
||||||
|
|
||||||
|
m.assert_called_once()
|
||||||
|
|
||||||
|
args, kwargs = m.call_args
|
||||||
|
|
||||||
|
command = args[0]
|
||||||
|
|
||||||
|
self.assertEqual(command[0], "script")
|
||||||
|
self.assertEqual(command[1], str(doc.pk))
|
||||||
|
self.assertEqual(command[5], f"/api/documents/{doc.pk}/download/")
|
||||||
|
self.assertEqual(command[6], f"/api/documents/{doc.pk}/thumb/")
|
||||||
|
self.assertEqual(command[7], "my_bank")
|
||||||
|
self.assertCountEqual(command[8].split(","), ["a", "b"])
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
from unittest import mock
|
|
||||||
|
|
||||||
from django.test import TestCase, override_settings
|
|
||||||
|
|
||||||
from documents.models import Document, Tag, Correspondent
|
|
||||||
from documents.signals.handlers import run_post_consume_script
|
|
||||||
|
|
||||||
|
|
||||||
class PostConsumeTestCase(TestCase):
|
|
||||||
|
|
||||||
@mock.patch("documents.signals.handlers.Popen")
|
|
||||||
@override_settings(POST_CONSUME_SCRIPT=None)
|
|
||||||
def test_no_post_consume_script(self, m):
|
|
||||||
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
|
||||||
tag1 = Tag.objects.create(name="a")
|
|
||||||
tag2 = Tag.objects.create(name="b")
|
|
||||||
doc.tags.add(tag1)
|
|
||||||
doc.tags.add(tag2)
|
|
||||||
|
|
||||||
run_post_consume_script(None, doc)
|
|
||||||
|
|
||||||
m.assert_not_called()
|
|
||||||
|
|
||||||
@mock.patch("documents.signals.handlers.Popen")
|
|
||||||
@override_settings(POST_CONSUME_SCRIPT="script")
|
|
||||||
def test_post_consume_script_simple(self, m):
|
|
||||||
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
|
||||||
|
|
||||||
run_post_consume_script(None, doc)
|
|
||||||
|
|
||||||
m.assert_called_once()
|
|
||||||
|
|
||||||
@mock.patch("documents.signals.handlers.Popen")
|
|
||||||
@override_settings(POST_CONSUME_SCRIPT="script")
|
|
||||||
def test_post_consume_script_with_correspondent(self, m):
|
|
||||||
c = Correspondent.objects.create(name="my_bank")
|
|
||||||
doc = Document.objects.create(title="Test", mime_type="application/pdf", correspondent=c)
|
|
||||||
tag1 = Tag.objects.create(name="a")
|
|
||||||
tag2 = Tag.objects.create(name="b")
|
|
||||||
doc.tags.add(tag1)
|
|
||||||
doc.tags.add(tag2)
|
|
||||||
|
|
||||||
run_post_consume_script(None, doc)
|
|
||||||
|
|
||||||
m.assert_called_once()
|
|
||||||
|
|
||||||
args, kwargs = m.call_args
|
|
||||||
|
|
||||||
command = args[0]
|
|
||||||
|
|
||||||
self.assertEqual(command[0], "script")
|
|
||||||
self.assertEqual(command[1], str(doc.pk))
|
|
||||||
self.assertEqual(command[5], f"/api/documents/{doc.pk}/download/")
|
|
||||||
self.assertEqual(command[6], f"/api/documents/{doc.pk}/thumb/")
|
|
||||||
self.assertEqual(command[7], "my_bank")
|
|
||||||
self.assertCountEqual(command[8].split(","), ["a", "b"])
|
|
@ -7,6 +7,7 @@ from django.conf import settings
|
|||||||
from django.db.models import Count, Max, Case, When, IntegerField
|
from django.db.models import Count, Max, Case, When, IntegerField
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, Http404
|
from django.http import HttpResponse, HttpResponseBadRequest, Http404
|
||||||
|
from django.utils.translation import get_language
|
||||||
from django.views.decorators.cache import cache_control
|
from django.views.decorators.cache import cache_control
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
@ -56,11 +57,29 @@ from .serialisers import (
|
|||||||
class IndexView(TemplateView):
|
class IndexView(TemplateView):
|
||||||
template_name = "index.html"
|
template_name = "index.html"
|
||||||
|
|
||||||
|
def get_language(self):
|
||||||
|
# This is here for the following reason:
|
||||||
|
# Django identifies languages in the form "en-us"
|
||||||
|
# However, angular generates locales as "en-US".
|
||||||
|
# this translates between these two forms.
|
||||||
|
lang = get_language()
|
||||||
|
if "-" in lang:
|
||||||
|
first = lang[:lang.index("-")]
|
||||||
|
second = lang[lang.index("-")+1:]
|
||||||
|
return f"{first}-{second.upper()}"
|
||||||
|
else:
|
||||||
|
return lang
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['cookie_prefix'] = settings.COOKIE_PREFIX
|
context['cookie_prefix'] = settings.COOKIE_PREFIX
|
||||||
context['username'] = self.request.user.username
|
context['username'] = self.request.user.username
|
||||||
context['full_name'] = self.request.user.get_full_name()
|
context['full_name'] = self.request.user.get_full_name()
|
||||||
|
context['styles_css'] = f"frontend/{self.get_language()}/styles.css"
|
||||||
|
context['runtime_js'] = f"frontend/{self.get_language()}/runtime.js"
|
||||||
|
context['polyfills_js'] = f"frontend/{self.get_language()}/polyfills.js" # NOQA: E501
|
||||||
|
context['main_js'] = f"frontend/{self.get_language()}/main.js"
|
||||||
|
context['manifest'] = f"frontend/{self.get_language()}/manifest.webmanifest" # NOQA: E501
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
567
src/locale/de/LC_MESSAGES/django.po
Normal file
567
src/locale/de/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,567 @@
|
|||||||
|
# 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:
|
||||||
|
# Jonas Winkler <dev@jpwinkler.de>, 2021
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2021-01-02 00:26+0000\n"
|
||||||
|
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
|
||||||
|
"Last-Translator: Jonas Winkler <dev@jpwinkler.de>, 2021\n"
|
||||||
|
"Language-Team: German (https://www.transifex.com/paperless/teams/115905/de/)\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: de\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: documents/apps.py:10
|
||||||
|
msgid "Documents"
|
||||||
|
msgstr "Dokumente"
|
||||||
|
|
||||||
|
#: documents/models.py:32
|
||||||
|
msgid "Any word"
|
||||||
|
msgstr "Irgendein Wort"
|
||||||
|
|
||||||
|
#: documents/models.py:33
|
||||||
|
msgid "All words"
|
||||||
|
msgstr "Alle Wörter"
|
||||||
|
|
||||||
|
#: documents/models.py:34
|
||||||
|
msgid "Exact match"
|
||||||
|
msgstr "Exakte Übereinstimmung"
|
||||||
|
|
||||||
|
#: documents/models.py:35
|
||||||
|
msgid "Regular expression"
|
||||||
|
msgstr "Regulärer Ausdruck"
|
||||||
|
|
||||||
|
#: documents/models.py:36
|
||||||
|
msgid "Fuzzy word"
|
||||||
|
msgstr "Ungenaues Wort"
|
||||||
|
|
||||||
|
#: documents/models.py:37
|
||||||
|
msgid "Automatic"
|
||||||
|
msgstr "Automatisch"
|
||||||
|
|
||||||
|
#: documents/models.py:41 documents/models.py:354 paperless_mail/models.py:25
|
||||||
|
#: paperless_mail/models.py:100
|
||||||
|
msgid "name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
|
#: documents/models.py:45
|
||||||
|
msgid "match"
|
||||||
|
msgstr "Zuweisungsmuster"
|
||||||
|
|
||||||
|
#: documents/models.py:49
|
||||||
|
msgid "matching algorithm"
|
||||||
|
msgstr "Zuweisungsalgorithmus"
|
||||||
|
|
||||||
|
#: documents/models.py:55
|
||||||
|
msgid "is insensitive"
|
||||||
|
msgstr "Groß-/Kleinschreibung irrelevant"
|
||||||
|
|
||||||
|
#: documents/models.py:80 documents/models.py:140
|
||||||
|
msgid "correspondent"
|
||||||
|
msgstr "Korrespondent"
|
||||||
|
|
||||||
|
#: documents/models.py:81
|
||||||
|
msgid "correspondents"
|
||||||
|
msgstr "Korrespondenten"
|
||||||
|
|
||||||
|
#: documents/models.py:103
|
||||||
|
msgid "color"
|
||||||
|
msgstr "Farbe"
|
||||||
|
|
||||||
|
#: documents/models.py:107
|
||||||
|
msgid "is inbox tag"
|
||||||
|
msgstr "Posteingangs-Tag"
|
||||||
|
|
||||||
|
#: documents/models.py:109
|
||||||
|
msgid ""
|
||||||
|
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
|
||||||
|
"with inbox tags."
|
||||||
|
msgstr ""
|
||||||
|
"Markiert das Tag als Posteingangs-Tag. Neue Dokumente werden immer mit "
|
||||||
|
"diesem Tag versehen."
|
||||||
|
|
||||||
|
#: documents/models.py:114
|
||||||
|
msgid "tag"
|
||||||
|
msgstr "Tag"
|
||||||
|
|
||||||
|
#: documents/models.py:115 documents/models.py:171
|
||||||
|
msgid "tags"
|
||||||
|
msgstr "Tags"
|
||||||
|
|
||||||
|
#: documents/models.py:121 documents/models.py:153
|
||||||
|
msgid "document type"
|
||||||
|
msgstr "Dokumenttyp"
|
||||||
|
|
||||||
|
#: documents/models.py:122
|
||||||
|
msgid "document types"
|
||||||
|
msgstr "Dokumenttypen"
|
||||||
|
|
||||||
|
#: documents/models.py:130
|
||||||
|
msgid "Unencrypted"
|
||||||
|
msgstr "Nicht verschlüsselt"
|
||||||
|
|
||||||
|
#: documents/models.py:131
|
||||||
|
msgid "Encrypted with GNU Privacy Guard"
|
||||||
|
msgstr "Verschlüsselt mit GNU Privacy Guard"
|
||||||
|
|
||||||
|
#: documents/models.py:144
|
||||||
|
msgid "title"
|
||||||
|
msgstr "Titel"
|
||||||
|
|
||||||
|
#: documents/models.py:157
|
||||||
|
msgid "content"
|
||||||
|
msgstr "Inhalt"
|
||||||
|
|
||||||
|
#: documents/models.py:159
|
||||||
|
msgid ""
|
||||||
|
"The raw, text-only data of the document. This field is primarily used for "
|
||||||
|
"searching."
|
||||||
|
msgstr ""
|
||||||
|
"Der Inhalt des Dokuments in Textform. Dieses Feld wird primär für die Suche "
|
||||||
|
"verwendet."
|
||||||
|
|
||||||
|
#: documents/models.py:164
|
||||||
|
msgid "mime type"
|
||||||
|
msgstr "MIME-Typ"
|
||||||
|
|
||||||
|
#: documents/models.py:175
|
||||||
|
msgid "checksum"
|
||||||
|
msgstr "Prüfsumme"
|
||||||
|
|
||||||
|
#: documents/models.py:179
|
||||||
|
msgid "The checksum of the original document."
|
||||||
|
msgstr "Die Prüfsumme des originalen Dokuments."
|
||||||
|
|
||||||
|
#: documents/models.py:183
|
||||||
|
msgid "archive checksum"
|
||||||
|
msgstr "Archiv-Prüfsumme"
|
||||||
|
|
||||||
|
#: documents/models.py:188
|
||||||
|
msgid "The checksum of the archived document."
|
||||||
|
msgstr "Die Prüfsumme des archivierten Dokuments."
|
||||||
|
|
||||||
|
#: documents/models.py:192 documents/models.py:332
|
||||||
|
msgid "created"
|
||||||
|
msgstr "Erstellt"
|
||||||
|
|
||||||
|
#: documents/models.py:196
|
||||||
|
msgid "modified"
|
||||||
|
msgstr "Geändert"
|
||||||
|
|
||||||
|
#: documents/models.py:200
|
||||||
|
msgid "storage type"
|
||||||
|
msgstr "Speichertyp"
|
||||||
|
|
||||||
|
#: documents/models.py:208
|
||||||
|
msgid "added"
|
||||||
|
msgstr "Hinzugefügt"
|
||||||
|
|
||||||
|
#: documents/models.py:212
|
||||||
|
msgid "filename"
|
||||||
|
msgstr "Dateiname"
|
||||||
|
|
||||||
|
#: documents/models.py:217
|
||||||
|
msgid "Current filename in storage"
|
||||||
|
msgstr "Aktueller Dateiname im Datenspeicher"
|
||||||
|
|
||||||
|
#: documents/models.py:221
|
||||||
|
msgid "archive serial number"
|
||||||
|
msgstr "Archiv-Seriennummer"
|
||||||
|
|
||||||
|
#: documents/models.py:226
|
||||||
|
msgid "The position of this document in your physical document archive."
|
||||||
|
msgstr "Die Position dieses Dokuments in Ihrem physischen Dokumentenarchiv."
|
||||||
|
|
||||||
|
#: documents/models.py:232
|
||||||
|
msgid "document"
|
||||||
|
msgstr "Dokument"
|
||||||
|
|
||||||
|
#: documents/models.py:233
|
||||||
|
msgid "documents"
|
||||||
|
msgstr "Dokumente"
|
||||||
|
|
||||||
|
#: documents/models.py:315
|
||||||
|
msgid "debug"
|
||||||
|
msgstr "Debug"
|
||||||
|
|
||||||
|
#: documents/models.py:316
|
||||||
|
msgid "information"
|
||||||
|
msgstr "Information"
|
||||||
|
|
||||||
|
#: documents/models.py:317
|
||||||
|
msgid "warning"
|
||||||
|
msgstr "Warnung"
|
||||||
|
|
||||||
|
#: documents/models.py:318
|
||||||
|
msgid "error"
|
||||||
|
msgstr "Fehler"
|
||||||
|
|
||||||
|
#: documents/models.py:319
|
||||||
|
msgid "critical"
|
||||||
|
msgstr "Kritisch"
|
||||||
|
|
||||||
|
#: documents/models.py:323
|
||||||
|
msgid "group"
|
||||||
|
msgstr "Gruppe"
|
||||||
|
|
||||||
|
#: documents/models.py:326
|
||||||
|
msgid "message"
|
||||||
|
msgstr "Nachricht"
|
||||||
|
|
||||||
|
#: documents/models.py:329
|
||||||
|
msgid "level"
|
||||||
|
msgstr "Level"
|
||||||
|
|
||||||
|
#: documents/models.py:336
|
||||||
|
msgid "log"
|
||||||
|
msgstr "Protokoll"
|
||||||
|
|
||||||
|
#: documents/models.py:337
|
||||||
|
msgid "logs"
|
||||||
|
msgstr "Protokoll"
|
||||||
|
|
||||||
|
#: documents/models.py:348 documents/models.py:398
|
||||||
|
msgid "saved view"
|
||||||
|
msgstr "Gespeicherte Ansicht"
|
||||||
|
|
||||||
|
#: documents/models.py:349
|
||||||
|
msgid "saved views"
|
||||||
|
msgstr "Gespeicherte Ansichten"
|
||||||
|
|
||||||
|
#: documents/models.py:352
|
||||||
|
msgid "user"
|
||||||
|
msgstr "Benutzer"
|
||||||
|
|
||||||
|
#: documents/models.py:358
|
||||||
|
msgid "show on dashboard"
|
||||||
|
msgstr "Auf Startseite zeigen"
|
||||||
|
|
||||||
|
#: documents/models.py:361
|
||||||
|
msgid "show in sidebar"
|
||||||
|
msgstr "In Seitenleiste zeigen"
|
||||||
|
|
||||||
|
#: documents/models.py:365
|
||||||
|
msgid "sort field"
|
||||||
|
msgstr "Sortierfeld"
|
||||||
|
|
||||||
|
#: documents/models.py:368
|
||||||
|
msgid "sort reverse"
|
||||||
|
msgstr "Umgekehrte Sortierung"
|
||||||
|
|
||||||
|
#: documents/models.py:374
|
||||||
|
msgid "title contains"
|
||||||
|
msgstr "Titel enthält"
|
||||||
|
|
||||||
|
#: documents/models.py:375
|
||||||
|
msgid "content contains"
|
||||||
|
msgstr "Inhalt enthält"
|
||||||
|
|
||||||
|
#: documents/models.py:376
|
||||||
|
msgid "ASN is"
|
||||||
|
msgstr "ASN ist"
|
||||||
|
|
||||||
|
#: documents/models.py:377
|
||||||
|
msgid "correspondent is"
|
||||||
|
msgstr "Korrespondent ist"
|
||||||
|
|
||||||
|
#: documents/models.py:378
|
||||||
|
msgid "document type is"
|
||||||
|
msgstr "Dokumenttyp ist"
|
||||||
|
|
||||||
|
#: documents/models.py:379
|
||||||
|
msgid "is in inbox"
|
||||||
|
msgstr "Ist im Posteingang"
|
||||||
|
|
||||||
|
#: documents/models.py:380
|
||||||
|
msgid "has tag"
|
||||||
|
msgstr "Hat Tag"
|
||||||
|
|
||||||
|
#: documents/models.py:381
|
||||||
|
msgid "has any tag"
|
||||||
|
msgstr "Hat irgendein Tag"
|
||||||
|
|
||||||
|
#: documents/models.py:382
|
||||||
|
msgid "created before"
|
||||||
|
msgstr "Erstellt vor"
|
||||||
|
|
||||||
|
#: documents/models.py:383
|
||||||
|
msgid "created after"
|
||||||
|
msgstr "Erstellt nach"
|
||||||
|
|
||||||
|
#: documents/models.py:384
|
||||||
|
msgid "created year is"
|
||||||
|
msgstr "Erstellt im Jahr"
|
||||||
|
|
||||||
|
#: documents/models.py:385
|
||||||
|
msgid "created month is"
|
||||||
|
msgstr "Erstellt im Monat"
|
||||||
|
|
||||||
|
#: documents/models.py:386
|
||||||
|
msgid "created day is"
|
||||||
|
msgstr "Erstellt am Tag"
|
||||||
|
|
||||||
|
#: documents/models.py:387
|
||||||
|
msgid "added before"
|
||||||
|
msgstr "Hinzugefügt vor"
|
||||||
|
|
||||||
|
#: documents/models.py:388
|
||||||
|
msgid "added after"
|
||||||
|
msgstr "Hinzugefügt nach"
|
||||||
|
|
||||||
|
#: documents/models.py:389
|
||||||
|
msgid "modified before"
|
||||||
|
msgstr "Geändert vor"
|
||||||
|
|
||||||
|
#: documents/models.py:390
|
||||||
|
msgid "modified after"
|
||||||
|
msgstr "Geändert nach"
|
||||||
|
|
||||||
|
#: documents/models.py:391
|
||||||
|
msgid "does not have tag"
|
||||||
|
msgstr "Hat nicht folgendes Tag"
|
||||||
|
|
||||||
|
#: documents/models.py:402
|
||||||
|
msgid "rule type"
|
||||||
|
msgstr "Regeltyp"
|
||||||
|
|
||||||
|
#: documents/models.py:406
|
||||||
|
msgid "value"
|
||||||
|
msgstr "Wert"
|
||||||
|
|
||||||
|
#: documents/models.py:412
|
||||||
|
msgid "filter rule"
|
||||||
|
msgstr "Filterregel"
|
||||||
|
|
||||||
|
#: documents/models.py:413
|
||||||
|
msgid "filter rules"
|
||||||
|
msgstr "Filterregeln"
|
||||||
|
|
||||||
|
#: paperless/settings.py:254
|
||||||
|
msgid "English"
|
||||||
|
msgstr "Englisch"
|
||||||
|
|
||||||
|
#: paperless/settings.py:255
|
||||||
|
msgid "German"
|
||||||
|
msgstr "Deutsch"
|
||||||
|
|
||||||
|
#: paperless/urls.py:108
|
||||||
|
msgid "Paperless-ng administration"
|
||||||
|
msgstr "Paperless-ng Administration"
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:24
|
||||||
|
msgid "Filter"
|
||||||
|
msgstr "Filter"
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:26
|
||||||
|
msgid ""
|
||||||
|
"Paperless will only process mails that match ALL of the filters given below."
|
||||||
|
msgstr ""
|
||||||
|
"Paperless wird nur E-Mails verarbeiten, für die alle der hier angegebenen "
|
||||||
|
"Filter zutreffen."
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:34
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr "Aktionen"
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:36
|
||||||
|
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 ""
|
||||||
|
"Die Aktion, die auf E-Mails angewendet werden soll. Diese Aktion wird nur "
|
||||||
|
"auf E-Mails angewendet, aus denen Anhänge verarbeitet wurden. E-Mails ohne "
|
||||||
|
"Anhänge werden vollständig ignoriert."
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:43
|
||||||
|
msgid "Metadata"
|
||||||
|
msgstr "Metadaten"
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:45
|
||||||
|
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 ""
|
||||||
|
"Folgende Metadaten werden Dokumenten dieser Regel automatisch zugewiesen. "
|
||||||
|
"Wenn Sie hier nichts auswählen wird Paperless weiterhin alle "
|
||||||
|
"Zuweisungsalgorithmen ausführen und Metadaten auf Basis des Dokumentinhalts "
|
||||||
|
"zuweisen."
|
||||||
|
|
||||||
|
#: paperless_mail/apps.py:9
|
||||||
|
msgid "Paperless mail"
|
||||||
|
msgstr "Paperless E-Mail"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:11
|
||||||
|
msgid "mail account"
|
||||||
|
msgstr "E-Mail-Konto"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:12
|
||||||
|
msgid "mail accounts"
|
||||||
|
msgstr "E-Mail-Konten"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:19
|
||||||
|
msgid "No encryption"
|
||||||
|
msgstr "Keine Verschlüsselung"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:20
|
||||||
|
msgid "Use SSL"
|
||||||
|
msgstr "SSL benutzen"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:21
|
||||||
|
msgid "Use STARTTLS"
|
||||||
|
msgstr "STARTTLS benutzen"
|
||||||
|
|
||||||
|
#: 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 ""
|
||||||
|
"Dies ist in der Regel 143 für unverschlüsselte und STARTTLS-Verbindungen und"
|
||||||
|
" 993 für SSL-Verbindungen."
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:40
|
||||||
|
msgid "IMAP security"
|
||||||
|
msgstr "IMAP-Sicherheit"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:46
|
||||||
|
msgid "username"
|
||||||
|
msgstr "Benutzername"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:50
|
||||||
|
msgid "password"
|
||||||
|
msgstr "Password"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:60
|
||||||
|
msgid "mail rule"
|
||||||
|
msgstr "E-Mail-Regel"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:61
|
||||||
|
msgid "mail rules"
|
||||||
|
msgstr "E-Mail-Regeln"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:69
|
||||||
|
msgid "Mark as read, don't process read mails"
|
||||||
|
msgstr "Als gelesen markieren, gelesene E-Mails nicht verarbeiten"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:70
|
||||||
|
msgid "Flag the mail, don't process flagged mails"
|
||||||
|
msgstr "Als wichtig markieren, markierte E-Mails nicht verarbeiten"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:71
|
||||||
|
msgid "Move to specified folder"
|
||||||
|
msgstr "In angegebenen Ordner verschieben"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:72
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "Löschen"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:79
|
||||||
|
msgid "Use subject as title"
|
||||||
|
msgstr "Betreff als Titel verwenden"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:80
|
||||||
|
msgid "Use attachment filename as title"
|
||||||
|
msgstr "Dateiname des Anhangs als Titel verwenden"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:90
|
||||||
|
msgid "Do not assign a correspondent"
|
||||||
|
msgstr "Keinen Korrespondenten zuweisen"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:92
|
||||||
|
msgid "Use mail address"
|
||||||
|
msgstr "E-Mail-Adresse benutzen"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:94
|
||||||
|
msgid "Use name (or mail address if not available)"
|
||||||
|
msgstr "Absendername benutzen (oder E-Mail-Adressen, wenn nicht verfügbar)"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:96
|
||||||
|
msgid "Use correspondent selected below"
|
||||||
|
msgstr "Nachfolgend ausgewählten Korrespondent verwenden"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:104
|
||||||
|
msgid "order"
|
||||||
|
msgstr "Reihenfolge"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:111
|
||||||
|
msgid "account"
|
||||||
|
msgstr "Konto"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:115
|
||||||
|
msgid "folder"
|
||||||
|
msgstr "Ordner"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:119
|
||||||
|
msgid "filter from"
|
||||||
|
msgstr "Absender filtern"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:122
|
||||||
|
msgid "filter subject"
|
||||||
|
msgstr "Betreff filtern"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:125
|
||||||
|
msgid "filter body"
|
||||||
|
msgstr "Nachrichteninhalt filtern"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:129
|
||||||
|
msgid "maximum age"
|
||||||
|
msgstr "Maximales Alter"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:131
|
||||||
|
msgid "Specified in days."
|
||||||
|
msgstr "Angegeben in Tagen."
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:134
|
||||||
|
msgid "action"
|
||||||
|
msgstr "Aktion"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:140
|
||||||
|
msgid "action parameter"
|
||||||
|
msgstr "Parameter für Aktion"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:142
|
||||||
|
msgid ""
|
||||||
|
"Additional parameter for the action selected above, i.e., the target folder "
|
||||||
|
"of the move to folder action."
|
||||||
|
msgstr ""
|
||||||
|
"Zusätzlicher Parameter für die oben ausgewählte Aktion, zum Beispiel der "
|
||||||
|
"Zielordner für die Aktion \"In angegebenen Ordner verschieben\""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:148
|
||||||
|
msgid "assign title from"
|
||||||
|
msgstr "Titel zuweisen von"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:158
|
||||||
|
msgid "assign this tag"
|
||||||
|
msgstr "Dieses Tag zuweisen"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:166
|
||||||
|
msgid "assign this document type"
|
||||||
|
msgstr "Diesen Dokumenttyp zuweisen"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:170
|
||||||
|
msgid "assign correspondent from"
|
||||||
|
msgstr "Korrespondent zuweisen von"
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:180
|
||||||
|
msgid "assign this correspondent"
|
||||||
|
msgstr "Diesen Korrespondent zuweisen"
|
546
src/locale/en-us/LC_MESSAGES/django.po
Normal file
546
src/locale/en-us/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
# 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.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2021-01-02 00:26+0000\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"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: documents/apps.py:10
|
||||||
|
msgid "Documents"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:32
|
||||||
|
msgid "Any word"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:33
|
||||||
|
msgid "All words"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:34
|
||||||
|
msgid "Exact match"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:35
|
||||||
|
msgid "Regular expression"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:36
|
||||||
|
msgid "Fuzzy word"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:37
|
||||||
|
msgid "Automatic"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:41 documents/models.py:354 paperless_mail/models.py:25
|
||||||
|
#: paperless_mail/models.py:100
|
||||||
|
msgid "name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:45
|
||||||
|
msgid "match"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:49
|
||||||
|
msgid "matching algorithm"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:55
|
||||||
|
msgid "is insensitive"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:80 documents/models.py:140
|
||||||
|
msgid "correspondent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:81
|
||||||
|
msgid "correspondents"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:103
|
||||||
|
msgid "color"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:107
|
||||||
|
msgid "is inbox tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:109
|
||||||
|
msgid ""
|
||||||
|
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
|
||||||
|
"with inbox tags."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:114
|
||||||
|
msgid "tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:115 documents/models.py:171
|
||||||
|
msgid "tags"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:121 documents/models.py:153
|
||||||
|
msgid "document type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:122
|
||||||
|
msgid "document types"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:130
|
||||||
|
msgid "Unencrypted"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:131
|
||||||
|
msgid "Encrypted with GNU Privacy Guard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:144
|
||||||
|
msgid "title"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:157
|
||||||
|
msgid "content"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:159
|
||||||
|
msgid ""
|
||||||
|
"The raw, text-only data of the document. This field is primarily used for "
|
||||||
|
"searching."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:164
|
||||||
|
msgid "mime type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:175
|
||||||
|
msgid "checksum"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:179
|
||||||
|
msgid "The checksum of the original document."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:183
|
||||||
|
msgid "archive checksum"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:188
|
||||||
|
msgid "The checksum of the archived document."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:192 documents/models.py:332
|
||||||
|
msgid "created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:196
|
||||||
|
msgid "modified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:200
|
||||||
|
msgid "storage type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:208
|
||||||
|
msgid "added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:212
|
||||||
|
msgid "filename"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:217
|
||||||
|
msgid "Current filename in storage"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:221
|
||||||
|
msgid "archive serial number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:226
|
||||||
|
msgid "The position of this document in your physical document archive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:232
|
||||||
|
msgid "document"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:233
|
||||||
|
msgid "documents"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:315
|
||||||
|
msgid "debug"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:316
|
||||||
|
msgid "information"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:317
|
||||||
|
msgid "warning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:318
|
||||||
|
msgid "error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:319
|
||||||
|
msgid "critical"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:323
|
||||||
|
msgid "group"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:326
|
||||||
|
msgid "message"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:329
|
||||||
|
msgid "level"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:336
|
||||||
|
msgid "log"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:337
|
||||||
|
msgid "logs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:348 documents/models.py:398
|
||||||
|
msgid "saved view"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:349
|
||||||
|
msgid "saved views"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:352
|
||||||
|
msgid "user"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:358
|
||||||
|
msgid "show on dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:361
|
||||||
|
msgid "show in sidebar"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:365
|
||||||
|
msgid "sort field"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:368
|
||||||
|
msgid "sort reverse"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:374
|
||||||
|
msgid "title contains"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:375
|
||||||
|
msgid "content contains"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:376
|
||||||
|
msgid "ASN is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:377
|
||||||
|
msgid "correspondent is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:378
|
||||||
|
msgid "document type is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:379
|
||||||
|
msgid "is in inbox"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:380
|
||||||
|
msgid "has tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:381
|
||||||
|
msgid "has any tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:382
|
||||||
|
msgid "created before"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:383
|
||||||
|
msgid "created after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:384
|
||||||
|
msgid "created year is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:385
|
||||||
|
msgid "created month is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:386
|
||||||
|
msgid "created day is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:387
|
||||||
|
msgid "added before"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:388
|
||||||
|
msgid "added after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:389
|
||||||
|
msgid "modified before"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:390
|
||||||
|
msgid "modified after"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:391
|
||||||
|
msgid "does not have tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:402
|
||||||
|
msgid "rule type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:406
|
||||||
|
msgid "value"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:412
|
||||||
|
msgid "filter rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:413
|
||||||
|
msgid "filter rules"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless/settings.py:254
|
||||||
|
msgid "English"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless/settings.py:255
|
||||||
|
msgid "German"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless/urls.py:108
|
||||||
|
msgid "Paperless-ng administration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:24
|
||||||
|
msgid "Filter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:26
|
||||||
|
msgid ""
|
||||||
|
"Paperless will only process mails that match ALL of the filters given below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:34
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:36
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:43
|
||||||
|
msgid "Metadata"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/admin.py:45
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
#: paperless_mail/apps.py:9
|
||||||
|
msgid "Paperless mail"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:11
|
||||||
|
msgid "mail account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:12
|
||||||
|
msgid "mail accounts"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:19
|
||||||
|
msgid "No encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:20
|
||||||
|
msgid "Use SSL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:21
|
||||||
|
msgid "Use STARTTLS"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:29
|
||||||
|
msgid "IMAP server"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:33
|
||||||
|
msgid "IMAP port"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:36
|
||||||
|
msgid ""
|
||||||
|
"This is usually 143 for unencrypted and STARTTLS connections, and 993 for "
|
||||||
|
"SSL connections."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:40
|
||||||
|
msgid "IMAP security"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:46
|
||||||
|
msgid "username"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:50
|
||||||
|
msgid "password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:60
|
||||||
|
msgid "mail rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:61
|
||||||
|
msgid "mail rules"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:69
|
||||||
|
msgid "Mark as read, don't process read mails"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:70
|
||||||
|
msgid "Flag the mail, don't process flagged mails"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:71
|
||||||
|
msgid "Move to specified folder"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:72
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:79
|
||||||
|
msgid "Use subject as title"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:80
|
||||||
|
msgid "Use attachment filename as title"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:90
|
||||||
|
msgid "Do not assign a correspondent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:92
|
||||||
|
msgid "Use mail address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:94
|
||||||
|
msgid "Use name (or mail address if not available)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:96
|
||||||
|
msgid "Use correspondent selected below"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:104
|
||||||
|
msgid "order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:111
|
||||||
|
msgid "account"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:115
|
||||||
|
msgid "folder"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:119
|
||||||
|
msgid "filter from"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:122
|
||||||
|
msgid "filter subject"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:125
|
||||||
|
msgid "filter body"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:129
|
||||||
|
msgid "maximum age"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:131
|
||||||
|
msgid "Specified in days."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:134
|
||||||
|
msgid "action"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:140
|
||||||
|
msgid "action parameter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:142
|
||||||
|
msgid ""
|
||||||
|
"Additional parameter for the action selected above, i.e., the target folder "
|
||||||
|
"of the move to folder action."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:148
|
||||||
|
msgid "assign title from"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:158
|
||||||
|
msgid "assign this tag"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:166
|
||||||
|
msgid "assign this document type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:170
|
||||||
|
msgid "assign correspondent from"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: paperless_mail/models.py:180
|
||||||
|
msgid "assign this correspondent"
|
||||||
|
msgstr ""
|
@ -6,6 +6,8 @@ import re
|
|||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
# Tap paperless.conf if it's available
|
# Tap paperless.conf if it's available
|
||||||
if os.path.exists("../paperless.conf"):
|
if os.path.exists("../paperless.conf"):
|
||||||
load_dotenv("../paperless.conf")
|
load_dotenv("../paperless.conf")
|
||||||
@ -87,6 +89,7 @@ INSTALLED_APPS = [
|
|||||||
"documents.apps.DocumentsConfig",
|
"documents.apps.DocumentsConfig",
|
||||||
"paperless_tesseract.apps.PaperlessTesseractConfig",
|
"paperless_tesseract.apps.PaperlessTesseractConfig",
|
||||||
"paperless_text.apps.PaperlessTextConfig",
|
"paperless_text.apps.PaperlessTextConfig",
|
||||||
|
"paperless_tika.apps.PaperlessTikaConfig",
|
||||||
"paperless_mail.apps.PaperlessMailConfig",
|
"paperless_mail.apps.PaperlessMailConfig",
|
||||||
|
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
@ -124,6 +127,7 @@ MIDDLEWARE = [
|
|||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
@ -253,6 +257,15 @@ if os.getenv("PAPERLESS_DBHOST"):
|
|||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
LANGUAGES = [
|
||||||
|
("en-us", _("English")),
|
||||||
|
("de", _("German"))
|
||||||
|
]
|
||||||
|
|
||||||
|
LOCALE_PATHS = [
|
||||||
|
os.path.join(BASE_DIR, "locale")
|
||||||
|
]
|
||||||
|
|
||||||
TIME_ZONE = os.getenv("PAPERLESS_TIME_ZONE", "UTC")
|
TIME_ZONE = os.getenv("PAPERLESS_TIME_ZONE", "UTC")
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
@ -431,3 +444,10 @@ for t in json.loads(os.getenv("PAPERLESS_FILENAME_PARSE_TRANSFORMS", "[]")):
|
|||||||
PAPERLESS_FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT")
|
PAPERLESS_FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT")
|
||||||
|
|
||||||
THUMBNAIL_FONT_NAME = os.getenv("PAPERLESS_THUMBNAIL_FONT_NAME", "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf")
|
THUMBNAIL_FONT_NAME = os.getenv("PAPERLESS_THUMBNAIL_FONT_NAME", "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf")
|
||||||
|
|
||||||
|
# Tika settings
|
||||||
|
PAPERLESS_TIKA_ENABLED = __get_boolean("PAPERLESS_TIKA_ENABLED", "NO")
|
||||||
|
PAPERLESS_TIKA_ENDPOINT = os.getenv("PAPERLESS_TIKA_ENDPOINT", "http://localhost:9998")
|
||||||
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT = os.getenv(
|
||||||
|
"PAPERLESS_TIKA_GOTENBERG_ENDPOINT", "http://localhost:3000"
|
||||||
|
)
|
||||||
|
@ -7,6 +7,8 @@ from django.views.generic import RedirectView
|
|||||||
from rest_framework.authtoken import views
|
from rest_framework.authtoken import views
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from documents.views import (
|
from documents.views import (
|
||||||
CorrespondentViewSet,
|
CorrespondentViewSet,
|
||||||
DocumentViewSet,
|
DocumentViewSet,
|
||||||
@ -88,7 +90,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Frontend assets TODO: this is pretty bad, but it works.
|
# Frontend assets TODO: this is pretty bad, but it works.
|
||||||
path('assets/<path:path>',
|
path('assets/<path:path>',
|
||||||
RedirectView.as_view(url='/static/frontend/assets/%(path)s')),
|
RedirectView.as_view(url='/static/frontend/en-US/assets/%(path)s')),
|
||||||
|
# TODO: with localization, this is even worse! :/
|
||||||
|
|
||||||
# login, logout
|
# login, logout
|
||||||
path('accounts/', include('django.contrib.auth.urls')),
|
path('accounts/', include('django.contrib.auth.urls')),
|
||||||
@ -102,4 +105,4 @@ admin.site.site_header = 'Paperless-ng'
|
|||||||
# Text at the end of each page's <title>.
|
# Text at the end of each page's <title>.
|
||||||
admin.site.site_title = 'Paperless-ng'
|
admin.site.site_title = 'Paperless-ng'
|
||||||
# Text at the top of the admin index page.
|
# Text at the top of the admin index page.
|
||||||
admin.site.index_title = 'Paperless-ng administration'
|
admin.site.index_title = _('Paperless-ng administration')
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = (0, 9, 10)
|
__version__ = (0, 9, 11)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from paperless_mail.models import MailAccount, MailRule
|
from paperless_mail.models import MailAccount, MailRule
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class MailAccountAdmin(admin.ModelAdmin):
|
class MailAccountAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@ -19,31 +21,31 @@ class MailRuleAdmin(admin.ModelAdmin):
|
|||||||
(None, {
|
(None, {
|
||||||
'fields': ('name', 'order', 'account', 'folder')
|
'fields': ('name', 'order', 'account', 'folder')
|
||||||
}),
|
}),
|
||||||
("Filter", {
|
(_("Filter"), {
|
||||||
'description':
|
'description':
|
||||||
"Paperless will only process mails that match ALL of the "
|
_("Paperless will only process mails that match ALL of the "
|
||||||
"filters given below.",
|
"filters given below."),
|
||||||
'fields':
|
'fields':
|
||||||
('filter_from',
|
('filter_from',
|
||||||
'filter_subject',
|
'filter_subject',
|
||||||
'filter_body',
|
'filter_body',
|
||||||
'maximum_age')
|
'maximum_age')
|
||||||
}),
|
}),
|
||||||
("Actions", {
|
(_("Actions"), {
|
||||||
'description':
|
'description':
|
||||||
"The action applied to the mail. This action is only "
|
_("The action applied to the mail. This action is only "
|
||||||
"performed when documents were consumed from the mail. Mails "
|
"performed when documents were consumed from the mail. "
|
||||||
"without attachments will remain entirely untouched.",
|
"Mails without attachments will remain entirely untouched."),
|
||||||
'fields': (
|
'fields': (
|
||||||
'action',
|
'action',
|
||||||
'action_parameter')
|
'action_parameter')
|
||||||
}),
|
}),
|
||||||
("Metadata", {
|
(_("Metadata"), {
|
||||||
'description':
|
'description':
|
||||||
"Assign metadata to documents consumed from this rule "
|
_("Assign metadata to documents consumed from this rule "
|
||||||
"automatically. If you do not assign tags, types or "
|
"automatically. If you do not assign tags, types or "
|
||||||
"correspondents here, paperless will still process all "
|
"correspondents here, paperless will still process all "
|
||||||
"matching rules that you have defined.",
|
"matching rules that you have defined."),
|
||||||
"fields": (
|
"fields": (
|
||||||
'assign_title_from',
|
'assign_title_from',
|
||||||
'assign_tag',
|
'assign_tag',
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class PaperlessMailConfig(AppConfig):
|
class PaperlessMailConfig(AppConfig):
|
||||||
name = 'paperless_mail'
|
name = 'paperless_mail'
|
||||||
|
|
||||||
verbose_name = 'Paperless Mail'
|
verbose_name = _('Paperless mail')
|
||||||
|
128
src/paperless_mail/migrations/0006_auto_20210101_2340.py
Normal file
128
src/paperless_mail/migrations/0006_auto_20210101_2340.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# Generated by Django 3.1.4 on 2021-01-01 23:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('documents', '1011_auto_20210101_2340'),
|
||||||
|
('paperless_mail', '0005_help_texts'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='mailaccount',
|
||||||
|
options={'verbose_name': 'mail account', 'verbose_name_plural': 'mail accounts'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='mailrule',
|
||||||
|
options={'verbose_name': 'mail rule', 'verbose_name_plural': 'mail rules'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailaccount',
|
||||||
|
name='imap_port',
|
||||||
|
field=models.IntegerField(blank=True, help_text='This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.', null=True, verbose_name='IMAP port'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailaccount',
|
||||||
|
name='imap_security',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, 'No encryption'), (2, 'Use SSL'), (3, 'Use STARTTLS')], default=2, verbose_name='IMAP security'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailaccount',
|
||||||
|
name='imap_server',
|
||||||
|
field=models.CharField(max_length=256, verbose_name='IMAP server'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailaccount',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=256, unique=True, verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailaccount',
|
||||||
|
name='password',
|
||||||
|
field=models.CharField(max_length=256, verbose_name='password'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailaccount',
|
||||||
|
name='username',
|
||||||
|
field=models.CharField(max_length=256, verbose_name='username'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='account',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='paperless_mail.mailaccount', verbose_name='account'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='action',
|
||||||
|
field=models.PositiveIntegerField(choices=[(3, "Mark as read, don't process read mails"), (4, "Flag the mail, don't process flagged mails"), (2, 'Move to specified folder'), (1, 'Delete')], default=3, verbose_name='action'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='action_parameter',
|
||||||
|
field=models.CharField(blank=True, help_text='Additional parameter for the action selected above, i.e., the target folder of the move to folder action.', max_length=256, null=True, verbose_name='action parameter'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='assign_correspondent',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.correspondent', verbose_name='assign this correspondent'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='assign_correspondent_from',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, 'Do not assign a correspondent'), (2, 'Use mail address'), (3, 'Use name (or mail address if not available)'), (4, 'Use correspondent selected below')], default=1, verbose_name='assign correspondent from'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='assign_document_type',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.documenttype', verbose_name='assign this document type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='assign_tag',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.tag', verbose_name='assign this tag'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='assign_title_from',
|
||||||
|
field=models.PositiveIntegerField(choices=[(1, 'Use subject as title'), (2, 'Use attachment filename as title')], default=1, verbose_name='assign title from'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='filter_body',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='filter body'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='filter_from',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='filter from'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='filter_subject',
|
||||||
|
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='filter subject'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='folder',
|
||||||
|
field=models.CharField(default='INBOX', max_length=256, verbose_name='folder'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='maximum_age',
|
||||||
|
field=models.PositiveIntegerField(default=30, help_text='Specified in days.', verbose_name='maximum age'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=256, unique=True, verbose_name='name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='mailrule',
|
||||||
|
name='order',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='order'),
|
||||||
|
),
|
||||||
|
]
|
@ -2,37 +2,53 @@ from django.db import models
|
|||||||
|
|
||||||
import documents.models as document_models
|
import documents.models as document_models
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class MailAccount(models.Model):
|
class MailAccount(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("mail account")
|
||||||
|
verbose_name_plural = _("mail accounts")
|
||||||
|
|
||||||
IMAP_SECURITY_NONE = 1
|
IMAP_SECURITY_NONE = 1
|
||||||
IMAP_SECURITY_SSL = 2
|
IMAP_SECURITY_SSL = 2
|
||||||
IMAP_SECURITY_STARTTLS = 3
|
IMAP_SECURITY_STARTTLS = 3
|
||||||
|
|
||||||
IMAP_SECURITY_OPTIONS = (
|
IMAP_SECURITY_OPTIONS = (
|
||||||
(IMAP_SECURITY_NONE, "No encryption"),
|
(IMAP_SECURITY_NONE, _("No encryption")),
|
||||||
(IMAP_SECURITY_SSL, "Use SSL"),
|
(IMAP_SECURITY_SSL, _("Use SSL")),
|
||||||
(IMAP_SECURITY_STARTTLS, "Use STARTTLS"),
|
(IMAP_SECURITY_STARTTLS, _("Use STARTTLS")),
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=256, unique=True)
|
name = models.CharField(
|
||||||
|
_("name"),
|
||||||
|
max_length=256, unique=True)
|
||||||
|
|
||||||
imap_server = models.CharField(max_length=256)
|
imap_server = models.CharField(
|
||||||
|
_("IMAP server"),
|
||||||
|
max_length=256)
|
||||||
|
|
||||||
imap_port = models.IntegerField(
|
imap_port = models.IntegerField(
|
||||||
|
_("IMAP port"),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text="This is usually 143 for unencrypted and STARTTLS "
|
help_text=_("This is usually 143 for unencrypted and STARTTLS "
|
||||||
"connections, and 993 for SSL connections.")
|
"connections, and 993 for SSL connections."))
|
||||||
|
|
||||||
imap_security = models.PositiveIntegerField(
|
imap_security = models.PositiveIntegerField(
|
||||||
|
_("IMAP security"),
|
||||||
choices=IMAP_SECURITY_OPTIONS,
|
choices=IMAP_SECURITY_OPTIONS,
|
||||||
default=IMAP_SECURITY_SSL
|
default=IMAP_SECURITY_SSL
|
||||||
)
|
)
|
||||||
|
|
||||||
username = models.CharField(max_length=256)
|
username = models.CharField(
|
||||||
|
_("username"),
|
||||||
|
max_length=256)
|
||||||
|
|
||||||
password = models.CharField(max_length=256)
|
password = models.CharField(
|
||||||
|
_("password"),
|
||||||
|
max_length=256)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -40,24 +56,28 @@ class MailAccount(models.Model):
|
|||||||
|
|
||||||
class MailRule(models.Model):
|
class MailRule(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("mail rule")
|
||||||
|
verbose_name_plural = _("mail rules")
|
||||||
|
|
||||||
ACTION_DELETE = 1
|
ACTION_DELETE = 1
|
||||||
ACTION_MOVE = 2
|
ACTION_MOVE = 2
|
||||||
ACTION_MARK_READ = 3
|
ACTION_MARK_READ = 3
|
||||||
ACTION_FLAG = 4
|
ACTION_FLAG = 4
|
||||||
|
|
||||||
ACTIONS = (
|
ACTIONS = (
|
||||||
(ACTION_MARK_READ, "Mark as read, don't process read mails"),
|
(ACTION_MARK_READ, _("Mark as read, don't process read mails")),
|
||||||
(ACTION_FLAG, "Flag the mail, don't process flagged mails"),
|
(ACTION_FLAG, _("Flag the mail, don't process flagged mails")),
|
||||||
(ACTION_MOVE, "Move to specified folder"),
|
(ACTION_MOVE, _("Move to specified folder")),
|
||||||
(ACTION_DELETE, "Delete"),
|
(ACTION_DELETE, _("Delete")),
|
||||||
)
|
)
|
||||||
|
|
||||||
TITLE_FROM_SUBJECT = 1
|
TITLE_FROM_SUBJECT = 1
|
||||||
TITLE_FROM_FILENAME = 2
|
TITLE_FROM_FILENAME = 2
|
||||||
|
|
||||||
TITLE_SELECTOR = (
|
TITLE_SELECTOR = (
|
||||||
(TITLE_FROM_SUBJECT, "Use subject as title"),
|
(TITLE_FROM_SUBJECT, _("Use subject as title")),
|
||||||
(TITLE_FROM_FILENAME, "Use attachment filename as title")
|
(TITLE_FROM_FILENAME, _("Use attachment filename as title"))
|
||||||
)
|
)
|
||||||
|
|
||||||
CORRESPONDENT_FROM_NOTHING = 1
|
CORRESPONDENT_FROM_NOTHING = 1
|
||||||
@ -67,47 +87,65 @@ class MailRule(models.Model):
|
|||||||
|
|
||||||
CORRESPONDENT_SELECTOR = (
|
CORRESPONDENT_SELECTOR = (
|
||||||
(CORRESPONDENT_FROM_NOTHING,
|
(CORRESPONDENT_FROM_NOTHING,
|
||||||
"Do not assign a correspondent"),
|
_("Do not assign a correspondent")),
|
||||||
(CORRESPONDENT_FROM_EMAIL,
|
(CORRESPONDENT_FROM_EMAIL,
|
||||||
"Use mail address"),
|
_("Use mail address")),
|
||||||
(CORRESPONDENT_FROM_NAME,
|
(CORRESPONDENT_FROM_NAME,
|
||||||
"Use name (or mail address if not available)"),
|
_("Use name (or mail address if not available)")),
|
||||||
(CORRESPONDENT_FROM_CUSTOM,
|
(CORRESPONDENT_FROM_CUSTOM,
|
||||||
"Use correspondent selected below")
|
_("Use correspondent selected below"))
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=256, unique=True)
|
name = models.CharField(
|
||||||
|
_("name"),
|
||||||
|
max_length=256, unique=True)
|
||||||
|
|
||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(
|
||||||
|
_("order"),
|
||||||
|
default=0)
|
||||||
|
|
||||||
account = models.ForeignKey(
|
account = models.ForeignKey(
|
||||||
MailAccount,
|
MailAccount,
|
||||||
related_name="rules",
|
related_name="rules",
|
||||||
on_delete=models.CASCADE
|
on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("account")
|
||||||
)
|
)
|
||||||
|
|
||||||
folder = models.CharField(default='INBOX', max_length=256)
|
folder = models.CharField(
|
||||||
|
_("folder"),
|
||||||
|
default='INBOX', max_length=256)
|
||||||
|
|
||||||
filter_from = models.CharField(max_length=256, null=True, blank=True)
|
filter_from = models.CharField(
|
||||||
filter_subject = models.CharField(max_length=256, null=True, blank=True)
|
_("filter from"),
|
||||||
filter_body = models.CharField(max_length=256, null=True, blank=True)
|
max_length=256, null=True, blank=True)
|
||||||
|
filter_subject = models.CharField(
|
||||||
|
_("filter subject"),
|
||||||
|
max_length=256, null=True, blank=True)
|
||||||
|
filter_body = models.CharField(
|
||||||
|
_("filter body"),
|
||||||
|
max_length=256, null=True, blank=True)
|
||||||
|
|
||||||
maximum_age = models.PositiveIntegerField(
|
maximum_age = models.PositiveIntegerField(
|
||||||
|
_("maximum age"),
|
||||||
default=30,
|
default=30,
|
||||||
help_text="Specified in days.")
|
help_text=_("Specified in days."))
|
||||||
|
|
||||||
action = models.PositiveIntegerField(
|
action = models.PositiveIntegerField(
|
||||||
|
_("action"),
|
||||||
choices=ACTIONS,
|
choices=ACTIONS,
|
||||||
default=ACTION_MARK_READ,
|
default=ACTION_MARK_READ,
|
||||||
)
|
)
|
||||||
|
|
||||||
action_parameter = models.CharField(
|
action_parameter = models.CharField(
|
||||||
|
_("action parameter"),
|
||||||
max_length=256, blank=True, null=True,
|
max_length=256, blank=True, null=True,
|
||||||
help_text="Additional parameter for the action selected above, i.e., "
|
help_text=_("Additional parameter for the action selected above, "
|
||||||
"the target folder of the move to folder action."
|
"i.e., "
|
||||||
|
"the target folder of the move to folder action.")
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_title_from = models.PositiveIntegerField(
|
assign_title_from = models.PositiveIntegerField(
|
||||||
|
_("assign title from"),
|
||||||
choices=TITLE_SELECTOR,
|
choices=TITLE_SELECTOR,
|
||||||
default=TITLE_FROM_SUBJECT
|
default=TITLE_FROM_SUBJECT
|
||||||
)
|
)
|
||||||
@ -116,17 +154,20 @@ class MailRule(models.Model):
|
|||||||
document_models.Tag,
|
document_models.Tag,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_("assign this tag"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_document_type = models.ForeignKey(
|
assign_document_type = models.ForeignKey(
|
||||||
document_models.DocumentType,
|
document_models.DocumentType,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_("assign this document type"),
|
||||||
)
|
)
|
||||||
|
|
||||||
assign_correspondent_from = models.PositiveIntegerField(
|
assign_correspondent_from = models.PositiveIntegerField(
|
||||||
|
_("assign correspondent from"),
|
||||||
choices=CORRESPONDENT_SELECTOR,
|
choices=CORRESPONDENT_SELECTOR,
|
||||||
default=CORRESPONDENT_FROM_NOTHING
|
default=CORRESPONDENT_FROM_NOTHING
|
||||||
)
|
)
|
||||||
@ -135,7 +176,8 @@ class MailRule(models.Model):
|
|||||||
document_models.Correspondent,
|
document_models.Correspondent,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name=_("assign this correspondent")
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
import pdftotext
|
import pdftotext
|
||||||
@ -10,7 +9,8 @@ from PIL import Image
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from ocrmypdf import InputFileError, EncryptedPdfError
|
from ocrmypdf import InputFileError, EncryptedPdfError
|
||||||
|
|
||||||
from documents.parsers import DocumentParser, ParseError, run_convert
|
from documents.parsers import DocumentParser, ParseError, \
|
||||||
|
make_thumbnail_from_pdf
|
||||||
|
|
||||||
|
|
||||||
class RasterisedDocumentParser(DocumentParser):
|
class RasterisedDocumentParser(DocumentParser):
|
||||||
@ -47,48 +47,8 @@ class RasterisedDocumentParser(DocumentParser):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def get_thumbnail(self, document_path, mime_type):
|
def get_thumbnail(self, document_path, mime_type):
|
||||||
"""
|
return make_thumbnail_from_pdf(
|
||||||
The thumbnail of a PDF is just a 500px wide image of the first page.
|
document_path, self.tempdir, self.logging_group)
|
||||||
"""
|
|
||||||
|
|
||||||
out_path = os.path.join(self.tempdir, "convert.png")
|
|
||||||
|
|
||||||
# Run convert to get a decent thumbnail
|
|
||||||
try:
|
|
||||||
run_convert(density=300,
|
|
||||||
scale="500x5000>",
|
|
||||||
alpha="remove",
|
|
||||||
strip=True,
|
|
||||||
trim=False,
|
|
||||||
input_file="{}[0]".format(document_path),
|
|
||||||
output_file=out_path,
|
|
||||||
logging_group=self.logging_group)
|
|
||||||
except ParseError:
|
|
||||||
# if convert fails, fall back to extracting
|
|
||||||
# the first PDF page as a PNG using Ghostscript
|
|
||||||
self.log(
|
|
||||||
'warning',
|
|
||||||
"Thumbnail generation with ImageMagick failed, falling back "
|
|
||||||
"to ghostscript. Check your /etc/ImageMagick-x/policy.xml!")
|
|
||||||
gs_out_path = os.path.join(self.tempdir, "gs_out.png")
|
|
||||||
cmd = [settings.GS_BINARY,
|
|
||||||
"-q",
|
|
||||||
"-sDEVICE=pngalpha",
|
|
||||||
"-o", gs_out_path,
|
|
||||||
document_path]
|
|
||||||
if not subprocess.Popen(cmd).wait() == 0:
|
|
||||||
raise ParseError("Thumbnail (gs) failed at {}".format(cmd))
|
|
||||||
# then run convert on the output from gs
|
|
||||||
run_convert(density=300,
|
|
||||||
scale="500x5000>",
|
|
||||||
alpha="remove",
|
|
||||||
strip=True,
|
|
||||||
trim=False,
|
|
||||||
input_file=gs_out_path,
|
|
||||||
output_file=out_path,
|
|
||||||
logging_group=self.logging_group)
|
|
||||||
|
|
||||||
return out_path
|
|
||||||
|
|
||||||
def is_image(self, mime_type):
|
def is_image(self, mime_type):
|
||||||
return mime_type in [
|
return mime_type in [
|
||||||
@ -128,7 +88,7 @@ class RasterisedDocumentParser(DocumentParser):
|
|||||||
f"Error while calculating DPI for image {image}: {e}")
|
f"Error while calculating DPI for image {image}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def parse(self, document_path, mime_type):
|
def parse(self, document_path, mime_type, file_name=None):
|
||||||
mode = settings.OCR_MODE
|
mode = settings.OCR_MODE
|
||||||
|
|
||||||
text_original = get_text_from_pdf(document_path)
|
text_original = get_text_from_pdf(document_path)
|
||||||
|
@ -78,7 +78,7 @@ class TestParser(DirectoriesMixin, TestCase):
|
|||||||
parser.get_thumbnail(os.path.join(self.SAMPLE_FILES, 'simple-digital.pdf'), "application/pdf")
|
parser.get_thumbnail(os.path.join(self.SAMPLE_FILES, 'simple-digital.pdf'), "application/pdf")
|
||||||
# dont really know how to test it, just call it and assert that it does not raise anything.
|
# dont really know how to test it, just call it and assert that it does not raise anything.
|
||||||
|
|
||||||
@mock.patch("paperless_tesseract.parsers.run_convert")
|
@mock.patch("documents.parsers.run_convert")
|
||||||
def test_thumbnail_fallback(self, m):
|
def test_thumbnail_fallback(self, m):
|
||||||
|
|
||||||
def call_convert(input_file, output_file, **kwargs):
|
def call_convert(input_file, output_file, **kwargs):
|
||||||
|
@ -32,6 +32,6 @@ class TextDocumentParser(DocumentParser):
|
|||||||
|
|
||||||
return out_path
|
return out_path
|
||||||
|
|
||||||
def parse(self, document_path, mime_type):
|
def parse(self, document_path, mime_type, file_name=None):
|
||||||
with open(document_path, 'r') as f:
|
with open(document_path, 'r') as f:
|
||||||
self.text = f.read()
|
self.text = f.read()
|
||||||
|
14
src/paperless_tika/apps.py
Normal file
14
src/paperless_tika/apps.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
from django.conf import settings
|
||||||
|
from paperless_tika.signals import tika_consumer_declaration
|
||||||
|
|
||||||
|
|
||||||
|
class PaperlessTikaConfig(AppConfig):
|
||||||
|
name = "paperless_tika"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from documents.signals import document_consumer_declaration
|
||||||
|
|
||||||
|
if settings.PAPERLESS_TIKA_ENABLED:
|
||||||
|
document_consumer_declaration.connect(tika_consumer_declaration)
|
||||||
|
AppConfig.ready(self)
|
87
src/paperless_tika/parsers.py
Normal file
87
src/paperless_tika/parsers.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import dateutil.parser
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from documents.parsers import DocumentParser, ParseError, \
|
||||||
|
make_thumbnail_from_pdf
|
||||||
|
from tika import parser
|
||||||
|
|
||||||
|
|
||||||
|
class TikaDocumentParser(DocumentParser):
|
||||||
|
"""
|
||||||
|
This parser sends documents to a local tika server
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_thumbnail(self, document_path, mime_type):
|
||||||
|
if not self.archive_path:
|
||||||
|
self.archive_path = self.convert_to_pdf(document_path)
|
||||||
|
|
||||||
|
return make_thumbnail_from_pdf(
|
||||||
|
self.archive_path, self.tempdir, self.logging_group)
|
||||||
|
|
||||||
|
def extract_metadata(self, document_path, mime_type):
|
||||||
|
tika_server = settings.PAPERLESS_TIKA_ENDPOINT
|
||||||
|
try:
|
||||||
|
parsed = parser.from_file(document_path, tika_server)
|
||||||
|
except Exception as e:
|
||||||
|
self.log("warning", f"Error while fetching document metadata for "
|
||||||
|
f"{document_path}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"namespace": "",
|
||||||
|
"prefix": "",
|
||||||
|
"key": key,
|
||||||
|
"value": parsed['metadata'][key]
|
||||||
|
} for key in parsed['metadata']
|
||||||
|
]
|
||||||
|
|
||||||
|
def parse(self, document_path, mime_type, file_name=None):
|
||||||
|
self.log("info", f"Sending {document_path} to Tika server")
|
||||||
|
tika_server = settings.PAPERLESS_TIKA_ENDPOINT
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = parser.from_file(document_path, tika_server)
|
||||||
|
except Exception as err:
|
||||||
|
raise ParseError(
|
||||||
|
f"Could not parse {document_path} with tika server at "
|
||||||
|
f"{tika_server}: {err}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.text = parsed["content"].strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.date = dateutil.parser.isoparse(
|
||||||
|
parsed["metadata"]["Creation-Date"])
|
||||||
|
except Exception as e:
|
||||||
|
self.log("warning", f"Unable to extract date for document "
|
||||||
|
f"{document_path}: {e}")
|
||||||
|
|
||||||
|
self.archive_path = self.convert_to_pdf(document_path, file_name)
|
||||||
|
|
||||||
|
def convert_to_pdf(self, document_path, file_name):
|
||||||
|
pdf_path = os.path.join(self.tempdir, "convert.pdf")
|
||||||
|
gotenberg_server = settings.PAPERLESS_TIKA_GOTENBERG_ENDPOINT
|
||||||
|
url = gotenberg_server + "/convert/office"
|
||||||
|
|
||||||
|
self.log("info", f"Converting {document_path} to PDF as {pdf_path}")
|
||||||
|
files = {"files": (file_name or os.path.basename(document_path),
|
||||||
|
open(document_path, "rb"))}
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, files=files, headers=headers)
|
||||||
|
response.raise_for_status() # ensure we notice bad responses
|
||||||
|
except Exception as err:
|
||||||
|
raise ParseError(
|
||||||
|
f"Error while converting document to PDF: {err}"
|
||||||
|
)
|
||||||
|
|
||||||
|
file = open(pdf_path, "wb")
|
||||||
|
file.write(response.content)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
return pdf_path
|
20
src/paperless_tika/signals.py
Normal file
20
src/paperless_tika/signals.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from .parsers import TikaDocumentParser
|
||||||
|
|
||||||
|
|
||||||
|
def tika_consumer_declaration(sender, **kwargs):
|
||||||
|
return {
|
||||||
|
"parser": TikaDocumentParser,
|
||||||
|
"weight": 10,
|
||||||
|
"mime_types": {
|
||||||
|
"application/msword": ".doc",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx", # NOQA: E501
|
||||||
|
"application/vnd.ms-excel": ".xls",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx", # NOQA: E501
|
||||||
|
"application/vnd.ms-powerpoint": ".ppt",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx", # NOQA: E501
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.slideshow": ".ppsx", # NOQA: E501
|
||||||
|
"application/vnd.oasis.opendocument.presentation": ".odp",
|
||||||
|
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
|
||||||
|
"application/vnd.oasis.opendocument.text": ".odt",
|
||||||
|
},
|
||||||
|
}
|
60
src/paperless_tika/tests/test_tika_parser.py
Normal file
60
src/paperless_tika/tests/test_tika_parser.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from requests import Response
|
||||||
|
|
||||||
|
from paperless_tika.parsers import TikaDocumentParser
|
||||||
|
|
||||||
|
|
||||||
|
class TestTikaParser(TestCase):
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.parser = TikaDocumentParser(logging_group=None)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.parser.cleanup()
|
||||||
|
|
||||||
|
@mock.patch("paperless_tika.parsers.parser.from_file")
|
||||||
|
@mock.patch("paperless_tika.parsers.requests.post")
|
||||||
|
def test_parse(self, post, from_file):
|
||||||
|
from_file.return_value = {
|
||||||
|
"content": "the content",
|
||||||
|
"metadata": {
|
||||||
|
"Creation-Date": "2020-11-21"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response = Response()
|
||||||
|
response._content = b"PDF document"
|
||||||
|
response.status_code = 200
|
||||||
|
post.return_value = response
|
||||||
|
|
||||||
|
file = os.path.join(self.parser.tempdir, "input.odt")
|
||||||
|
Path(file).touch()
|
||||||
|
self.parser.parse(file, "application/vnd.oasis.opendocument.text")
|
||||||
|
|
||||||
|
self.assertEqual(self.parser.text, "the content")
|
||||||
|
self.assertIsNotNone(self.parser.archive_path)
|
||||||
|
with open(self.parser.archive_path, "rb") as f:
|
||||||
|
self.assertEqual(f.read(), b"PDF document")
|
||||||
|
|
||||||
|
self.assertEqual(self.parser.date, datetime.datetime(2020, 11, 21))
|
||||||
|
|
||||||
|
@mock.patch("paperless_tika.parsers.parser.from_file")
|
||||||
|
def test_metadata(self, from_file):
|
||||||
|
from_file.return_value = {
|
||||||
|
"metadata": {
|
||||||
|
"Creation-Date": "2020-11-21",
|
||||||
|
"Some-key": "value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file = os.path.join(self.parser.tempdir, "input.odt")
|
||||||
|
Path(file).touch()
|
||||||
|
|
||||||
|
metadata = self.parser.extract_metadata(file, "application/vnd.oasis.opendocument.text")
|
||||||
|
|
||||||
|
self.assertTrue("Creation-Date" in [m['key'] for m in metadata])
|
||||||
|
self.assertTrue("Some-key" in [m['key'] for m in metadata])
|
Loading…
x
Reference in New Issue
Block a user