mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-07 19:08:32 -05:00
Compare commits
276 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f9ce4d8f6a | ||
![]() |
8c9a74ee0c | ||
![]() |
0b59ef2cfa | ||
![]() |
0099631905 | ||
![]() |
4548038525 | ||
![]() |
a2b7687c3b | ||
![]() |
15cba8e14d | ||
![]() |
605f86f0cf | ||
![]() |
8cbaca22c1 | ||
![]() |
4269074944 | ||
![]() |
7b7331683d | ||
![]() |
a83637b2bf | ||
![]() |
721447999e | ||
![]() |
72cbdca6e8 | ||
![]() |
22e060e00e | ||
![]() |
23fb5c2a1f | ||
![]() |
d6e6f49c15 | ||
![]() |
fc259c8bfd | ||
![]() |
ecf90c4718 | ||
![]() |
f0b359889e | ||
![]() |
b0fb44db86 | ||
![]() |
bfd955b210 | ||
![]() |
8af21d6fe3 | ||
![]() |
e9f25190e9 | ||
![]() |
e81b829eb0 | ||
![]() |
5f4e5c2cfb | ||
![]() |
383358376f | ||
![]() |
00f0b55729 | ||
![]() |
b10b981cb5 | ||
![]() |
f805407bce | ||
![]() |
4f169da4a8 | ||
![]() |
4031381c31 | ||
![]() |
b7bc3830cc | ||
![]() |
f2872d6475 | ||
![]() |
f5219c101c | ||
![]() |
f329b5a3d0 | ||
![]() |
6f6a5f2eed | ||
![]() |
266a8cd1a9 | ||
![]() |
7675014c90 | ||
![]() |
ec5971c134 | ||
![]() |
d7fedfcd87 | ||
![]() |
cb99a8741e | ||
![]() |
a63ed236a4 | ||
![]() |
4594a5c41c | ||
![]() |
9109c25b3e | ||
![]() |
5435dc2499 | ||
![]() |
1d300fafad | ||
![]() |
b1194f9524 | ||
![]() |
2532bd1e2c | ||
![]() |
800e842ab3 | ||
![]() |
6f6f365e2b | ||
![]() |
43b863b816 | ||
![]() |
94e32005ca | ||
![]() |
204e14877d | ||
![]() |
92f05e051f | ||
![]() |
17bdf2a233 | ||
![]() |
ce37100a0a | ||
![]() |
81c371d66b | ||
![]() |
c114653977 | ||
![]() |
6280b9948a | ||
![]() |
3a322a7b33 | ||
![]() |
327ae03589 | ||
![]() |
cffbea9053 | ||
![]() |
ad0ef9a5a8 | ||
![]() |
03e7299925 | ||
![]() |
d3ba910f2d | ||
![]() |
329e649878 | ||
![]() |
206ee97554 | ||
![]() |
e512d4af8c | ||
![]() |
6a5e752172 | ||
![]() |
6ba527ef55 | ||
![]() |
220cc1927c | ||
![]() |
9956f4cb47 | ||
![]() |
fe055f6391 | ||
![]() |
eec506a13c | ||
![]() |
1530bbd1cb | ||
![]() |
db6afdd926 | ||
![]() |
5642715721 | ||
![]() |
3bd22f0b0f | ||
![]() |
c92c7e1ced | ||
![]() |
940f5d5b50 | ||
![]() |
4dc893a4fa | ||
![]() |
6d324dbd8e | ||
![]() |
8ddf05e573 | ||
![]() |
d869a6bcca | ||
![]() |
40bdeffa38 | ||
![]() |
5bf5710d39 | ||
![]() |
551a7e606c | ||
![]() |
feec36939b | ||
![]() |
554bba839e | ||
![]() |
ebaaa3a1e8 | ||
![]() |
bae715cd34 | ||
![]() |
2a3b8f5a7f | ||
![]() |
6a023507e2 | ||
![]() |
1551052cde | ||
![]() |
b6dd36a439 | ||
![]() |
ce38e4ae08 | ||
![]() |
97d6503fef | ||
![]() |
b0625cdced | ||
![]() |
31e8c44c18 | ||
![]() |
0472dfe25a | ||
![]() |
8b36c9ad64 | ||
![]() |
1266f2d5b9 | ||
![]() |
8196051959 | ||
![]() |
d198142a1e | ||
![]() |
5e15ede849 | ||
![]() |
06a6eb0326 | ||
![]() |
28819d6d0f | ||
![]() |
8a9e564dac | ||
![]() |
534704693b | ||
![]() |
bc40607c51 | ||
![]() |
6fdc17cc72 | ||
![]() |
69a5ba0618 | ||
![]() |
3c71a9160f | ||
![]() |
48ef8eca80 | ||
![]() |
812df3782a | ||
![]() |
54bb1ae27d | ||
![]() |
ff4a8b37bd | ||
![]() |
37d3a624b7 | ||
![]() |
493f6173da | ||
![]() |
272e87b741 | ||
![]() |
2b5e6f7a9d | ||
![]() |
70960f86ba | ||
![]() |
ee4d25567c | ||
![]() |
80a126e838 | ||
![]() |
c02bd66b3f | ||
![]() |
cea6720c1a | ||
![]() |
700d58058c | ||
![]() |
33e413af65 | ||
![]() |
45a13523d4 | ||
![]() |
95257d5723 | ||
![]() |
8da3ae2c53 | ||
![]() |
f17b541a5b | ||
![]() |
2b2e518dea | ||
![]() |
3f6e3a2750 | ||
![]() |
14784d5832 | ||
![]() |
8cd5e25364 | ||
![]() |
7788d93227 | ||
![]() |
826503802a | ||
![]() |
6db1e36e14 | ||
![]() |
3bc4d7dad7 | ||
![]() |
32d546740b | ||
![]() |
24da3e5034 | ||
![]() |
9e295ddf4f | ||
![]() |
eff6f2fb01 | ||
![]() |
c597da495c | ||
![]() |
de5e9c95ec | ||
![]() |
4e27242373 | ||
![]() |
7bf1e24616 | ||
![]() |
fd0759bf6f | ||
![]() |
d6bbf2cc8d | ||
![]() |
80495d42de | ||
![]() |
eac21f773f | ||
![]() |
52f5831657 | ||
![]() |
f35f33539a | ||
![]() |
46f310603b | ||
![]() |
531d3f03f9 | ||
![]() |
85cfd7610d | ||
![]() |
201b77189a | ||
![]() |
5b76b45e33 | ||
![]() |
bf2fac9393 | ||
![]() |
5a3affe8c0 | ||
![]() |
a5834393b3 | ||
![]() |
cd6e37c520 | ||
![]() |
af51165229 | ||
![]() |
d480620be9 | ||
![]() |
d470de3576 | ||
![]() |
538249b26c | ||
![]() |
fb9d3f736b | ||
![]() |
a6b7beaf6b | ||
![]() |
4d4d545343 | ||
![]() |
049dc17902 | ||
![]() |
b0ca57a7f0 | ||
![]() |
cdd49c5142 | ||
![]() |
4b31e5d0b4 | ||
![]() |
8076ebd78c | ||
![]() |
c864b3cd19 | ||
![]() |
2704bcb979 | ||
![]() |
ce9f604d81 | ||
![]() |
4f876db5d1 | ||
![]() |
5e5f56dc67 | ||
![]() |
93fab8bb95 | ||
![]() |
35ca2195fe | ||
![]() |
7ace66d7fd | ||
![]() |
4f9a31244b | ||
![]() |
14cf4f7095 | ||
![]() |
8bd7c27826 | ||
![]() |
8c4f486fe9 | ||
![]() |
2849414445 | ||
![]() |
ea1ea0816f | ||
![]() |
52d3a8703c | ||
![]() |
4cb4d6adcd | ||
![]() |
24444237f2 | ||
![]() |
40c8629aef | ||
![]() |
98cdf614a5 | ||
![]() |
2eb2d99a91 | ||
![]() |
18ad9bcbf2 | ||
![]() |
997bff4917 | ||
![]() |
78f9a80895 | ||
![]() |
9231df7a4a | ||
![]() |
6f25917c86 | ||
![]() |
c41d1a78a8 | ||
![]() |
c3331086d5 | ||
![]() |
fe2db4dbf7 | ||
![]() |
47c88a6bdd | ||
![]() |
2c1333a75f | ||
![]() |
3c48ce0225 | ||
![]() |
4aa318598f | ||
![]() |
00f39d8b58 | ||
![]() |
0b1a16908f | ||
![]() |
d9796e5003 | ||
![]() |
3599bb52c0 | ||
![]() |
af8a6c3764 | ||
![]() |
6d37ebf79e | ||
![]() |
f6a70b85f4 | ||
![]() |
538a4219bd | ||
![]() |
85c41b79be | ||
![]() |
df101f5e7a | ||
![]() |
1fa735eb23 | ||
![]() |
ebe21a0114 | ||
![]() |
d132eba143 | ||
![]() |
073c3c8fed | ||
![]() |
e3c1bde793 | ||
![]() |
b68906b14e | ||
![]() |
681eecc46e | ||
![]() |
1578e8de2d | ||
![]() |
b01cbc9aa0 | ||
![]() |
acd3832417 | ||
![]() |
82b2ba3cc2 | ||
![]() |
3de6e0bcf1 | ||
![]() |
6df73ae940 | ||
![]() |
2204090151 | ||
![]() |
3c81a7468b | ||
![]() |
5ef86f9489 | ||
![]() |
90cb0836bb | ||
![]() |
ef1d4264b5 | ||
![]() |
e1fa59122d | ||
![]() |
5bf26369e2 | ||
![]() |
36239ba09f | ||
![]() |
318c1d2fbd | ||
![]() |
e7c40fc3dc | ||
![]() |
0da0b1c062 | ||
![]() |
08988e11f8 | ||
![]() |
30372b0e85 | ||
![]() |
567e89d1c7 | ||
![]() |
f1f5227ccd | ||
![]() |
09b5bd17f2 | ||
![]() |
e384bd78c5 | ||
![]() |
fda844f64c | ||
![]() |
daf90399bd | ||
![]() |
3d37e49c1a | ||
![]() |
261c6fb990 | ||
![]() |
cdd2b99b6b | ||
![]() |
d0a0ae91c4 | ||
![]() |
c04b9fd7f6 | ||
![]() |
6809b15ce1 | ||
![]() |
c317eca1ca | ||
![]() |
466afa8203 | ||
![]() |
c2e3dc76d9 | ||
![]() |
5a899664f8 | ||
![]() |
990e905a04 | ||
![]() |
6b7155a849 | ||
![]() |
47851ddd3f | ||
![]() |
47189643ff | ||
![]() |
c1efe11cf3 | ||
![]() |
0e40ef5f35 | ||
![]() |
c8081595c4 | ||
![]() |
a2b5b3b253 | ||
![]() |
790bcf05ed | ||
![]() |
d8d2d53c59 | ||
![]() |
027897ff03 | ||
![]() |
cca576f518 | ||
![]() |
5fcf1b5434 | ||
![]() |
942b5aa9df | ||
![]() |
c05b39a056 | ||
![]() |
3c8196527f |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"qpdf": {
|
"qpdf": {
|
||||||
"version": "11.1.1"
|
"version": "11.2.0"
|
||||||
},
|
},
|
||||||
"jbig2enc": {
|
"jbig2enc": {
|
||||||
"version": "0.29",
|
"version": "0.29",
|
||||||
|
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -6,7 +6,7 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Have a question? 👉 [Start a new discussion](https://github.com/paperless-ngx/paperless-ngx/discussions/new) or [ask in chat](https://matrix.to/#/#paperless:adnidor.de).
|
Have a question? 👉 [Start a new discussion](https://github.com/paperless-ngx/paperless-ngx/discussions/new) or [ask in chat](https://matrix.to/#/#paperlessngx:matrix.org).
|
||||||
|
|
||||||
Before opening an issue, please double check:
|
Before opening an issue, please double check:
|
||||||
|
|
||||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -4,7 +4,7 @@ contact_links:
|
|||||||
url: https://github.com/paperless-ngx/paperless-ngx/discussions
|
url: https://github.com/paperless-ngx/paperless-ngx/discussions
|
||||||
about: This issue tracker is not for support questions. Please refer to our Discussions.
|
about: This issue tracker is not for support questions. Please refer to our Discussions.
|
||||||
- name: 💬 Chat
|
- name: 💬 Chat
|
||||||
url: https://matrix.to/#/#paperless:adnidor.de
|
url: https://matrix.to/#/#paperlessngx:matrix.org
|
||||||
about: Want to discuss Paperless-ngx with others? Check out our chat.
|
about: Want to discuss Paperless-ngx with others? Check out our chat.
|
||||||
- name: 🚀 Feature Request
|
- name: 🚀 Feature Request
|
||||||
url: https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=feature-requests
|
url: https://github.com/paperless-ngx/paperless-ngx/discussions/new?category=feature-requests
|
||||||
|
12
.github/release-drafter.yml
vendored
12
.github/release-drafter.yml
vendored
@@ -4,6 +4,7 @@ autolabeler:
|
|||||||
- '/^fix/'
|
- '/^fix/'
|
||||||
title:
|
title:
|
||||||
- "/^fix/i"
|
- "/^fix/i"
|
||||||
|
- "/^Bugfix/i"
|
||||||
- label: "enhancement"
|
- label: "enhancement"
|
||||||
branch:
|
branch:
|
||||||
- '/^feature/'
|
- '/^feature/'
|
||||||
@@ -13,6 +14,9 @@ categories:
|
|||||||
- title: 'Breaking Changes'
|
- title: 'Breaking Changes'
|
||||||
labels:
|
labels:
|
||||||
- 'breaking-change'
|
- 'breaking-change'
|
||||||
|
- title: 'Notable Changes'
|
||||||
|
labels:
|
||||||
|
- 'notable'
|
||||||
- title: 'Features'
|
- title: 'Features'
|
||||||
labels:
|
labels:
|
||||||
- 'enhancement'
|
- 'enhancement'
|
||||||
@@ -20,7 +24,8 @@ categories:
|
|||||||
labels:
|
labels:
|
||||||
- 'bug'
|
- 'bug'
|
||||||
- title: 'Documentation'
|
- title: 'Documentation'
|
||||||
label: 'documentation'
|
labels:
|
||||||
|
- 'documentation'
|
||||||
- title: 'Maintenance'
|
- title: 'Maintenance'
|
||||||
labels:
|
labels:
|
||||||
- 'chore'
|
- 'chore'
|
||||||
@@ -29,7 +34,8 @@ categories:
|
|||||||
- 'ci-cd'
|
- 'ci-cd'
|
||||||
- title: 'Dependencies'
|
- title: 'Dependencies'
|
||||||
collapse-after: 3
|
collapse-after: 3
|
||||||
label: 'dependencies'
|
labels:
|
||||||
|
- 'dependencies'
|
||||||
- title: 'All App Changes'
|
- title: 'All App Changes'
|
||||||
labels:
|
labels:
|
||||||
- 'frontend'
|
- 'frontend'
|
||||||
@@ -46,6 +52,8 @@ include-labels:
|
|||||||
- 'frontend'
|
- 'frontend'
|
||||||
- 'backend'
|
- 'backend'
|
||||||
- 'ci-cd'
|
- 'ci-cd'
|
||||||
|
- 'breaking-change'
|
||||||
|
- 'notable'
|
||||||
category-template: '### $TITLE'
|
category-template: '### $TITLE'
|
||||||
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
||||||
change-title-escapes: '\<*_&#@'
|
change-title-escapes: '\<*_&#@'
|
||||||
|
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
pre-commit:
|
pre-commit:
|
||||||
name: Linting Checks
|
name: Linting Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout repository
|
name: Checkout repository
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
documentation:
|
documentation:
|
||||||
name: "Build Documentation"
|
name: "Build Documentation"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install pipenv
|
name: Install pipenv
|
||||||
run: |
|
run: |
|
||||||
pipx install pipenv==2022.10.12
|
pipx install pipenv==2022.11.30
|
||||||
-
|
-
|
||||||
name: Set up Python
|
name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
|
|
||||||
documentation-deploy:
|
documentation-deploy:
|
||||||
name: "Deploy Documentation"
|
name: "Deploy Documentation"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
needs:
|
needs:
|
||||||
- documentation
|
- documentation
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
|
|
||||||
tests-backend:
|
tests-backend:
|
||||||
name: "Tests (${{ matrix.python-version }})"
|
name: "Tests (${{ matrix.python-version }})"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
strategy:
|
strategy:
|
||||||
@@ -106,6 +106,10 @@ jobs:
|
|||||||
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
||||||
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
||||||
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
||||||
|
# Skip Tests which require convert
|
||||||
|
PAPERLESS_TEST_SKIP_CONVERT: 1
|
||||||
|
# Enable Gotenberg end to end testing
|
||||||
|
GOTENBERG_LIVE: 1
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -120,7 +124,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install pipenv
|
name: Install pipenv
|
||||||
run: |
|
run: |
|
||||||
pipx install pipenv==2022.10.12
|
pipx install pipenv==2022.11.30
|
||||||
-
|
-
|
||||||
name: Set up Python
|
name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
@@ -177,7 +181,7 @@ jobs:
|
|||||||
|
|
||||||
tests-frontend:
|
tests-frontend:
|
||||||
name: "Tests Frontend"
|
name: "Tests Frontend"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
strategy:
|
strategy:
|
||||||
@@ -191,13 +195,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: cd src-ui && npm ci
|
- run: cd src-ui && npm ci
|
||||||
|
- run: cd src-ui && npm run lint
|
||||||
- run: cd src-ui && npm run test
|
- run: cd src-ui && npm run test
|
||||||
- run: cd src-ui && npm run e2e:ci
|
- run: cd src-ui && npm run e2e:ci
|
||||||
|
|
||||||
prepare-docker-build:
|
prepare-docker-build:
|
||||||
name: Prepare Docker Pipeline Data
|
name: Prepare Docker Pipeline Data
|
||||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v'))
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
# If the push triggered the installer library workflow, wait for it to
|
# If the push triggered the installer library workflow, wait for it to
|
||||||
# complete here. This ensures the required versions for the final
|
# complete here. This ensures the required versions for the final
|
||||||
# image have been built, while not waiting at all if the versions haven't changed
|
# image have been built, while not waiting at all if the versions haven't changed
|
||||||
@@ -274,7 +279,7 @@ jobs:
|
|||||||
|
|
||||||
# build and push image to docker hub.
|
# build and push image to docker hub.
|
||||||
build-docker-image:
|
build-docker-image:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
group: ${{ github.workflow }}-build-docker-image-${{ github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -379,7 +384,7 @@ jobs:
|
|||||||
build-release:
|
build-release:
|
||||||
needs:
|
needs:
|
||||||
- build-docker-image
|
- build-docker-image
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -458,7 +463,7 @@ jobs:
|
|||||||
path: dist/paperless-ngx.tar.xz
|
path: dist/paperless-ngx.tar.xz
|
||||||
|
|
||||||
publish-release:
|
publish-release:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
||||||
changelog: ${{ steps.create-release.outputs.body }}
|
changelog: ${{ steps.create-release.outputs.body }}
|
||||||
@@ -507,7 +512,7 @@ jobs:
|
|||||||
asset_content_type: application/x-xz
|
asset_content_type: application/x-xz
|
||||||
|
|
||||||
append-changelog:
|
append-changelog:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- publish-release
|
- publish-release
|
||||||
if: needs.publish-release.outputs.prerelease == 'false'
|
if: needs.publish-release.outputs.prerelease == 'false'
|
||||||
|
12
.github/workflows/cleanup-tags.yml
vendored
12
.github/workflows/cleanup-tags.yml
vendored
@@ -1,17 +1,14 @@
|
|||||||
# This workflow runs on certain conditions to check for and potentially
|
# This workflow runs on certain conditions to check for and potentially
|
||||||
# delete container images from the GHCR which no longer have an associated
|
# delete container images from the GHCR which no longer have an associated
|
||||||
# code branch.
|
# code branch.
|
||||||
# Requires a PAT with the correct scope set in the secrets
|
# Requires a PAT with the correct scope set in the secrets.
|
||||||
|
#
|
||||||
|
# This workflow will not trigger runs on forked repos.
|
||||||
|
|
||||||
name: Cleanup Image Tags
|
name: Cleanup Image Tags
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * SAT'
|
|
||||||
delete:
|
delete:
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- closed
|
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- ".github/workflows/cleanup-tags.yml"
|
- ".github/workflows/cleanup-tags.yml"
|
||||||
@@ -26,7 +23,8 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
cleanup-images:
|
cleanup-images:
|
||||||
name: Cleanup Image Tags for ${{ matrix.primary-name }}
|
name: Cleanup Image Tags for ${{ matrix.primary-name }}
|
||||||
runs-on: ubuntu-latest
|
if: github.repository_owner == 'paperless-ngx'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
3
.github/workflows/installer-library.yml
vendored
3
.github/workflows/installer-library.yml
vendored
@@ -34,7 +34,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
prepare-docker-build:
|
prepare-docker-build:
|
||||||
name: Prepare Docker Image Version Data
|
name: Prepare Docker Image Version Data
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Set ghcr repository name
|
name: Set ghcr repository name
|
||||||
@@ -127,6 +127,7 @@ jobs:
|
|||||||
uses: ./.github/workflows/reusable-workflow-builder.yml
|
uses: ./.github/workflows/reusable-workflow-builder.yml
|
||||||
with:
|
with:
|
||||||
dockerfile: ./docker-builders/Dockerfile.qpdf
|
dockerfile: ./docker-builders/Dockerfile.qpdf
|
||||||
|
build-platforms: linux/amd64
|
||||||
build-json: ${{ needs.prepare-docker-build.outputs.qpdf-json }}
|
build-json: ${{ needs.prepare-docker-build.outputs.qpdf-json }}
|
||||||
build-args: |
|
build-args: |
|
||||||
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
QPDF_VERSION=${{ fromJSON(needs.prepare-docker-build.outputs.qpdf-json).version }}
|
||||||
|
4
.github/workflows/project-actions.yml
vendored
4
.github/workflows/project-actions.yml
vendored
@@ -24,7 +24,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
issue_opened_or_reopened:
|
issue_opened_or_reopened:
|
||||||
name: issue_opened_or_reopened
|
name: issue_opened_or_reopened
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
|
||||||
steps:
|
steps:
|
||||||
- name: Add issue to project and set status to ${{ env.todo }}
|
- name: Add issue to project and set status to ${{ env.todo }}
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
status_value: ${{ env.todo }} # Target status
|
status_value: ${{ env.todo }} # Target status
|
||||||
pr_opened_or_reopened:
|
pr_opened_or_reopened:
|
||||||
name: pr_opened_or_reopened
|
name: pr_opened_or_reopened
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
permissions:
|
||||||
# write permission is required for autolabeler
|
# write permission is required for autolabeler
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
2
.github/workflows/release-chart.yml
vendored
2
.github/workflows/release-chart.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release_chart:
|
release_chart:
|
||||||
name: "Release Chart"
|
name: "Release Chart"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@@ -13,6 +13,10 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
type: string
|
type: string
|
||||||
|
build-platforms:
|
||||||
|
required: false
|
||||||
|
default: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
type: string
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ fromJSON(inputs.build-json).name }}-${{ fromJSON(inputs.build-json).version }}
|
group: ${{ github.workflow }}-${{ fromJSON(inputs.build-json).name }}-${{ fromJSON(inputs.build-json).version }}
|
||||||
@@ -21,7 +25,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
build-image:
|
build-image:
|
||||||
name: Build ${{ fromJSON(inputs.build-json).name }} @ ${{ fromJSON(inputs.build-json).version }}
|
name: Build ${{ fromJSON(inputs.build-json).name }} @ ${{ fromJSON(inputs.build-json).version }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -46,7 +50,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ${{ inputs.dockerfile }}
|
file: ${{ inputs.dockerfile }}
|
||||||
tags: ${{ fromJSON(inputs.build-json).image_tag }}
|
tags: ${{ fromJSON(inputs.build-json).image_tag }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: ${{ inputs.build-platforms }}
|
||||||
build-args: ${{ inputs.build-args }}
|
build-args: ${{ inputs.build-args }}
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=registry,ref=${{ fromJSON(inputs.build-json).cache_tag }}
|
cache-from: type=registry,ref=${{ fromJSON(inputs.build-json).cache_tag }}
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
repos:
|
repos:
|
||||||
# General hooks
|
# General hooks
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.3.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-docstring-first
|
- id: check-docstring-first
|
||||||
- id: check-json
|
- id: check-json
|
||||||
@@ -48,23 +48,23 @@ repos:
|
|||||||
- id: yesqa
|
- id: yesqa
|
||||||
exclude: "(migrations)"
|
exclude: "(migrations)"
|
||||||
- repo: https://github.com/asottile/add-trailing-comma
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
rev: "v2.3.0"
|
rev: "v2.4.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: add-trailing-comma
|
- id: add-trailing-comma
|
||||||
exclude: "(migrations)"
|
exclude: "(migrations)"
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 5.0.4
|
rev: 6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
files: ^src/
|
files: ^src/
|
||||||
args:
|
args:
|
||||||
- "--config=./src/setup.cfg"
|
- "--config=./src/setup.cfg"
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.10.0
|
rev: 22.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.2.2
|
rev: v3.3.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
exclude: "(migrations)"
|
exclude: "(migrations)"
|
||||||
@@ -83,6 +83,6 @@ repos:
|
|||||||
args:
|
args:
|
||||||
- "--tab"
|
- "--tab"
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||||
rev: "v0.8.0.4"
|
rev: "v0.9.0.2"
|
||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
|
22
Dockerfile
22
Dockerfile
@@ -45,7 +45,7 @@ COPY Pipfile* ./
|
|||||||
|
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& echo "Installing pipenv" \
|
&& echo "Installing pipenv" \
|
||||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv \
|
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2022.11.30 \
|
||||||
&& echo "Generating requirement.txt" \
|
&& echo "Generating requirement.txt" \
|
||||||
&& pipenv requirements > requirements.txt
|
&& pipenv requirements > requirements.txt
|
||||||
|
|
||||||
@@ -58,6 +58,12 @@ LABEL org.opencontainers.image.url="https://github.com/paperless-ngx/paperless-n
|
|||||||
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
LABEL org.opencontainers.image.licenses="GPL-3.0-only"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
# Buildx provided
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG TARGETVARIANT
|
||||||
|
|
||||||
|
# Workflow provided
|
||||||
|
ARG QPDF_VERSION
|
||||||
|
|
||||||
#
|
#
|
||||||
# Begin installation and configuration
|
# Begin installation and configuration
|
||||||
@@ -194,14 +200,10 @@ RUN --mount=type=bind,from=qpdf-builder,target=/qpdf \
|
|||||||
--mount=type=bind,from=pikepdf-builder,target=/pikepdf \
|
--mount=type=bind,from=pikepdf-builder,target=/pikepdf \
|
||||||
set -eux \
|
set -eux \
|
||||||
&& echo "Installing qpdf" \
|
&& echo "Installing qpdf" \
|
||||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/libqpdf29_*.deb \
|
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/libqpdf29_*.deb \
|
||||||
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/qpdf_*.deb \
|
&& apt-get install --yes --no-install-recommends /qpdf/usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/qpdf_*.deb \
|
||||||
&& echo "Installing pikepdf and dependencies" \
|
&& echo "Installing pikepdf and dependencies" \
|
||||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pyparsing*.whl \
|
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/*.whl \
|
||||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/packaging*.whl \
|
|
||||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/lxml*.whl \
|
|
||||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/Pillow*.whl \
|
|
||||||
&& python3 -m pip install --no-cache-dir /pikepdf/usr/src/wheels/pikepdf*.whl \
|
|
||||||
&& python3 -m pip list \
|
&& python3 -m pip list \
|
||||||
&& echo "Installing psycopg2" \
|
&& echo "Installing psycopg2" \
|
||||||
&& python3 -m pip install --no-cache-dir /psycopg2/usr/src/wheels/psycopg2*.whl \
|
&& python3 -m pip install --no-cache-dir /psycopg2/usr/src/wheels/psycopg2*.whl \
|
||||||
@@ -228,6 +230,10 @@ RUN set -eux \
|
|||||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||||
&& echo "Installing Python requirements" \
|
&& echo "Installing Python requirements" \
|
||||||
&& python3 -m pip install --default-timeout=1000 --no-cache-dir --requirement requirements.txt \
|
&& python3 -m pip install --default-timeout=1000 --no-cache-dir --requirement requirements.txt \
|
||||||
|
&& echo "Installing NLTK data" \
|
||||||
|
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/local/share/nltk_data" snowball_data \
|
||||||
|
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/local/share/nltk_data" stopwords \
|
||||||
|
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/local/share/nltk_data" punkt \
|
||||||
&& echo "Cleaning up image" \
|
&& echo "Cleaning up image" \
|
||||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||||
&& apt-get -y autoremove --purge \
|
&& apt-get -y autoremove --purge \
|
||||||
|
21
Pipfile
21
Pipfile
@@ -30,8 +30,6 @@ psycopg2 = "*"
|
|||||||
rapidfuzz = "*"
|
rapidfuzz = "*"
|
||||||
redis = {extras = ["hiredis"], version = "*"}
|
redis = {extras = ["hiredis"], version = "*"}
|
||||||
scikit-learn = "~=1.1"
|
scikit-learn = "~=1.1"
|
||||||
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
|
|
||||||
scipy = "==1.8.1"
|
|
||||||
numpy = "*"
|
numpy = "*"
|
||||||
whitenoise = "~=6.2"
|
whitenoise = "~=6.2"
|
||||||
watchdog = "~=2.1"
|
watchdog = "~=2.1"
|
||||||
@@ -43,9 +41,6 @@ tika = "*"
|
|||||||
# TODO: This will sadly also install daphne+dependencies,
|
# TODO: This will sadly also install daphne+dependencies,
|
||||||
# which an ASGI server we don't need. Adds about 15MB image size.
|
# which an ASGI server we don't need. Adds about 15MB image size.
|
||||||
channels = "~=3.0"
|
channels = "~=3.0"
|
||||||
# Locked version until https://github.com/django/channels_redis/issues/332
|
|
||||||
# is resolved
|
|
||||||
channels-redis = "==3.4.1"
|
|
||||||
uvicorn = {extras = ["standard"], version = "*"}
|
uvicorn = {extras = ["standard"], version = "*"}
|
||||||
concurrent-log-handler = "*"
|
concurrent-log-handler = "*"
|
||||||
"pdfminer.six" = "*"
|
"pdfminer.six" = "*"
|
||||||
@@ -60,6 +55,21 @@ setproctitle = "*"
|
|||||||
nltk = "*"
|
nltk = "*"
|
||||||
pdf2image = "*"
|
pdf2image = "*"
|
||||||
flower = "*"
|
flower = "*"
|
||||||
|
bleach = "*"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Packages locked due to issues (try to check if these are fixed in a release every so often)
|
||||||
|
#
|
||||||
|
|
||||||
|
# Pin this until piwheels is building 1.9 (see https://www.piwheels.org/project/scipy/)
|
||||||
|
scipy = "==1.8.1"
|
||||||
|
|
||||||
|
# Newer versions aren't builting yet (see https://www.piwheels.org/project/cryptography/)
|
||||||
|
cryptography = "==38.0.1"
|
||||||
|
|
||||||
|
# Locked version until https://github.com/django/channels_redis/issues/332
|
||||||
|
# is resolved
|
||||||
|
channels-redis = "==3.4.1"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
@@ -76,4 +86,5 @@ black = "*"
|
|||||||
pre-commit = "*"
|
pre-commit = "*"
|
||||||
sphinx-autobuild = "*"
|
sphinx-autobuild = "*"
|
||||||
myst-parser = "*"
|
myst-parser = "*"
|
||||||
|
imagehash = "*"
|
||||||
mkdocs-material = "*"
|
mkdocs-material = "*"
|
||||||
|
1508
Pipfile.lock
generated
1508
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,9 +10,9 @@
|
|||||||
# Example Usage:
|
# Example Usage:
|
||||||
# ./build-docker-image.sh Dockerfile -t paperless-ngx:my-awesome-feature
|
# ./build-docker-image.sh Dockerfile -t paperless-ngx:my-awesome-feature
|
||||||
|
|
||||||
set -eux
|
set -eu
|
||||||
|
|
||||||
if ! command -v jq; then
|
if ! command -v jq &> /dev/null ; then
|
||||||
echo "jq required"
|
echo "jq required"
|
||||||
exit 1
|
exit 1
|
||||||
elif [ ! -f "$1" ]; then
|
elif [ ! -f "$1" ]; then
|
||||||
@@ -20,28 +20,62 @@ elif [ ! -f "$1" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Parse what we can from Pipfile.lock
|
|
||||||
pikepdf_version=$(jq ".default.pikepdf.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
|
||||||
psycopg2_version=$(jq ".default.psycopg2.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
|
||||||
pillow_version=$(jq ".default.pillow.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
|
||||||
lxml_version=$(jq ".default.lxml.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
|
||||||
# Read this from the other config file
|
|
||||||
qpdf_version=$(jq ".qpdf.version" .build-config.json | sed 's/"//g')
|
|
||||||
jbig2enc_version=$(jq ".jbig2enc.version" .build-config.json | sed 's/"//g')
|
|
||||||
# Get the branch name (used for caching)
|
# Get the branch name (used for caching)
|
||||||
branch_name=$(git rev-parse --abbrev-ref HEAD)
|
branch_name=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
# https://docs.docker.com/develop/develop-images/build_enhancements/
|
# Parse eithe Pipfile.lock or the .build-config.json
|
||||||
# Required to use cache-from
|
jbig2enc_version=$(jq ".jbig2enc.version" .build-config.json | sed 's/"//g')
|
||||||
export DOCKER_BUILDKIT=1
|
qpdf_version=$(jq ".qpdf.version" .build-config.json | sed 's/"//g')
|
||||||
|
psycopg2_version=$(jq ".default.psycopg2.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||||
|
pikepdf_version=$(jq ".default.pikepdf.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||||
|
pillow_version=$(jq ".default.pillow.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||||
|
lxml_version=$(jq ".default.lxml.version" Pipfile.lock | sed 's/=//g' | sed 's/"//g')
|
||||||
|
|
||||||
docker build --file "$1" \
|
base_filename="$(basename -- "${1}")"
|
||||||
|
build_args_str=""
|
||||||
|
cache_from_str=""
|
||||||
|
|
||||||
|
case "${base_filename}" in
|
||||||
|
|
||||||
|
*.jbig2enc)
|
||||||
|
build_args_str="--build-arg JBIG2ENC_VERSION=${jbig2enc_version}"
|
||||||
|
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/jbig2enc:${jbig2enc_version}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*.psycopg2)
|
||||||
|
build_args_str="--build-arg PSYCOPG2_VERSION=${psycopg2_version}"
|
||||||
|
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/psycopg2:${psycopg2_version}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*.qpdf)
|
||||||
|
build_args_str="--build-arg QPDF_VERSION=${qpdf_version}"
|
||||||
|
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/qpdf:${qpdf_version}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*.pikepdf)
|
||||||
|
build_args_str="--build-arg QPDF_VERSION=${qpdf_version} --build-arg PIKEPDF_VERSION=${pikepdf_version} --build-arg PILLOW_VERSION=${pillow_version} --build-arg LXML_VERSION=${lxml_version}"
|
||||||
|
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/pikepdf:${pikepdf_version}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
Dockerfile)
|
||||||
|
build_args_str="--build-arg QPDF_VERSION=${qpdf_version} --build-arg PIKEPDF_VERSION=${pikepdf_version} --build-arg PSYCOPG2_VERSION=${psycopg2_version} --build-arg JBIG2ENC_VERSION=${jbig2enc_version}"
|
||||||
|
cache_from_str="--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:${branch_name} --cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Unable to match ${base_filename}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
read -r -a build_args_arr <<< "${build_args_str}"
|
||||||
|
read -r -a cache_from_arr <<< "${cache_from_str}"
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
docker buildx build --file "${1}" \
|
||||||
--progress=plain \
|
--progress=plain \
|
||||||
--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:"${branch_name}" \
|
--output=type=docker \
|
||||||
--cache-from ghcr.io/paperless-ngx/paperless-ngx/builder/cache/app:dev \
|
"${cache_from_arr[@]}" \
|
||||||
--build-arg JBIG2ENC_VERSION="${jbig2enc_version}" \
|
"${build_args_arr[@]}" \
|
||||||
--build-arg QPDF_VERSION="${qpdf_version}" \
|
"${@:2}" .
|
||||||
--build-arg PIKEPDF_VERSION="${pikepdf_version}" \
|
|
||||||
--build-arg PILLOW_VERSION="${pillow_version}" \
|
|
||||||
--build-arg LXML_VERSION="${lxml_version}" \
|
|
||||||
--build-arg PSYCOPG2_VERSION="${psycopg2_version}" "${@:2}" .
|
|
||||||
|
@@ -3,7 +3,7 @@ apiVersion: v2
|
|||||||
appVersion: "1.9.2"
|
appVersion: "1.9.2"
|
||||||
description: Paperless-ngx - Index and archive all of your scanned paper documents
|
description: Paperless-ngx - Index and archive all of your scanned paper documents
|
||||||
name: paperless
|
name: paperless
|
||||||
version: 10.0.0
|
version: 10.0.1
|
||||||
kubeVersion: ">=1.16.0-0"
|
kubeVersion: ">=1.16.0-0"
|
||||||
keywords:
|
keywords:
|
||||||
- paperless
|
- paperless
|
||||||
|
@@ -16,7 +16,13 @@ FROM python:3.9-slim-bullseye as main
|
|||||||
|
|
||||||
LABEL org.opencontainers.image.description="A intermediate image with pikepdf wheel built"
|
LABEL org.opencontainers.image.description="A intermediate image with pikepdf wheel built"
|
||||||
|
|
||||||
|
# Buildx provided
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG TARGETVARIANT
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
# Workflow provided
|
||||||
|
ARG QPDF_VERSION
|
||||||
ARG PIKEPDF_VERSION
|
ARG PIKEPDF_VERSION
|
||||||
# These are not used, but will still bust the cache if one changes
|
# These are not used, but will still bust the cache if one changes
|
||||||
# Otherwise, the main image will try to build thing (and fail)
|
# Otherwise, the main image will try to build thing (and fail)
|
||||||
@@ -54,7 +60,7 @@ ARG BUILD_PACKAGES="\
|
|||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
COPY --from=qpdf-builder /usr/src/qpdf/*.deb ./
|
COPY --from=qpdf-builder /usr/src/qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/*.deb ./
|
||||||
|
|
||||||
# As this is an base image for a multi-stage final image
|
# As this is an base image for a multi-stage final image
|
||||||
# the added size of the install is basically irrelevant
|
# the added size of the install is basically irrelevant
|
||||||
@@ -77,6 +83,8 @@ RUN set -eux \
|
|||||||
&& python3 -m pip wheel \
|
&& python3 -m pip wheel \
|
||||||
# Build the package at the required version
|
# Build the package at the required version
|
||||||
pikepdf==${PIKEPDF_VERSION} \
|
pikepdf==${PIKEPDF_VERSION} \
|
||||||
|
# Look to piwheels for additional pre-built wheels
|
||||||
|
--extra-index-url https://www.piwheels.org/simple \
|
||||||
# Output the *.whl into this directory
|
# Output the *.whl into this directory
|
||||||
--wheel-dir wheels \
|
--wheel-dir wheels \
|
||||||
# Do not use a binary packge for the package being built
|
# Do not use a binary packge for the package being built
|
||||||
@@ -86,6 +94,8 @@ RUN set -eux \
|
|||||||
# Don't cache build files
|
# Don't cache build files
|
||||||
--no-cache-dir \
|
--no-cache-dir \
|
||||||
&& ls -ahl wheels \
|
&& ls -ahl wheels \
|
||||||
|
&& echo "Gathering package data" \
|
||||||
|
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./wheels/pkg-list.txt \
|
||||||
&& echo "Cleaning up image" \
|
&& echo "Cleaning up image" \
|
||||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||||
&& apt-get -y autoremove --purge \
|
&& apt-get -y autoremove --purge \
|
||||||
|
@@ -42,6 +42,8 @@ RUN set -eux \
|
|||||||
# Don't cache build files
|
# Don't cache build files
|
||||||
--no-cache-dir \
|
--no-cache-dir \
|
||||||
&& ls -ahl wheels/ \
|
&& ls -ahl wheels/ \
|
||||||
|
&& echo "Gathering package data" \
|
||||||
|
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ./wheels/pkg-list.txt \
|
||||||
&& echo "Cleaning up image" \
|
&& echo "Cleaning up image" \
|
||||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
&& apt-get -y purge ${BUILD_PACKAGES} \
|
||||||
&& apt-get -y autoremove --purge \
|
&& apt-get -y autoremove --purge \
|
||||||
|
@@ -1,48 +1,156 @@
|
|||||||
# This Dockerfile compiles the jbig2enc library
|
#
|
||||||
# Inputs:
|
# Stage: pre-build
|
||||||
# - QPDF_VERSION - the version of qpdf to build a .deb.
|
# Purpose:
|
||||||
# Must be present as a deb-src in bookworm
|
# - Installs common packages
|
||||||
|
# - Sets common environment variables related to dpkg
|
||||||
|
# - Aquires the qpdf source from bookwork
|
||||||
|
# Useful Links:
|
||||||
|
# - https://qpdf.readthedocs.io/en/stable/installation.html#system-requirements
|
||||||
|
# - https://wiki.debian.org/Multiarch/HOWTO
|
||||||
|
# - https://wiki.debian.org/CrossCompiling
|
||||||
|
#
|
||||||
|
|
||||||
FROM debian:bullseye-slim as main
|
FROM debian:bullseye-slim as pre-build
|
||||||
|
|
||||||
LABEL org.opencontainers.image.description="A intermediate image with qpdf built"
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
# This must match to pikepdf's minimum at least
|
|
||||||
ARG QPDF_VERSION
|
ARG QPDF_VERSION
|
||||||
|
|
||||||
ARG BUILD_PACKAGES="\
|
ARG COMMON_BUILD_PACKAGES="\
|
||||||
build-essential \
|
cmake \
|
||||||
debhelper \
|
debhelper\
|
||||||
debian-keyring \
|
debian-keyring \
|
||||||
devscripts \
|
devscripts \
|
||||||
|
dpkg-dev \
|
||||||
equivs \
|
equivs \
|
||||||
libtool \
|
|
||||||
# https://qpdf.readthedocs.io/en/stable/installation.html#system-requirements
|
|
||||||
libjpeg62-turbo-dev \
|
|
||||||
libgnutls28-dev \
|
|
||||||
packaging-dev \
|
packaging-dev \
|
||||||
cmake \
|
libtool"
|
||||||
zlib1g-dev"
|
|
||||||
|
ENV DEB_BUILD_OPTIONS="terse nocheck nodoc parallel=2"
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& echo "Installing build tools" \
|
&& echo "Installing common packages" \
|
||||||
&& apt-get update --quiet \
|
&& apt-get update --quiet \
|
||||||
&& apt-get install --yes --quiet --no-install-recommends $BUILD_PACKAGES \
|
&& apt-get install --yes --quiet --no-install-recommends ${COMMON_BUILD_PACKAGES} \
|
||||||
&& echo "Getting qpdf src" \
|
&& echo "Getting qpdf source" \
|
||||||
&& echo "deb-src http://deb.debian.org/debian/ bookworm main" > /etc/apt/sources.list.d/bookworm-src.list \
|
&& echo "deb-src http://deb.debian.org/debian/ bookworm main" > /etc/apt/sources.list.d/bookworm-src.list \
|
||||||
&& apt-get update \
|
&& apt-get update --quiet \
|
||||||
&& mkdir qpdf \
|
&& apt-get source --yes --quiet qpdf=${QPDF_VERSION}-1/bookworm
|
||||||
&& cd qpdf \
|
|
||||||
&& apt-get source --yes --quiet qpdf=${QPDF_VERSION}-1/bookworm \
|
#
|
||||||
&& echo "Building qpdf" \
|
# Stage: amd64-builder
|
||||||
&& cd qpdf-$QPDF_VERSION \
|
# Purpose: Builds qpdf for x86_64 (native build)
|
||||||
&& export DEB_BUILD_OPTIONS="terse nocheck nodoc parallel=2" \
|
#
|
||||||
|
FROM pre-build as amd64-builder
|
||||||
|
|
||||||
|
ARG AMD64_BUILD_PACKAGES="\
|
||||||
|
build-essential \
|
||||||
|
libjpeg62-turbo-dev:amd64 \
|
||||||
|
libgnutls28-dev:amd64 \
|
||||||
|
zlib1g-dev:amd64"
|
||||||
|
|
||||||
|
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||||
|
|
||||||
|
RUN set -eux \
|
||||||
|
&& echo "Beginning amd64" \
|
||||||
|
&& echo "Install amd64 packages" \
|
||||||
|
&& apt-get update --quiet \
|
||||||
|
&& apt-get install --yes --quiet --no-install-recommends ${AMD64_BUILD_PACKAGES} \
|
||||||
|
&& echo "Building amd64" \
|
||||||
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean \
|
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean \
|
||||||
&& ls -ahl ../*.deb \
|
&& echo "Removing debug files" \
|
||||||
&& echo "Cleaning up image" \
|
&& rm -f ../libqpdf29-dbgsym* \
|
||||||
&& apt-get -y purge ${BUILD_PACKAGES} \
|
&& rm -f ../qpdf-dbgsym* \
|
||||||
&& apt-get -y autoremove --purge \
|
&& echo "Gathering package data" \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||||
|
#
|
||||||
|
# Stage: armhf-builder
|
||||||
|
# Purpose:
|
||||||
|
# - Sets armhf specific environment
|
||||||
|
# - Builds qpdf for armhf (cross compile)
|
||||||
|
#
|
||||||
|
FROM pre-build as armhf-builder
|
||||||
|
|
||||||
|
ARG ARMHF_PACKAGES="\
|
||||||
|
crossbuild-essential-armhf \
|
||||||
|
libjpeg62-turbo-dev:armhf \
|
||||||
|
libgnutls28-dev:armhf \
|
||||||
|
zlib1g-dev:armhf"
|
||||||
|
|
||||||
|
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||||
|
|
||||||
|
ENV CXX="/usr/bin/arm-linux-gnueabihf-g++" \
|
||||||
|
CC="/usr/bin/arm-linux-gnueabihf-gcc"
|
||||||
|
|
||||||
|
RUN set -eux \
|
||||||
|
&& echo "Beginning armhf" \
|
||||||
|
&& echo "Install armhf packages" \
|
||||||
|
&& dpkg --add-architecture armhf \
|
||||||
|
&& apt-get update --quiet \
|
||||||
|
&& apt-get install --yes --quiet --no-install-recommends ${ARMHF_PACKAGES} \
|
||||||
|
&& echo "Building armhf" \
|
||||||
|
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean --host-arch armhf \
|
||||||
|
&& echo "Removing debug files" \
|
||||||
|
&& rm -f ../libqpdf29-dbgsym* \
|
||||||
|
&& rm -f ../qpdf-dbgsym* \
|
||||||
|
&& echo "Gathering package data" \
|
||||||
|
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||||
|
|
||||||
|
#
|
||||||
|
# Stage: aarch64-builder
|
||||||
|
# Purpose:
|
||||||
|
# - Sets aarch64 specific environment
|
||||||
|
# - Builds qpdf for aarch64 (cross compile)
|
||||||
|
#
|
||||||
|
FROM pre-build as aarch64-builder
|
||||||
|
|
||||||
|
ARG ARM64_PACKAGES="\
|
||||||
|
crossbuild-essential-arm64 \
|
||||||
|
libjpeg62-turbo-dev:arm64 \
|
||||||
|
libgnutls28-dev:arm64 \
|
||||||
|
zlib1g-dev:arm64"
|
||||||
|
|
||||||
|
ENV CXX="/usr/bin/aarch64-linux-gnu-g++" \
|
||||||
|
CC="/usr/bin/aarch64-linux-gnu-gcc"
|
||||||
|
|
||||||
|
WORKDIR /usr/src/qpdf-${QPDF_VERSION}
|
||||||
|
|
||||||
|
RUN set -eux \
|
||||||
|
&& echo "Beginning arm64" \
|
||||||
|
&& echo "Install arm64 packages" \
|
||||||
|
&& dpkg --add-architecture arm64 \
|
||||||
|
&& apt-get update --quiet \
|
||||||
|
&& apt-get install --yes --quiet --no-install-recommends ${ARM64_PACKAGES} \
|
||||||
|
&& echo "Building arm64" \
|
||||||
|
&& dpkg-buildpackage --build=binary --unsigned-source --unsigned-changes --post-clean --host-arch arm64 \
|
||||||
|
&& echo "Removing debug files" \
|
||||||
|
&& rm -f ../libqpdf29-dbgsym* \
|
||||||
|
&& rm -f ../qpdf-dbgsym* \
|
||||||
|
&& echo "Gathering package data" \
|
||||||
|
&& dpkg-query -f '${Package;-40}${Version}\n' -W > ../pkg-list.txt
|
||||||
|
|
||||||
|
#
|
||||||
|
# Stage: package
|
||||||
|
# Purpose: Holds the compiled .deb files in arch/variant specific folders
|
||||||
|
#
|
||||||
|
FROM alpine:3.17 as package
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.description="A image with qpdf installers stored in architecture & version specific folders"
|
||||||
|
|
||||||
|
ARG QPDF_VERSION
|
||||||
|
|
||||||
|
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/amd64
|
||||||
|
|
||||||
|
COPY --from=amd64-builder /usr/src/*.deb ./
|
||||||
|
COPY --from=amd64-builder /usr/src/pkg-list.txt ./
|
||||||
|
|
||||||
|
# Note this is ${TARGETARCH}${TARGETVARIANT} for armv7
|
||||||
|
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/armv7
|
||||||
|
|
||||||
|
COPY --from=armhf-builder /usr/src/*.deb ./
|
||||||
|
COPY --from=armhf-builder /usr/src/pkg-list.txt ./
|
||||||
|
|
||||||
|
WORKDIR /usr/src/qpdf/${QPDF_VERSION}/arm64
|
||||||
|
|
||||||
|
COPY --from=aarch64-builder /usr/src/*.deb ./
|
||||||
|
COPY --from=aarch64-builder /usr/src/pkg-list.txt ./
|
||||||
|
@@ -11,9 +11,12 @@ services:
|
|||||||
container_name: gotenberg
|
container_name: gotenberg
|
||||||
network_mode: host
|
network_mode: host
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
command:
|
command:
|
||||||
- "gotenberg"
|
- "gotenberg"
|
||||||
- "--chromium-disable-routes=true"
|
- "--chromium-disable-javascript=true"
|
||||||
|
- "--chromium-allow-list=file:///tmp/.*"
|
||||||
tika:
|
tika:
|
||||||
image: ghcr.io/paperless-ngx/tika:latest
|
image: ghcr.io/paperless-ngx/tika:latest
|
||||||
hostname: tika
|
hostname: tika
|
||||||
|
@@ -49,8 +49,6 @@ services:
|
|||||||
MARIADB_USER: paperless
|
MARIADB_USER: paperless
|
||||||
MARIADB_PASSWORD: paperless
|
MARIADB_PASSWORD: paperless
|
||||||
MARIADB_ROOT_PASSWORD: paperless
|
MARIADB_ROOT_PASSWORD: paperless
|
||||||
ports:
|
|
||||||
- "3306:3306"
|
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
@@ -87,9 +85,12 @@ services:
|
|||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:7.6
|
image: docker.io/gotenberg/gotenberg:7.6
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
command:
|
command:
|
||||||
- "gotenberg"
|
- "gotenberg"
|
||||||
- "--chromium-disable-routes=true"
|
- "--chromium-disable-javascript=true"
|
||||||
|
- "--chromium-allow-list=file:///tmp/.*"
|
||||||
|
|
||||||
tika:
|
tika:
|
||||||
image: ghcr.io/paperless-ngx/tika:latest
|
image: ghcr.io/paperless-ngx/tika:latest
|
||||||
|
@@ -45,8 +45,6 @@ services:
|
|||||||
MARIADB_USER: paperless
|
MARIADB_USER: paperless
|
||||||
MARIADB_PASSWORD: paperless
|
MARIADB_PASSWORD: paperless
|
||||||
MARIADB_ROOT_PASSWORD: paperless
|
MARIADB_ROOT_PASSWORD: paperless
|
||||||
ports:
|
|
||||||
- "3306:3306"
|
|
||||||
|
|
||||||
webserver:
|
webserver:
|
||||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
|
@@ -79,9 +79,13 @@ services:
|
|||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:7.6
|
image: docker.io/gotenberg/gotenberg:7.6
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
command:
|
command:
|
||||||
- "gotenberg"
|
- "gotenberg"
|
||||||
- "--chromium-disable-routes=true"
|
- "--chromium-disable-javascript=true"
|
||||||
|
- "--chromium-allow-list=file:///tmp/.*"
|
||||||
|
|
||||||
tika:
|
tika:
|
||||||
image: ghcr.io/paperless-ngx/tika:latest
|
image: ghcr.io/paperless-ngx/tika:latest
|
||||||
|
@@ -67,9 +67,13 @@ services:
|
|||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:7.6
|
image: docker.io/gotenberg/gotenberg:7.6
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
command:
|
command:
|
||||||
- "gotenberg"
|
- "gotenberg"
|
||||||
- "--chromium-disable-routes=true"
|
- "--chromium-disable-javascript=true"
|
||||||
|
- "--chromium-allow-list=file:///tmp/.*"
|
||||||
|
|
||||||
tika:
|
tika:
|
||||||
image: ghcr.io/paperless-ngx/tika:latest
|
image: ghcr.io/paperless-ngx/tika:latest
|
||||||
|
@@ -53,30 +53,6 @@ map_folders() {
|
|||||||
export CONSUME_DIR="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
|
export CONSUME_DIR="${PAPERLESS_CONSUMPTION_DIR:-/usr/src/paperless/consume}"
|
||||||
}
|
}
|
||||||
|
|
||||||
nltk_data () {
|
|
||||||
# Store the NLTK data outside the Docker container
|
|
||||||
local -r nltk_data_dir="${DATA_DIR}/nltk"
|
|
||||||
local -r truthy_things=("yes y 1 t true")
|
|
||||||
|
|
||||||
# If not set, or it looks truthy
|
|
||||||
if [[ -z "${PAPERLESS_ENABLE_NLTK}" ]] || [[ "${truthy_things[*]}" =~ ${PAPERLESS_ENABLE_NLTK,} ]]; then
|
|
||||||
|
|
||||||
# Download or update the snowball stemmer data
|
|
||||||
python3 -W ignore::RuntimeWarning -m nltk.downloader -d "${nltk_data_dir}" snowball_data
|
|
||||||
|
|
||||||
# Download or update the stopwords corpus
|
|
||||||
python3 -W ignore::RuntimeWarning -m nltk.downloader -d "${nltk_data_dir}" stopwords
|
|
||||||
|
|
||||||
# Download or update the punkt tokenizer data
|
|
||||||
python3 -W ignore::RuntimeWarning -m nltk.downloader -d "${nltk_data_dir}" punkt
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "Skipping NLTK data download"
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
custom_container_init() {
|
custom_container_init() {
|
||||||
# Mostly borrowed from the LinuxServer.io base image
|
# Mostly borrowed from the LinuxServer.io base image
|
||||||
# https://github.com/linuxserver/docker-baseimage-ubuntu/tree/bionic/root/etc/cont-init.d
|
# https://github.com/linuxserver/docker-baseimage-ubuntu/tree/bionic/root/etc/cont-init.d
|
||||||
@@ -157,8 +133,6 @@ initialize() {
|
|||||||
echo "Creating directory ${tmp_dir}"
|
echo "Creating directory ${tmp_dir}"
|
||||||
mkdir -p "${tmp_dir}"
|
mkdir -p "${tmp_dir}"
|
||||||
|
|
||||||
nltk_data
|
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
echo "Adjusting permissions of paperless files. This may take a while."
|
echo "Adjusting permissions of paperless files. This may take a while."
|
||||||
chown -R paperless:paperless ${tmp_dir}
|
chown -R paperless:paperless ${tmp_dir}
|
||||||
@@ -191,10 +165,6 @@ install_languages() {
|
|||||||
|
|
||||||
for lang in "${langs[@]}"; do
|
for lang in "${langs[@]}"; do
|
||||||
pkg="tesseract-ocr-$lang"
|
pkg="tesseract-ocr-$lang"
|
||||||
# English is installed by default
|
|
||||||
#if [[ "$lang" == "eng" ]]; then
|
|
||||||
# continue
|
|
||||||
#fi
|
|
||||||
|
|
||||||
if dpkg -s "$pkg" &>/dev/null; then
|
if dpkg -s "$pkg" &>/dev/null; then
|
||||||
echo "Package $pkg already installed!"
|
echo "Package $pkg already installed!"
|
||||||
|
@@ -20,7 +20,6 @@ wait_for_postgres() {
|
|||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "Attempt $attempt_num failed! Trying again in 5 seconds..."
|
echo "Attempt $attempt_num failed! Trying again in 5 seconds..."
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
attempt_num=$(("$attempt_num" + 1))
|
attempt_num=$(("$attempt_num" + 1))
|
||||||
@@ -37,6 +36,8 @@ wait_for_mariadb() {
|
|||||||
local attempt_num=1
|
local attempt_num=1
|
||||||
local -r max_attempts=5
|
local -r max_attempts=5
|
||||||
|
|
||||||
|
# Disable warning, host and port can't have spaces
|
||||||
|
# shellcheck disable=SC2086
|
||||||
while ! true > /dev/tcp/$host/$port; do
|
while ! true > /dev/tcp/$host/$port; do
|
||||||
|
|
||||||
if [ $attempt_num -eq $max_attempts ]; then
|
if [ $attempt_num -eq $max_attempts ]; then
|
||||||
@@ -67,10 +68,16 @@ migrations() {
|
|||||||
# of the current container starts.
|
# of the current container starts.
|
||||||
flock 200
|
flock 200
|
||||||
echo "Apply database migrations..."
|
echo "Apply database migrations..."
|
||||||
python3 manage.py migrate
|
python3 manage.py migrate --skip-checks --no-input
|
||||||
) 200>"${DATA_DIR}/migration_lock"
|
) 200>"${DATA_DIR}/migration_lock"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
django_checks() {
|
||||||
|
# Explicitly run the Django system checks
|
||||||
|
echo "Running Django checks"
|
||||||
|
python3 manage.py check
|
||||||
|
}
|
||||||
|
|
||||||
search_index() {
|
search_index() {
|
||||||
|
|
||||||
local -r index_version=1
|
local -r index_version=1
|
||||||
@@ -100,6 +107,8 @@ do_work() {
|
|||||||
|
|
||||||
migrations
|
migrations
|
||||||
|
|
||||||
|
django_checks
|
||||||
|
|
||||||
search_index
|
search_index
|
||||||
|
|
||||||
superuser
|
superuser
|
||||||
|
@@ -9,7 +9,7 @@ Before making backups, make sure that paperless is not running.
|
|||||||
|
|
||||||
Options available to any installation of paperless:
|
Options available to any installation of paperless:
|
||||||
|
|
||||||
- Use the [document exporter](administration#exporter). The document exporter exports all your documents,
|
- Use the [document exporter](#exporter). The document exporter exports all your documents,
|
||||||
thumbnails and metadata to a specific folder. You may import your
|
thumbnails and metadata to a specific folder. You may import your
|
||||||
documents into a fresh instance of paperless again or store your
|
documents into a fresh instance of paperless again or store your
|
||||||
documents in another DMS with this export.
|
documents in another DMS with this export.
|
||||||
@@ -52,7 +52,7 @@ Options available to bare-metal and non-docker installations:
|
|||||||
|
|
||||||
## Updating Paperless {#updating}
|
## Updating Paperless {#updating}
|
||||||
|
|
||||||
### Docker Route
|
### Docker Route {#docker-updating}
|
||||||
|
|
||||||
If a new release of paperless-ngx is available, upgrading depends on how
|
If a new release of paperless-ngx is available, upgrading depends on how
|
||||||
you installed paperless-ngx in the first place. The releases are
|
you installed paperless-ngx in the first place. The releases are
|
||||||
@@ -68,9 +68,9 @@ $ docker-compose down
|
|||||||
|
|
||||||
After that, [make a backup](#backup).
|
After that, [make a backup](#backup).
|
||||||
|
|
||||||
A. If you pull the image from the docker hub, all you need to do is:
|
1. If you pull the image from the docker hub, all you need to do is:
|
||||||
|
|
||||||
``` shell-session
|
```shell-session
|
||||||
$ docker-compose pull
|
$ docker-compose pull
|
||||||
$ docker-compose up
|
$ docker-compose up
|
||||||
```
|
```
|
||||||
@@ -78,9 +78,9 @@ A. If you pull the image from the docker hub, all you need to do is:
|
|||||||
The docker-compose files refer to the `latest` version, which is
|
The docker-compose files refer to the `latest` version, which is
|
||||||
always the latest stable release.
|
always the latest stable release.
|
||||||
|
|
||||||
B. If you built the image yourself, do the following:
|
2. If you built the image yourself, do the following:
|
||||||
|
|
||||||
``` shell-session
|
```shell-session
|
||||||
$ git pull
|
$ git pull
|
||||||
$ docker-compose build
|
$ docker-compose build
|
||||||
$ docker-compose up
|
$ docker-compose up
|
||||||
@@ -131,7 +131,7 @@ the background.
|
|||||||
image: ghcr.io/paperless-ngx/paperless-ngx:1.7
|
image: ghcr.io/paperless-ngx/paperless-ngx:1.7
|
||||||
```
|
```
|
||||||
|
|
||||||
### Bare Metal Route
|
### Bare Metal Route {#bare-metal-updating}
|
||||||
|
|
||||||
After grabbing the new release and unpacking the contents, do the
|
After grabbing the new release and unpacking the contents, do the
|
||||||
following:
|
following:
|
||||||
@@ -158,7 +158,7 @@ following:
|
|||||||
This might not actually do anything. Not every new paperless version
|
This might not actually do anything. Not every new paperless version
|
||||||
comes with new database migrations.
|
comes with new database migrations.
|
||||||
|
|
||||||
## Downgrading Paperless
|
## Downgrading Paperless {#downgrade-paperless}
|
||||||
|
|
||||||
Downgrades are possible. However, some updates also contain database
|
Downgrades are possible. However, some updates also contain database
|
||||||
migrations (these change the layout of the database and may move data).
|
migrations (these change the layout of the database and may move data).
|
||||||
@@ -233,6 +233,7 @@ optional arguments:
|
|||||||
-c, --compare-checksums
|
-c, --compare-checksums
|
||||||
-f, --use-filename-format
|
-f, --use-filename-format
|
||||||
-d, --delete
|
-d, --delete
|
||||||
|
-z --zip
|
||||||
```
|
```
|
||||||
|
|
||||||
`target` is a folder to which the data gets written. This includes
|
`target` is a folder to which the data gets written. This includes
|
||||||
@@ -258,6 +259,9 @@ current export such as files from deleted documents, specify `--delete`.
|
|||||||
Be careful when pointing paperless to a directory that already contains
|
Be careful when pointing paperless to a directory that already contains
|
||||||
other files.
|
other files.
|
||||||
|
|
||||||
|
If `-z` or `--zip` is provided, the export will be a zipfile
|
||||||
|
in the target directory, named according to the current date.
|
||||||
|
|
||||||
The filenames generated by this command follow the format
|
The filenames generated by this command follow the format
|
||||||
`[date created] [correspondent] [title].[extension]`. If you want
|
`[date created] [correspondent] [title].[extension]`. If you want
|
||||||
paperless to use `PAPERLESS_FILENAME_FORMAT` for exported filenames
|
paperless to use `PAPERLESS_FILENAME_FORMAT` for exported filenames
|
||||||
@@ -266,7 +270,7 @@ instead, specify `--use-filename-format`.
|
|||||||
### Document importer {#importer}
|
### Document importer {#importer}
|
||||||
|
|
||||||
The document importer takes the export produced by the [Document
|
The document importer takes the export produced by the [Document
|
||||||
exporter](#document-exporter) and imports it into paperless.
|
exporter](#exporter) and imports it into paperless.
|
||||||
|
|
||||||
The importer works just like the exporter. You point it at a directory,
|
The importer works just like the exporter. You point it at a directory,
|
||||||
and the script does the rest of the work:
|
and the script does the rest of the work:
|
||||||
@@ -366,7 +370,7 @@ task scheduler.
|
|||||||
### Managing filenames {#renamer}
|
### Managing filenames {#renamer}
|
||||||
|
|
||||||
If you use paperless' feature to
|
If you use paperless' feature to
|
||||||
[assign custom filenames to your documents](/advanced_usage#file_name_handling), you can use this command to move all your files after
|
[assign custom filenames to your documents](/advanced_usage#file-name-handling), you can use this command to move all your files after
|
||||||
changing the naming scheme.
|
changing the naming scheme.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
@@ -430,9 +434,7 @@ rules.
|
|||||||
As of October 2022 Microsoft no longer supports IMAP authentication
|
As of October 2022 Microsoft no longer supports IMAP authentication
|
||||||
for Exchange servers, thus Exchange is no longer supported until a
|
for Exchange servers, thus Exchange is no longer supported until a
|
||||||
solution is implemented in the Python IMAP library used by Paperless.
|
solution is implemented in the Python IMAP library used by Paperless.
|
||||||
See
|
See [learn.microsoft.com](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/deprecation-of-basic-authentication-exchange-online)
|
||||||
|
|
||||||
[learn.microsoft.com](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/deprecation-of-basic-authentication-exchange-online)
|
|
||||||
|
|
||||||
### Creating archived documents {#archiver}
|
### Creating archived documents {#archiver}
|
||||||
|
|
||||||
@@ -467,13 +469,13 @@ document.
|
|||||||
documents, such as encrypted PDF documents. The archiver will skip over
|
documents, such as encrypted PDF documents. The archiver will skip over
|
||||||
these documents each time it sees them.
|
these documents each time it sees them.
|
||||||
|
|
||||||
### Managing encryption {#encyption}
|
### Managing encryption {#encryption}
|
||||||
|
|
||||||
Documents can be stored in Paperless using GnuPG encryption.
|
Documents can be stored in Paperless using GnuPG encryption.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
Encryption is deprecated since paperless-ngx 0.9 and doesn't really
|
Encryption is deprecated since [paperless-ng 0.9](/changelog#paperless-ng-090) and doesn't really
|
||||||
provide any additional security, since you have to store the passphrase
|
provide any additional security, since you have to store the passphrase
|
||||||
in a configuration file on the same system as the encrypted documents
|
in a configuration file on the same system as the encrypted documents
|
||||||
for paperless to work. Furthermore, the entire text content of the
|
for paperless to work. Furthermore, the entire text content of the
|
||||||
|
@@ -50,7 +50,7 @@ and run another document through the consumer. Once complete, you should
|
|||||||
see the newly-created document, automatically tagged with the
|
see the newly-created document, automatically tagged with the
|
||||||
appropriate data.
|
appropriate data.
|
||||||
|
|
||||||
### Automatic matching {#automatic_matching}
|
### Automatic matching {#automatic-matching}
|
||||||
|
|
||||||
Paperless-ngx comes with a new matching algorithm called _Auto_. This
|
Paperless-ngx comes with a new matching algorithm called _Auto_. This
|
||||||
matching algorithm tries to assign tags, correspondents, document types,
|
matching algorithm tries to assign tags, correspondents, document types,
|
||||||
@@ -59,8 +59,8 @@ assigned these on existing documents. It uses a neural network under the
|
|||||||
hood.
|
hood.
|
||||||
|
|
||||||
If, for example, all your bank statements of your account 123 at the
|
If, for example, all your bank statements of your account 123 at the
|
||||||
Bank of America are tagged with the tag "bofa*123" and the matching
|
Bank of America are tagged with the tag "bofa123" and the matching
|
||||||
algorithm of this tag is set to \_Auto*, this neural network will examine
|
algorithm of this tag is set to _Auto_, this neural network will examine
|
||||||
your documents and automatically learn when to assign this tag.
|
your documents and automatically learn when to assign this tag.
|
||||||
|
|
||||||
Paperless tries to hide much of the involved complexity with this
|
Paperless tries to hide much of the involved complexity with this
|
||||||
@@ -95,7 +95,7 @@ when using this feature:
|
|||||||
of these correspondents to ANY new document, if both are set to
|
of these correspondents to ANY new document, if both are set to
|
||||||
automatic matching.
|
automatic matching.
|
||||||
|
|
||||||
## Hooking into the consumption process
|
## Hooking into the consumption process {#consume-hooks}
|
||||||
|
|
||||||
Sometimes you may want to do something arbitrary whenever a document is
|
Sometimes you may want to do something arbitrary whenever a document is
|
||||||
consumed. Rather than try to predict what you may want to do, Paperless
|
consumed. Rather than try to predict what you may want to do, Paperless
|
||||||
@@ -115,7 +115,7 @@ and then put the path to that script in `paperless.conf` or
|
|||||||
asynchronously, you'll have to fork the process in your script and
|
asynchronously, you'll have to fork the process in your script and
|
||||||
exit.
|
exit.
|
||||||
|
|
||||||
### Pre-consumption script
|
### Pre-consumption script {#pre-consume-script}
|
||||||
|
|
||||||
Executed after the consumer sees a new document in the consumption
|
Executed after the consumer sees a new document in the consumption
|
||||||
folder, but before any processing of the document is performed. This
|
folder, but before any processing of the document is performed. This
|
||||||
@@ -151,7 +151,7 @@ with the newly modified file.
|
|||||||
The script's stdout and stderr will be logged line by line to the
|
The script's stdout and stderr will be logged line by line to the
|
||||||
webserver log, along with the exit code of the script.
|
webserver log, along with the exit code of the script.
|
||||||
|
|
||||||
### Post-consumption script {#post_consume_script}
|
### Post-consumption script {#post-consume-script}
|
||||||
|
|
||||||
Executed after the consumer has successfully processed a document and
|
Executed after the consumer has successfully processed a document and
|
||||||
has moved it into paperless. It receives the following environment
|
has moved it into paperless. It receives the following environment
|
||||||
@@ -181,33 +181,34 @@ The post consumption script cannot cancel the consumption process.
|
|||||||
The script's stdout and stderr will be logged line by line to the
|
The script's stdout and stderr will be logged line by line to the
|
||||||
webserver log, along with the exit code of the script.
|
webserver log, along with the exit code of the script.
|
||||||
|
|
||||||
#### Docker
|
### Docker {#docker-consume-hooks}
|
||||||
|
|
||||||
Assumed you have
|
To hook into the consumption process when using Docker, you
|
||||||
`/home/foo/paperless-ngx/scripts/post-consumption-example.sh`.
|
will need to pass the scripts into the container via a host mount
|
||||||
|
in your `docker-compose.yml`.
|
||||||
|
|
||||||
You can pass that script into the consumer container via a host mount in
|
Assuming you have
|
||||||
your `docker-compose.yml`.
|
`/home/paperless-ngx/scripts/post-consumption-example.sh` as a
|
||||||
|
script which you'd like to run.
|
||||||
|
|
||||||
```bash
|
You can pass that script into the consumer container via a host mount:
|
||||||
|
|
||||||
|
```yaml
|
||||||
...
|
...
|
||||||
consumer:
|
webserver:
|
||||||
...
|
...
|
||||||
volumes:
|
volumes:
|
||||||
...
|
...
|
||||||
- /home/paperless-ngx/scripts:/path/in/container/scripts/
|
- /home/paperless-ngx/scripts:/path/in/container/scripts/ # (1)!
|
||||||
|
environment: # (3)!
|
||||||
|
...
|
||||||
|
PAPERLESS_POST_CONSUME_SCRIPT: /path/in/container/scripts/post-consumption-example.sh # (2)!
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Example (docker-compose.yml):
|
1. The external scripts directory is mounted to a location inside the container.
|
||||||
`- /home/foo/paperless-ngx/scripts:/usr/src/paperless/scripts`
|
2. The internal location of the script is used to set the script to run
|
||||||
|
3. This can also be set in `docker-compose.env`
|
||||||
which in turn requires the variable `PAPERLESS_POST_CONSUME_SCRIPT` in
|
|
||||||
`docker-compose.env` to point to
|
|
||||||
`/path/in/container/scripts/post-consumption-example.sh`.
|
|
||||||
|
|
||||||
Example (docker-compose.env):
|
|
||||||
`PAPERLESS_POST_CONSUME_SCRIPT=/usr/src/paperless/scripts/post-consumption-example.sh`
|
|
||||||
|
|
||||||
Troubleshooting:
|
Troubleshooting:
|
||||||
|
|
||||||
@@ -218,7 +219,7 @@ Troubleshooting:
|
|||||||
- Pipe your scripts's output to a log file e.g.
|
- Pipe your scripts's output to a log file e.g.
|
||||||
`echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`
|
`echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`
|
||||||
|
|
||||||
## File name handling {#file_name_handling}
|
## File name handling {#file-name-handling}
|
||||||
|
|
||||||
By default, paperless stores your documents in the media directory and
|
By default, paperless stores your documents in the media directory and
|
||||||
renames them using the identifier which it has assigned to each
|
renames them using the identifier which it has assigned to each
|
||||||
@@ -301,7 +302,7 @@ value.
|
|||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
You can affect how empty placeholders are treated by changing the
|
You can affect how empty placeholders are treated by changing the
|
||||||
following setting to [true]{.title-ref}.
|
following setting to `true`.
|
||||||
|
|
||||||
```
|
```
|
||||||
PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=True
|
PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=True
|
||||||
@@ -316,7 +317,7 @@ value.
|
|||||||
Paperless checks the filename of a document whenever it is saved.
|
Paperless checks the filename of a document whenever it is saved.
|
||||||
Therefore, you need to update the filenames of your documents and move
|
Therefore, you need to update the filenames of your documents and move
|
||||||
them after altering this setting by invoking the
|
them after altering this setting by invoking the
|
||||||
[`document renamer <utilities-renamer>`]().
|
[`document renamer`](/administration#renamer).
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
@@ -344,7 +345,7 @@ When as single storage layout is not sufficient for your use case,
|
|||||||
storage paths come to the rescue. Storage paths allow you to configure
|
storage paths come to the rescue. Storage paths allow you to configure
|
||||||
more precisely where each document is stored in the file system.
|
more precisely where each document is stored in the file system.
|
||||||
|
|
||||||
- Each storage path is a [PAPERLESS_FILENAME_FORMAT]{.title-ref} and
|
- Each storage path is a `PAPERLESS_FILENAME_FORMAT` and
|
||||||
follows the rules described above
|
follows the rules described above
|
||||||
- Each document is assigned a storage path using the matching
|
- Each document is assigned a storage path using the matching
|
||||||
algorithms described above, but can be overwritten at any time
|
algorithms described above, but can be overwritten at any time
|
||||||
@@ -352,7 +353,7 @@ more precisely where each document is stored in the file system.
|
|||||||
For example, you could define the following two storage paths:
|
For example, you could define the following two storage paths:
|
||||||
|
|
||||||
1. Normal communications are put into a folder structure sorted by
|
1. Normal communications are put into a folder structure sorted by
|
||||||
[year/correspondent]{.title-ref}
|
`year/correspondent`
|
||||||
2. Communications with insurance companies are stored in a flat
|
2. Communications with insurance companies are stored in a flat
|
||||||
structure with longer file names, but containing the full date of
|
structure with longer file names, but containing the full date of
|
||||||
the correspondence.
|
the correspondence.
|
||||||
@@ -363,7 +364,7 @@ Insurances = Insurances/{correspondent}/{created_year}-{created_month}-{created_
|
|||||||
```
|
```
|
||||||
|
|
||||||
If you then map these storage paths to the documents, you might get the
|
If you then map these storage paths to the documents, you might get the
|
||||||
following result. For simplicity, [By Year]{.title-ref} defines the same
|
following result. For simplicity, `By Year` defines the same
|
||||||
structure as in the previous example above.
|
structure as in the previous example above.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -384,7 +385,7 @@ structure as in the previous example above.
|
|||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
Defining a storage path is optional. If no storage path is defined for a
|
Defining a storage path is optional. If no storage path is defined for a
|
||||||
document, the global [PAPERLESS_FILENAME_FORMAT]{.title-ref} is applied.
|
document, the global `PAPERLESS_FILENAME_FORMAT` is applied.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
@@ -403,33 +404,38 @@ queued and completed tasks, timing and more. Flower can also be used
|
|||||||
with Prometheus, as it exports metrics. For details on its capabilities,
|
with Prometheus, as it exports metrics. For details on its capabilities,
|
||||||
refer to the Flower documentation.
|
refer to the Flower documentation.
|
||||||
|
|
||||||
To configure Flower further, create a [flowerconfig.py]{.title-ref} and
|
To configure Flower further, create a `flowerconfig.py` and
|
||||||
place it into the [src/paperless]{.title-ref} directory. For a Docker
|
place it into the `src/paperless` directory. For a Docker
|
||||||
installation, you can use volumes to accomplish this:
|
installation, you can use volumes to accomplish this:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
# ...
|
# ...
|
||||||
webserver:
|
webserver:
|
||||||
|
ports:
|
||||||
|
- 5555:5555 # (2)!
|
||||||
# ...
|
# ...
|
||||||
volumes:
|
volumes:
|
||||||
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro
|
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro # (1)!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1. Note the `:ro` tag means the file will be mounted as read only.
|
||||||
|
2. `flower` runs by default on port 5555, but this can be configured
|
||||||
|
|
||||||
## Custom Container Initialization
|
## Custom Container Initialization
|
||||||
|
|
||||||
The Docker image includes the ability to run custom user scripts during
|
The Docker image includes the ability to run custom user scripts during
|
||||||
startup. This could be utilized for installing additional tools or
|
startup. This could be utilized for installing additional tools or
|
||||||
Python packages, for example.
|
Python packages, for example. Scripts are expected to be shell scripts.
|
||||||
|
|
||||||
To utilize this, mount a folder containing your scripts to the custom
|
To utilize this, mount a folder containing your scripts to the custom
|
||||||
initialization directory, [/custom-cont-init.d]{.title-ref} and place
|
initialization directory, `/custom-cont-init.d` and place
|
||||||
scripts you wish to run inside. For security, the folder must be owned
|
scripts you wish to run inside. For security, the folder must be owned
|
||||||
by `root` and should have permissions of `a=rx`. Additionally, scripts
|
by `root` and should have permissions of `a=rx`. Additionally, scripts
|
||||||
must only be writable by `root`.
|
must only be writable by `root`.
|
||||||
|
|
||||||
Your scripts will be run directly before the webserver completes
|
Your scripts will be run directly before the webserver completes
|
||||||
startup. Scripts will be run by the [root]{.title-ref} user.
|
startup. Scripts will be run by the `root` user.
|
||||||
If you would like to switch users, the utility `gosu` is available and
|
If you would like to switch users, the utility `gosu` is available and
|
||||||
preferred over `sudo`.
|
preferred over `sudo`.
|
||||||
|
|
||||||
@@ -445,9 +451,11 @@ services:
|
|||||||
webserver:
|
webserver:
|
||||||
# ...
|
# ...
|
||||||
volumes:
|
volumes:
|
||||||
- /path/to/my/scripts:/custom-cont-init.d:ro
|
- /path/to/my/scripts:/custom-cont-init.d:ro # (1)!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1. Note the `:ro` tag means the folder will be mounted as read only. This is for extra security against changes
|
||||||
|
|
||||||
## MySQL Caveats {#mysql-caveats}
|
## MySQL Caveats {#mysql-caveats}
|
||||||
|
|
||||||
### Case Sensitivity
|
### Case Sensitivity
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# The REST API
|
# The REST API
|
||||||
|
|
||||||
Paperless makes use of the [Django REST
|
Paperless makes use of the [Django REST
|
||||||
Framework](http://django-rest-framework.org/) standard API interface. It
|
Framework](https://django-rest-framework.org/) standard API interface. It
|
||||||
provides a browsable API for most of its endpoints, which you can
|
provides a browsable API for most of its endpoints, which you can
|
||||||
inspect at `http://<paperless-host>:<port>/api/`. This also documents
|
inspect at `http://<paperless-host>:<port>/api/`. This also documents
|
||||||
most of the available filters and ordering fields.
|
most of the available filters and ordering fields.
|
||||||
@@ -162,7 +162,7 @@ specific query parameters cause the API to return full text search
|
|||||||
results:
|
results:
|
||||||
|
|
||||||
- `/api/documents/?query=your%20search%20query`: Search for a document
|
- `/api/documents/?query=your%20search%20query`: Search for a document
|
||||||
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage#basic-usage_searching).
|
using a full text query. For details on the syntax, see [Basic Usage - Searching](/usage#basic-usage_searching).
|
||||||
- `/api/documents/?more_like=1234`: Search for documents similar to
|
- `/api/documents/?more_like=1234`: Search for documents similar to
|
||||||
the document with id 1234.
|
the document with id 1234.
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ Query parameters:
|
|||||||
|
|
||||||
Results returned by the endpoint are ordered by importance of the term
|
Results returned by the endpoint are ordered by importance of the term
|
||||||
in the document index. The first result is the term that has the highest
|
in the document index. The first result is the term that has the highest
|
||||||
Tf/Idf score in the index.
|
[Tf/Idf](https://en.wikipedia.org/wiki/Tf%E2%80%93idf) score in the index.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
["term1", "term3", "term6", "term4"]
|
["term1", "term3", "term6", "term4"]
|
||||||
|
@@ -1,5 +1,35 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 1.10.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Take ownership of k8s-at-home Helm chart [@alexander-bauer](https://github.com/alexander-bauer) ([#1947](https://github.com/paperless-ngx/paperless-ngx/pull/1947))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Bugfix: Language code checks around two part languages [@stumpylog](https://github.com/stumpylog) ([#2112](https://github.com/paperless-ngx/paperless-ngx/pull/2112))
|
||||||
|
- Bugfix: Redis socket compatibility didn't handle URLs with ports [@stumpylog](https://github.com/stumpylog) ([#2109](https://github.com/paperless-ngx/paperless-ngx/pull/2109))
|
||||||
|
- Bugfix: Incompatible URL schemes for socket based Redis [@stumpylog](https://github.com/stumpylog) ([#2092](https://github.com/paperless-ngx/paperless-ngx/pull/2092))
|
||||||
|
- Fix doc links in contributing [@tooomm](https://github.com/tooomm) ([#2102](https://github.com/paperless-ngx/paperless-ngx/pull/2102))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Docs: Some more small MkDocs updates [@tooomm](https://github.com/tooomm) ([#2106](https://github.com/paperless-ngx/paperless-ngx/pull/2106))
|
||||||
|
- Chore: Cleans up documentation links [@stumpylog](https://github.com/stumpylog) ([#2104](https://github.com/paperless-ngx/paperless-ngx/pull/2104))
|
||||||
|
- Feature: Move docs to material-mkdocs [@shamoon](https://github.com/shamoon) ([#2067](https://github.com/paperless-ngx/paperless-ngx/pull/2067))
|
||||||
|
- Chore: Add v1.10.1 changelong [@shamoon](https://github.com/shamoon) ([#2082](https://github.com/paperless-ngx/paperless-ngx/pull/2082))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Take ownership of k8s-at-home Helm chart [@alexander-bauer](https://github.com/alexander-bauer) ([#1947](https://github.com/paperless-ngx/paperless-ngx/pull/1947))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
- Bugfix: Language code checks around two part languages [@stumpylog](https://github.com/stumpylog) ([#2112](https://github.com/paperless-ngx/paperless-ngx/pull/2112))
|
||||||
|
- Bugfix: Redis socket compatibility didn't handle URLs with ports [@stumpylog](https://github.com/stumpylog) ([#2109](https://github.com/paperless-ngx/paperless-ngx/pull/2109))
|
||||||
|
- Bugfix: Incompatible URL schemes for socket based Redis [@stumpylog](https://github.com/stumpylog) ([#2092](https://github.com/paperless-ngx/paperless-ngx/pull/2092))
|
||||||
|
|
||||||
## paperless-ngx 1.10.1
|
## paperless-ngx 1.10.1
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -961,11 +991,10 @@ This is a maintenance release.
|
|||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
The changed to the full text searching require you to reindex your
|
The changed to the full text searching require you to reindex your
|
||||||
documents. _The docker image does this automatically, you don't need to
|
documents. _The docker image does this automatically, you don't need to
|
||||||
do anything._ To do this, execute the `document_index reindex`
|
do anything._ To do this, execute the `document_index reindex`
|
||||||
management command (see `administration-index`{.interpreted-text
|
management command (see [Managing the document search index](/administration#index)).
|
||||||
role="ref"}).
|
|
||||||
|
|
||||||
### paperless-ng 1.3.2
|
### paperless-ng 1.3.2
|
||||||
|
|
||||||
@@ -1004,8 +1033,7 @@ This release contains new database migrations.
|
|||||||
- Changes
|
- Changes
|
||||||
- The REST API is versioned from this point onwards. This will
|
- The REST API is versioned from this point onwards. This will
|
||||||
allow me to make changes without breaking existing clients. See
|
allow me to make changes without breaking existing clients. See
|
||||||
the documentation about `api-versioning`{.interpreted-text
|
the documentation about [API versioning](/api#api-versioning) for details.
|
||||||
role="ref"} for details.
|
|
||||||
- Added a color picker for tag colors.
|
- Added a color picker for tag colors.
|
||||||
- Added the ability to use the filter for searching the document
|
- Added the ability to use the filter for searching the document
|
||||||
content as well.
|
content as well.
|
||||||
@@ -1039,7 +1067,7 @@ This release contains new database migrations.
|
|||||||
- Changes to the OCRmyPDF integration
|
- Changes to the OCRmyPDF integration
|
||||||
- Added support for deskewing and automatic rotation of
|
- Added support for deskewing and automatic rotation of
|
||||||
incorrectly rotated pages. This is enabled by default, see
|
incorrectly rotated pages. This is enabled by default, see
|
||||||
`configuration-ocr`{.interpreted-text role="ref"}.
|
[OCR settings](/configuration#ocr).
|
||||||
- Better support for encrypted files.
|
- Better support for encrypted files.
|
||||||
- Better support for various other PDF files: Paperless will now
|
- Better support for various other PDF files: Paperless will now
|
||||||
attempt to force OCR with safe options when OCR fails with the
|
attempt to force OCR with safe options when OCR fails with the
|
||||||
@@ -1066,7 +1094,7 @@ This release contains new database migrations.
|
|||||||
|
|
||||||
- Added a docker-specific configuration option to adjust the number of
|
- Added a docker-specific configuration option to adjust the number of
|
||||||
worker processes of the web server. See
|
worker processes of the web server. See
|
||||||
`configuration-docker`{.interpreted-text role="ref"}.
|
[Docker options](/configuration#docker).
|
||||||
- Some more memory usage optimizations.
|
- Some more memory usage optimizations.
|
||||||
- Don't show inbox statistics if no inbox tag is defined.
|
- Don't show inbox statistics if no inbox tag is defined.
|
||||||
|
|
||||||
@@ -1075,8 +1103,7 @@ This release contains new database migrations.
|
|||||||
- Always show top left corner of thumbnails, even for extra wide
|
- Always show top left corner of thumbnails, even for extra wide
|
||||||
documents.
|
documents.
|
||||||
- Added a management command for executing the sanity checker
|
- Added a management command for executing the sanity checker
|
||||||
directly. See `utilities-sanity-checker`{.interpreted-text
|
directly. See [management utilities](/administration#sanity-checker).
|
||||||
role="ref"}.
|
|
||||||
- The weekly sanity check now reports messages in the log files.
|
- The weekly sanity check now reports messages in the log files.
|
||||||
- Fixed an issue with the metadata tab not reporting anything in case
|
- Fixed an issue with the metadata tab not reporting anything in case
|
||||||
of missing files.
|
of missing files.
|
||||||
@@ -1110,7 +1137,7 @@ This release contains new database migrations.
|
|||||||
management commands, since these also ensure that they're always
|
management commands, since these also ensure that they're always
|
||||||
executed as the paperless user and you're less likely to run into
|
executed as the paperless user and you're less likely to run into
|
||||||
permission issues. See
|
permission issues. See
|
||||||
`utilities-management-commands`{.interpreted-text role="ref"}.
|
[management commands](/administration#management-commands).
|
||||||
|
|
||||||
### paperless-ng 1.1.0
|
### paperless-ng 1.1.0
|
||||||
|
|
||||||
@@ -1135,7 +1162,7 @@ This release contains new database migrations.
|
|||||||
For status notifications and live updates to work, paperless now
|
For status notifications and live updates to work, paperless now
|
||||||
requires an [ASGI](https://asgi.readthedocs.io/en/latest/)-enabled
|
requires an [ASGI](https://asgi.readthedocs.io/en/latest/)-enabled
|
||||||
web server. The docker images uses `gunicorn` and an ASGI-enabled
|
web server. The docker images uses `gunicorn` and an ASGI-enabled
|
||||||
worker called [uvicorn](http://www.uvicorn.org/), and there is no
|
worker called [uvicorn](https://www.uvicorn.org/), and there is no
|
||||||
need to configure anything.
|
need to configure anything.
|
||||||
|
|
||||||
For bare metal installations, changes are required for the
|
For bare metal installations, changes are required for the
|
||||||
@@ -1152,7 +1179,7 @@ This release contains new database migrations.
|
|||||||
status notifications.
|
status notifications.
|
||||||
|
|
||||||
Apache `mod_wsgi` users, see
|
Apache `mod_wsgi` users, see
|
||||||
`this note <faq-mod_wsgi>`{.interpreted-text role="ref"}.
|
[this note](/faq#how-do-i-get-websocket-support-with-apache-mod_wsgi).
|
||||||
|
|
||||||
- Paperless now offers suggestions for tags, correspondents and types
|
- Paperless now offers suggestions for tags, correspondents and types
|
||||||
on the document detail page.
|
on the document detail page.
|
||||||
@@ -1197,9 +1224,7 @@ bug reports coming in, I think that this is reasonably stable.
|
|||||||
- The document exporter locks the media directory and the database
|
- The document exporter locks the media directory and the database
|
||||||
during execution to ensure that the resulting export is
|
during execution to ensure that the resulting export is
|
||||||
consistent.
|
consistent.
|
||||||
- See the
|
- See the [updated documentation](/administration#exporter) for more details.
|
||||||
`updated documentation <utilities-exporter>`{.interpreted-text
|
|
||||||
role="ref"} for more details.
|
|
||||||
- Other changes and additions
|
- Other changes and additions
|
||||||
- Added a language selector to the settings.
|
- Added a language selector to the settings.
|
||||||
- Added date format options to the settings.
|
- Added date format options to the settings.
|
||||||
@@ -1288,11 +1313,11 @@ paperless.
|
|||||||
- Thanks to [Jo Vandeginste](https://github.com/jovandeginste),
|
- Thanks to [Jo Vandeginste](https://github.com/jovandeginste),
|
||||||
Paperless has optional support for Office documents such as .docx,
|
Paperless has optional support for Office documents such as .docx,
|
||||||
.doc, .odt and more.
|
.doc, .odt and more.
|
||||||
- See the `configuration<configuration-tika>`{.interpreted-text
|
- See the [Tika settings](/configuration#tika) on how to enable this
|
||||||
role="ref"} on how to enable this feature. This feature requires
|
feature. This feature requires two additional services (one for
|
||||||
two additional services (one for parsing Office documents and
|
parsing Office documents and metadata extraction and another for
|
||||||
metadata extraction and another for converting Office documents
|
converting Office documents to PDF), and is therefore not enabled
|
||||||
to PDF), and is therefore not enabled on default installations.
|
on default installations.
|
||||||
- As with all other documents, paperless converts Office documents
|
- As with all other documents, paperless converts Office documents
|
||||||
to PDF and stores both the original as well as the archived PDF.
|
to PDF and stores both the original as well as the archived PDF.
|
||||||
- Dark mode
|
- Dark mode
|
||||||
@@ -1368,15 +1393,14 @@ paperless.
|
|||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
The bulk delete operations did not update the search index. Therefore,
|
The bulk delete operations did not update the search index. Therefore,
|
||||||
documents that you deleted remained in the index and caused the search
|
documents that you deleted remained in the index and caused the search
|
||||||
to return messages about missing documents when searching. Further bulk
|
to return messages about missing documents when searching. Further bulk
|
||||||
operations will properly update the index.
|
operations will properly update the index.
|
||||||
|
|
||||||
However, this change is not retroactive: If you used the delete method
|
However, this change is not retroactive: If you used the delete method
|
||||||
of the bulk editor, you need to reindex your search index by
|
of the bulk editor, you need to reindex your search index by
|
||||||
`running the management command document_index with the argument reindex <administration-index>`{.interpreted-text
|
[running the management command `document_index` with the argument `reindex`](/administration#index).
|
||||||
role="ref"}.
|
|
||||||
|
|
||||||
### paperless-ng 0.9.9
|
### paperless-ng 0.9.9
|
||||||
|
|
||||||
@@ -1533,19 +1557,16 @@ primarily.
|
|||||||
edit page. If available, a dropdown menu will appear next to the
|
edit page. If available, a dropdown menu will appear next to the
|
||||||
download button.
|
download button.
|
||||||
- Many of the configuration options regarding OCR have changed.
|
- Many of the configuration options regarding OCR have changed.
|
||||||
See `configuration-ocr`{.interpreted-text role="ref"} for
|
See [OCR settings](/configuration#ocr) for details.
|
||||||
details.
|
|
||||||
- Paperless no longer guesses the language of your documents. It
|
- Paperless no longer guesses the language of your documents. It
|
||||||
always uses the language that you specified with
|
always uses the language that you specified with
|
||||||
`PAPERLESS_OCR_LANGUAGE`. Be sure to set this to the language
|
`PAPERLESS_OCR_LANGUAGE`. Be sure to set this to the language
|
||||||
the majority of your documents are in. Multiple languages can be
|
the majority of your documents are in. Multiple languages can be
|
||||||
specified, but that requires more CPU time.
|
specified, but that requires more CPU time.
|
||||||
- The management command
|
- The management command [`document_archiver`](/administration#archiver)
|
||||||
`document_archiver <utilities-archiver>`{.interpreted-text
|
can be used to create archived versions for already existing documents.
|
||||||
role="ref"} can be used to create archived versions for already
|
|
||||||
existing documents.
|
|
||||||
- Tags from consumption folder.
|
- Tags from consumption folder.
|
||||||
- Thanks to [jayme-github](http://github.com/jayme-github),
|
- Thanks to [jayme-github](https://github.com/jayme-github),
|
||||||
paperless now consumes files from sub folders in the consumption
|
paperless now consumes files from sub folders in the consumption
|
||||||
folder and is able to assign tags based on the sub folders a
|
folder and is able to assign tags based on the sub folders a
|
||||||
document was found in. This can be configured with
|
document was found in. This can be configured with
|
||||||
@@ -1556,7 +1577,7 @@ primarily.
|
|||||||
- The endpoint for uploading documents now supports specifying
|
- The endpoint for uploading documents now supports specifying
|
||||||
custom titles, correspondents, tags and types. This can be used
|
custom titles, correspondents, tags and types. This can be used
|
||||||
by clients to override the default behavior of paperless. See
|
by clients to override the default behavior of paperless. See
|
||||||
`api-file_uploads`{.interpreted-text role="ref"}.
|
[POSTing documents](/api#file-uploads).
|
||||||
- The document endpoint of API now serves documents in this form:
|
- The document endpoint of API now serves documents in this form:
|
||||||
- correspondents, document types and tags are referenced by
|
- correspondents, document types and tags are referenced by
|
||||||
their ID in the fields `correspondent`, `document_type` and
|
their ID in the fields `correspondent`, `document_type` and
|
||||||
@@ -1590,16 +1611,14 @@ primarily.
|
|||||||
- Paperless now supports searching by tags, types and dates and
|
- Paperless now supports searching by tags, types and dates and
|
||||||
correspondents. In order to have this applied to your existing
|
correspondents. In order to have this applied to your existing
|
||||||
documents, you need to perform a `document_index reindex`
|
documents, you need to perform a `document_index reindex`
|
||||||
management command (see `administration-index`{.interpreted-text
|
management command (see [document search index](/administration#index))
|
||||||
role="ref"}) that adds the data to the search index. You only
|
that adds the data to the search index. You only need to do this
|
||||||
need to do this once, since the schema of the search index
|
once, since the schema of the search index changed. Paperless
|
||||||
changed. Paperless keeps the index updated after that whenever
|
keeps the index updated after that whenever something changes.
|
||||||
something changes.
|
|
||||||
- Paperless now has spelling corrections ("Did you mean") for
|
- Paperless now has spelling corrections ("Did you mean") for
|
||||||
miss-typed queries.
|
miss-typed queries.
|
||||||
- The documentation contains
|
- The documentation contains
|
||||||
`information about the query syntax <basic-searching>`{.interpreted-text
|
[information about the query syntax](/usage#basic-usage_searching).
|
||||||
role="ref"}.
|
|
||||||
- Front end:
|
- Front end:
|
||||||
- Clickable tags, correspondents and types allow quick filtering
|
- Clickable tags, correspondents and types allow quick filtering
|
||||||
for related documents.
|
for related documents.
|
||||||
@@ -1660,10 +1679,8 @@ primarily.
|
|||||||
|
|
||||||
### paperless-ng 0.9.0
|
### paperless-ng 0.9.0
|
||||||
|
|
||||||
- **Deprecated:** GnuPG.
|
- **Deprecated:** GnuPG. [See this note on the state of GnuPG in paperless-ng.](/administration#encryption)
|
||||||
`See this note on the state of GnuPG in paperless-ng. <utilities-encyption>`{.interpreted-text
|
This features will most likely be removed in future versions.
|
||||||
role="ref"} This features will most likely be removed in future
|
|
||||||
versions.
|
|
||||||
- **Added:** New frontend. Features:
|
- **Added:** New frontend. Features:
|
||||||
- Single page application: It's much more responsive than the
|
- Single page application: It's much more responsive than the
|
||||||
django admin pages.
|
django admin pages.
|
||||||
@@ -1720,7 +1737,7 @@ primarily.
|
|||||||
uses PostgreSQL instead of SQLite. Username, database and
|
uses PostgreSQL instead of SQLite. Username, database and
|
||||||
password all default to `paperless` if not specified.
|
password all default to `paperless` if not specified.
|
||||||
- **Modified \[breaking\]:** document_retagger management command
|
- **Modified \[breaking\]:** document_retagger management command
|
||||||
rework. See `utilities-retagger`{.interpreted-text role="ref"} for
|
rework. See [Document retagger](/administration#retagger) for
|
||||||
details. Replaces `document_correspondents` management command.
|
details. Replaces `document_correspondents` management command.
|
||||||
- **Removed \[breaking\]:** Reminders.
|
- **Removed \[breaking\]:** Reminders.
|
||||||
- **Removed:** All customizations made to the django admin pages.
|
- **Removed:** All customizations made to the django admin pages.
|
||||||
@@ -1830,7 +1847,7 @@ primarily.
|
|||||||
### 2.5.0
|
### 2.5.0
|
||||||
|
|
||||||
- **New dependency**: Paperless now optimises thumbnail generation
|
- **New dependency**: Paperless now optimises thumbnail generation
|
||||||
with [optipng](http://optipng.sourceforge.net/), so you'll need to
|
with [optipng](https://optipng.sourceforge.net/), so you'll need to
|
||||||
install that somewhere in your PATH or declare its location in
|
install that somewhere in your PATH or declare its location in
|
||||||
`PAPERLESS_OPTIPNG_BINARY`. The Docker image has already been
|
`PAPERLESS_OPTIPNG_BINARY`. The Docker image has already been
|
||||||
updated on the Docker Hub, so you just need to pull the latest one
|
updated on the Docker Hub, so you just need to pull the latest one
|
||||||
|
@@ -10,12 +10,10 @@ run paperless, these settings have to be defined in different places.
|
|||||||
- If you are running paperless on anything else, paperless will search
|
- If you are running paperless on anything else, paperless will search
|
||||||
for the configuration file in these locations and use the first one
|
for the configuration file in these locations and use the first one
|
||||||
it finds:
|
it finds:
|
||||||
|
- The environment variable `PAPERLESS_CONFIGURATION_PATH`
|
||||||
```
|
- `/path/to/paperless/paperless.conf`
|
||||||
/path/to/paperless/paperless.conf
|
- `/etc/paperless.conf`
|
||||||
/etc/paperless.conf
|
- `/usr/local/etc/paperless.conf`
|
||||||
/usr/local/etc/paperless.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
## Required services
|
## Required services
|
||||||
|
|
||||||
@@ -33,19 +31,19 @@ matcher.
|
|||||||
[More information on securing your Redis
|
[More information on securing your Redis
|
||||||
Instance](https://redis.io/docs/getting-started/#securing-redis).
|
Instance](https://redis.io/docs/getting-started/#securing-redis).
|
||||||
|
|
||||||
Defaults to <redis://localhost:6379>.
|
Defaults to `redis://localhost:6379`.
|
||||||
|
|
||||||
`PAPERLESS_DBENGINE=<engine_name>`
|
`PAPERLESS_DBENGINE=<engine_name>`
|
||||||
|
|
||||||
: Optional, gives the ability to choose Postgres or MariaDB for
|
: Optional, gives the ability to choose Postgres or MariaDB for
|
||||||
database engine. Available options are [postgresql]{.title-ref} and
|
database engine. Available options are `postgresql` and
|
||||||
[mariadb]{.title-ref}.
|
`mariadb`.
|
||||||
|
|
||||||
Default is [postgresql]{.title-ref}.
|
Default is `postgresql`.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
Using MariaDB comes with some caveats. See [MySQL Caveats](advanced_usage#mysql-caveats).
|
Using MariaDB comes with some caveats. See [MySQL Caveats](/advanced_usage#mysql-caveats).
|
||||||
|
|
||||||
`PAPERLESS_DBHOST=<hostname>`
|
`PAPERLESS_DBHOST=<hostname>`
|
||||||
|
|
||||||
@@ -150,25 +148,38 @@ files created using "collectstatic" manager command are stored.
|
|||||||
`PAPERLESS_FILENAME_FORMAT=<format>`
|
`PAPERLESS_FILENAME_FORMAT=<format>`
|
||||||
|
|
||||||
: Changes the filenames paperless uses to store documents in the media
|
: Changes the filenames paperless uses to store documents in the media
|
||||||
directory. See [File name handling](advanced_usage#file_name_handling) for details.
|
directory. See [File name handling](/advanced_usage#file-name-handling) for details.
|
||||||
|
|
||||||
Default is none, which disables this feature.
|
Default is none, which disables this feature.
|
||||||
|
|
||||||
`PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=<bool>`
|
`PAPERLESS_FILENAME_FORMAT_REMOVE_NONE=<bool>`
|
||||||
|
|
||||||
: Tells paperless to replace placeholders in
|
: Tells paperless to replace placeholders in
|
||||||
[PAPERLESS_FILENAME_FORMAT]{.title-ref} that would resolve to
|
`PAPERLESS_FILENAME_FORMAT` that would resolve to
|
||||||
'none' to be omitted from the resulting filename. This also holds
|
'none' to be omitted from the resulting filename. This also holds
|
||||||
true for directory names. See [File name handling](advanced_usage#file_name_handling) for
|
true for directory names. See [File name handling](/advanced_usage#file-name-handling) for
|
||||||
details.
|
details.
|
||||||
|
|
||||||
Defaults to [false]{.title-ref} which disables this feature.
|
Defaults to `false` which disables this feature.
|
||||||
|
|
||||||
`PAPERLESS_LOGGING_DIR=<path>`
|
`PAPERLESS_LOGGING_DIR=<path>`
|
||||||
|
|
||||||
: This is where paperless will store log files.
|
: This is where paperless will store log files.
|
||||||
|
|
||||||
Defaults to "`PAPERLESS_DATA_DIR`/log/".
|
Defaults to `PAPERLESS_DATA_DIR/log/`.
|
||||||
|
|
||||||
|
`PAPERLESS_NLTK_DIR=<path>`
|
||||||
|
|
||||||
|
: This is where paperless will search for the data required for NLTK
|
||||||
|
processing, if you are using it. If you are using the Docker image,
|
||||||
|
this should not be changed, as the data is included in the image
|
||||||
|
already.
|
||||||
|
|
||||||
|
Previously, the location defaulted to `PAPERLESS_DATA_DIR/nltk`.
|
||||||
|
Unless you are using this in a bare metal install or other setup,
|
||||||
|
this folder is no longer needed and can be removed manually.
|
||||||
|
|
||||||
|
Defaults to `/usr/local/share/nltk_data`
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
@@ -283,10 +294,10 @@ login with the selected user.
|
|||||||
: If this environment variable is specified, Paperless automatically
|
: If this environment variable is specified, Paperless automatically
|
||||||
creates a superuser with the provided username at start. This is
|
creates a superuser with the provided username at start. This is
|
||||||
useful in cases where you can not run the
|
useful in cases where you can not run the
|
||||||
[createsuperuser]{.title-ref} command separately, such as Kubernetes
|
`createsuperuser` command separately, such as Kubernetes
|
||||||
or AWS ECS.
|
or AWS ECS.
|
||||||
|
|
||||||
Requires [PAPERLESS_ADMIN_PASSWORD]{.title-ref} to be set.
|
Requires PAPERLESS_ADMIN_PASSWORD be set.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
@@ -297,13 +308,13 @@ or AWS ECS.
|
|||||||
`PAPERLESS_ADMIN_MAIL=<email>`
|
`PAPERLESS_ADMIN_MAIL=<email>`
|
||||||
|
|
||||||
: (Optional) Specify superuser email address. Only used when
|
: (Optional) Specify superuser email address. Only used when
|
||||||
[PAPERLESS_ADMIN_USER]{.title-ref} is set.
|
PAPERLESS_ADMIN_USER is set.
|
||||||
|
|
||||||
Defaults to `root@localhost`.
|
Defaults to `root@localhost`.
|
||||||
|
|
||||||
`PAPERLESS_ADMIN_PASSWORD=<password>`
|
`PAPERLESS_ADMIN_PASSWORD=<password>`
|
||||||
|
|
||||||
: Only used when [PAPERLESS_ADMIN_USER]{.title-ref} is set. This will
|
: Only used when PAPERLESS_ADMIN_USER is set. This will
|
||||||
be the password of the automatically created superuser.
|
be the password of the automatically created superuser.
|
||||||
|
|
||||||
`PAPERLESS_COOKIE_PREFIX=<str>`
|
`PAPERLESS_COOKIE_PREFIX=<str>`
|
||||||
@@ -331,26 +342,25 @@ applications.
|
|||||||
If you're exposing paperless to the internet directly, do not use
|
If you're exposing paperless to the internet directly, do not use
|
||||||
this.
|
this.
|
||||||
|
|
||||||
Also see the warning [in the official documentation
|
Also see the warning [in the official documentation](https://docs.djangoproject.com/en/3.1/howto/auth-remote-user/#configuration).
|
||||||
<https://docs.djangoproject.com/en/3.1/howto/auth-remote-user/#configuration>]{.title-ref}.
|
|
||||||
|
|
||||||
Defaults to [false]{.title-ref} which disables this feature.
|
Defaults to "false" which disables this feature.
|
||||||
|
|
||||||
`PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=<str>`
|
`PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=<str>`
|
||||||
|
|
||||||
: If [PAPERLESS_ENABLE_HTTP_REMOTE_USER]{.title-ref} is enabled, this
|
: If "PAPERLESS*ENABLE_HTTP_REMOTE_USER" is enabled, this
|
||||||
property allows to customize the name of the HTTP header from which
|
property allows to customize the name of the HTTP header from which
|
||||||
the authenticated username is extracted. Values are in terms of
|
the authenticated username is extracted. Values are in terms of
|
||||||
\[HttpRequest.META\](<https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpRequest.META>).
|
[HttpRequest.META](https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpRequest.META).
|
||||||
Thus, the configured value must start with [HTTP\_]{.title-ref}
|
Thus, the configured value must start with `HTTP*`
|
||||||
followed by the normalized actual header name.
|
followed by the normalized actual header name.
|
||||||
|
|
||||||
Defaults to [HTTP_REMOTE_USER]{.title-ref}.
|
Defaults to "HTTP_REMOTE_USER".
|
||||||
|
|
||||||
`PAPERLESS_LOGOUT_REDIRECT_URL=<str>`
|
`PAPERLESS_LOGOUT_REDIRECT_URL=<str>`
|
||||||
|
|
||||||
: URL to redirect the user to after a logout. This can be used
|
: URL to redirect the user to after a logout. This can be used
|
||||||
together with [PAPERLESS_ENABLE_HTTP_REMOTE_USER]{.title-ref} to
|
together with PAPERLESS_ENABLE_HTTP_REMOTE_USER to
|
||||||
redirect the user back to the SSO application's logout page.
|
redirect the user back to the SSO application's logout page.
|
||||||
|
|
||||||
Defaults to None, which disables this feature.
|
Defaults to None, which disables this feature.
|
||||||
@@ -368,7 +378,7 @@ needs.
|
|||||||
parsing documents.
|
parsing documents.
|
||||||
|
|
||||||
It should be a 3-letter language code consistent with ISO 639:
|
It should be a 3-letter language code consistent with ISO 639:
|
||||||
<https://www.loc.gov/standards/iso639-2/php/code_list.php>
|
https://www.loc.gov/standards/iso639-2/php/code_list.php
|
||||||
|
|
||||||
Set this to the language most of your documents are written in.
|
Set this to the language most of your documents are written in.
|
||||||
|
|
||||||
@@ -565,8 +575,10 @@ they use underscores instead of dashes.
|
|||||||
|
|
||||||
Paperless can make use of [Tika](https://tika.apache.org/) and
|
Paperless can make use of [Tika](https://tika.apache.org/) and
|
||||||
[Gotenberg](https://gotenberg.dev/) for parsing and converting
|
[Gotenberg](https://gotenberg.dev/) for parsing and converting
|
||||||
"Office" documents (such as ".doc", ".xlsx" and ".odt"). If you
|
"Office" documents (such as ".doc", ".xlsx" and ".odt").
|
||||||
wish to use this, you must provide a Tika server and a Gotenberg server,
|
Tika and Gotenberg are also needed to allow parsing of E-Mails (.eml).
|
||||||
|
|
||||||
|
If you wish to use this, you must provide a Tika server and a Gotenberg server,
|
||||||
configure their endpoints, and enable the feature.
|
configure their endpoints, and enable the feature.
|
||||||
|
|
||||||
`PAPERLESS_TIKA_ENABLED=<bool>`
|
`PAPERLESS_TIKA_ENABLED=<bool>`
|
||||||
@@ -610,9 +622,12 @@ services:
|
|||||||
gotenberg:
|
gotenberg:
|
||||||
image: gotenberg/gotenberg:7.6
|
image: gotenberg/gotenberg:7.6
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
command:
|
command:
|
||||||
- 'gotenberg'
|
- 'gotenberg'
|
||||||
- '--chromium-disable-routes=true'
|
- '--chromium-disable-javascript=true'
|
||||||
|
- '--chromium-allow-list=file:///tmp/.*'
|
||||||
|
|
||||||
tika:
|
tika:
|
||||||
image: ghcr.io/paperless-ngx/tika:latest
|
image: ghcr.io/paperless-ngx/tika:latest
|
||||||
@@ -624,8 +639,7 @@ Add the configuration variables to the environment of the webserver
|
|||||||
and add the additional services below the webserver service. Watch out
|
and add the additional services below the webserver service. Watch out
|
||||||
for indentation.
|
for indentation.
|
||||||
|
|
||||||
Make sure to use the correct format [PAPERLESS_TIKA_ENABLED =
|
Make sure to use the correct format `PAPERLESS_TIKA_ENABLED = 1` so python_dotenv can parse the statement correctly.
|
||||||
1]{.title-ref} so python_dotenv can parse the statement correctly.
|
|
||||||
|
|
||||||
## Software tweaks {#software_tweaks}
|
## Software tweaks {#software_tweaks}
|
||||||
|
|
||||||
@@ -648,7 +662,7 @@ paperless will process in parallel on a single document.
|
|||||||
|
|
||||||
Ensure that the product
|
Ensure that the product
|
||||||
|
|
||||||
`PAPERLESS_TASK_WORKERS \: PAPERLESS_THREADS_PER_WORKER`
|
`PAPERLESS_TASK_WORKERS * PAPERLESS_THREADS_PER_WORKER`
|
||||||
|
|
||||||
does not exceed your CPU core count or else paperless will be
|
does not exceed your CPU core count or else paperless will be
|
||||||
extremely slow. If you want paperless to process many documents in
|
extremely slow. If you want paperless to process many documents in
|
||||||
@@ -660,7 +674,7 @@ paperless will process in parallel on a single document.
|
|||||||
count, with a slight favor towards threads per worker:
|
count, with a slight favor towards threads per worker:
|
||||||
|
|
||||||
| CPU core count | Workers | Threads |
|
| CPU core count | Workers | Threads |
|
||||||
|----------------|---------|---------|
|
| -------------- | ------- | ------- |
|
||||||
| > 1 | > 1 | > 1 |
|
| > 1 | > 1 | > 1 |
|
||||||
| > 2 | > 2 | > 1 |
|
| > 2 | > 2 | > 1 |
|
||||||
| > 4 | > 2 | > 2 |
|
| > 4 | > 2 | > 2 |
|
||||||
@@ -693,6 +707,16 @@ for details on how to set it.
|
|||||||
|
|
||||||
Defaults to UTC.
|
Defaults to UTC.
|
||||||
|
|
||||||
|
`PAPERLESS_ENABLE_NLTK=<bool>`
|
||||||
|
|
||||||
|
: Enables or disables the advanced natural language processing
|
||||||
|
used during automatic classification. If disabled, paperless will
|
||||||
|
still preform some basic text pre-processing before matching.
|
||||||
|
|
||||||
|
See also `PAPERLESS_NLTK_DIR`.
|
||||||
|
|
||||||
|
Defaults to 1.
|
||||||
|
|
||||||
## Polling {#polling}
|
## Polling {#polling}
|
||||||
|
|
||||||
`PAPERLESS_CONSUMER_POLLING=<num>`
|
`PAPERLESS_CONSUMER_POLLING=<num>`
|
||||||
@@ -752,7 +776,7 @@ consumption directory as well.
|
|||||||
`PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=<bool>`
|
`PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=<bool>`
|
||||||
|
|
||||||
: Set the names of subdirectories as tags for consumed files. E.g.
|
: Set the names of subdirectories as tags for consumed files. E.g.
|
||||||
<CONSUMPTION_DIR>/foo/bar/file.pdf will add the tags "foo" and
|
`<CONSUMPTION_DIR>/foo/bar/file.pdf` will add the tags "foo" and
|
||||||
"bar" to the consumed file. Paperless will create any tags that
|
"bar" to the consumed file. Paperless will create any tags that
|
||||||
don't exist yet.
|
don't exist yet.
|
||||||
|
|
||||||
@@ -827,7 +851,7 @@ documents.
|
|||||||
|
|
||||||
: After a document is consumed, Paperless can trigger an arbitrary
|
: After a document is consumed, Paperless can trigger an arbitrary
|
||||||
script if you like. This script will be passed a number of arguments
|
script if you like. This script will be passed a number of arguments
|
||||||
for you to work with. For more information, take a look at [Post-consumption script](advanced_usage#post_consume_script).
|
for you to work with. For more information, take a look at [Post-consumption script](/advanced_usage#post-consume-script).
|
||||||
|
|
||||||
The default is blank, which means nothing will be executed.
|
The default is blank, which means nothing will be executed.
|
||||||
|
|
||||||
@@ -841,8 +865,7 @@ option as specified in
|
|||||||
The filename will be checked first, and if nothing is found, the
|
The filename will be checked first, and if nothing is found, the
|
||||||
document text will be checked as normal.
|
document text will be checked as normal.
|
||||||
|
|
||||||
A date in a filename must have some separators ([.]{.title-ref},
|
A date in a filename must have some separators (`.`, `,`, `-`, `/`, etc) for it to be parsed.
|
||||||
[-]{.title-ref}, [/]{.title-ref}, etc) for it to be parsed.
|
|
||||||
|
|
||||||
Defaults to none, which disables this feature.
|
Defaults to none, which disables this feature.
|
||||||
|
|
||||||
@@ -928,7 +951,7 @@ the literal path for that program.
|
|||||||
|
|
||||||
These options don't have any effect in `paperless.conf`. These options
|
These options don't have any effect in `paperless.conf`. These options
|
||||||
adjust the behavior of the docker container. Configure these in
|
adjust the behavior of the docker container. Configure these in
|
||||||
[docker-compose.env]{.title-ref}.
|
`docker-compose.env`.
|
||||||
|
|
||||||
`PAPERLESS_WEBSERVER_WORKERS=<num>`
|
`PAPERLESS_WEBSERVER_WORKERS=<num>`
|
||||||
|
|
||||||
@@ -946,7 +969,7 @@ increase RAM usage.
|
|||||||
There are special setups where you may need to configure this value
|
There are special setups where you may need to configure this value
|
||||||
to restrict the Ip address or interface the webserver listens on.
|
to restrict the Ip address or interface the webserver listens on.
|
||||||
|
|
||||||
Defaults to \[::\], meaning all interfaces, including IPv6.
|
Defaults to `[::]`, meaning all interfaces, including IPv6.
|
||||||
|
|
||||||
`PAPERLESS_PORT=<port>`
|
`PAPERLESS_PORT=<port>`
|
||||||
|
|
||||||
@@ -1017,7 +1040,7 @@ configuration option:
|
|||||||
[Flower](https://flower.readthedocs.io/en/latest/index.html) will be
|
[Flower](https://flower.readthedocs.io/en/latest/index.html) will be
|
||||||
started by the container.
|
started by the container.
|
||||||
|
|
||||||
You can read more about this in the [advanced documentation](advanced#celery-monitoring).
|
You can read more about this in the [advanced documentation](/advanced_usage#celery-monitoring).
|
||||||
|
|
||||||
## Update Checking {#update-checking}
|
## Update Checking {#update-checking}
|
||||||
|
|
||||||
|
@@ -39,16 +39,16 @@ guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTIN
|
|||||||
## Code formatting with pre-commit Hooks
|
## Code formatting with pre-commit Hooks
|
||||||
|
|
||||||
To ensure a consistent style and formatting across the project source,
|
To ensure a consistent style and formatting across the project source,
|
||||||
the project utilizes a Git [pre-commit]{.title-ref} hook to perform some
|
the project utilizes a Git [`pre-commit`](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)
|
||||||
formatting and linting before a commit is allowed. That way, everyone
|
hook to perform some formatting and linting before a commit is allowed.
|
||||||
uses the same style and some common issues can be caught early on. See
|
That way, everyone uses the same style and some common issues can be caught
|
||||||
below for installation instructions.
|
early on. See below for installation instructions.
|
||||||
|
|
||||||
Once installed, hooks will run when you commit. If the formatting isn't
|
Once installed, hooks will run when you commit. If the formatting isn't
|
||||||
quite right or a linter catches something, the commit will be rejected.
|
quite right or a linter catches something, the commit will be rejected.
|
||||||
You'll need to look at the output and fix the issue. Some hooks, such
|
You'll need to look at the output and fix the issue. Some hooks, such
|
||||||
as the Python formatting tool [black]{.title-ref}, will format failing
|
as the Python formatting tool `black`, will format failing
|
||||||
files, so all you need to do is [git add]{.title-ref} those files again
|
files, so all you need to do is `git add` those files again
|
||||||
and retry your commit.
|
and retry your commit.
|
||||||
|
|
||||||
## Initial setup and first start
|
## Initial setup and first start
|
||||||
@@ -58,7 +58,7 @@ first-time setup. To do the setup you need to perform the steps from the
|
|||||||
following chapters in a certain order:
|
following chapters in a certain order:
|
||||||
|
|
||||||
1. Install prerequisites + pipenv as mentioned in
|
1. Install prerequisites + pipenv as mentioned in
|
||||||
`[Bare metal route](/setup#bare_metal)
|
[Bare metal route](/setup#bare_metal)
|
||||||
|
|
||||||
2. Copy `paperless.conf.example` to `paperless.conf` and enable debug
|
2. Copy `paperless.conf.example` to `paperless.conf` and enable debug
|
||||||
mode.
|
mode.
|
||||||
@@ -69,7 +69,7 @@ following chapters in a certain order:
|
|||||||
$ npm install -g @angular/cli
|
$ npm install -g @angular/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Install pre-commit
|
4. Install pre-commit hooks
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
pre-commit install
|
pre-commit install
|
||||||
@@ -81,7 +81,7 @@ following chapters in a certain order:
|
|||||||
mkdir -p consume media
|
mkdir -p consume media
|
||||||
```
|
```
|
||||||
|
|
||||||
6. You can now either \...
|
6. You can now either ...
|
||||||
|
|
||||||
- install redis or
|
- install redis or
|
||||||
|
|
||||||
@@ -91,9 +91,9 @@ following chapters in a certain order:
|
|||||||
|
|
||||||
- spin up a bare redis container
|
- spin up a bare redis container
|
||||||
|
|
||||||
> ```shell-session
|
```shell-session
|
||||||
> docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
|
||||||
> ```
|
```
|
||||||
|
|
||||||
7. Install the python dependencies by performing in the src/ directory.
|
7. Install the python dependencies by performing in the src/ directory.
|
||||||
|
|
||||||
@@ -101,10 +101,12 @@ following chapters in a certain order:
|
|||||||
pipenv install --dev
|
pipenv install --dev
|
||||||
```
|
```
|
||||||
|
|
||||||
> - Make sure you're using python 3.9.x or lower. Otherwise you might
|
!!! note
|
||||||
> get issues with building dependencies. You can use
|
|
||||||
> [pyenv](https://github.com/pyenv/pyenv) to install a specific
|
Make sure you're using python 3.10.x or lower. Otherwise you might
|
||||||
> python version.
|
get issues with building dependencies. You can use
|
||||||
|
[pyenv](https://github.com/pyenv/pyenv) to install a specific
|
||||||
|
python version.
|
||||||
|
|
||||||
8. Generate the static UI so you can perform a login to get session
|
8. Generate the static UI so you can perform a login to get session
|
||||||
that is required for frontend development (this needs to be done one
|
that is required for frontend development (this needs to be done one
|
||||||
@@ -126,9 +128,9 @@ following chapters in a certain order:
|
|||||||
you're developing for, you need to have some or all of them
|
you're developing for, you need to have some or all of them
|
||||||
running.
|
running.
|
||||||
|
|
||||||
> ```shell-session
|
```shell-session
|
||||||
> python3 manage.py runserver & python3 manage.py document_consumer & celery --app paperless worker
|
python3 manage.py runserver & python3 manage.py document_consumer & celery --app paperless worker
|
||||||
> ```
|
```
|
||||||
|
|
||||||
11. Login with the superuser credentials provided in step 8 at
|
11. Login with the superuser credentials provided in step 8 at
|
||||||
`http://localhost:8000` to create a session that enables you to use
|
`http://localhost:8000` to create a session that enables you to use
|
||||||
@@ -140,15 +142,15 @@ development go to `/src-ui` and run `ng serve`. From there you can use
|
|||||||
|
|
||||||
## Back end development
|
## Back end development
|
||||||
|
|
||||||
The backend is a django application. PyCharm works well for development,
|
The backend is a [Django](https://www.djangoproject.com/) application. PyCharm works well for development,
|
||||||
but you can use whatever you want.
|
but you can use whatever you want.
|
||||||
|
|
||||||
Configure the IDE to use the src/ folder as the base source folder.
|
Configure the IDE to use the src/ folder as the base source folder.
|
||||||
Configure the following launch configurations in your IDE:
|
Configure the following launch configurations in your IDE:
|
||||||
|
|
||||||
- python3 manage.py runserver
|
- `python3 manage.py runserver`
|
||||||
- celery \--app paperless worker
|
- `celery --app paperless worker`
|
||||||
- python3 manage.py document_consumer
|
- `python3 manage.py document_consumer`
|
||||||
|
|
||||||
To start them all:
|
To start them all:
|
||||||
|
|
||||||
@@ -158,24 +160,26 @@ python3 manage.py runserver & python3 manage.py document_consumer & celery --app
|
|||||||
|
|
||||||
Testing and code style:
|
Testing and code style:
|
||||||
|
|
||||||
- Run `pytest` in the src/ directory to execute all tests. This also
|
- Run `pytest` in the `src/` directory to execute all tests. This also
|
||||||
generates a HTML coverage report. When runnings test, paperless.conf
|
generates a HTML coverage report. When runnings test, paperless.conf
|
||||||
is loaded as well. However: the tests rely on the default
|
is loaded as well. However: the tests rely on the default
|
||||||
configuration. This is not ideal. But for now, make sure no settings
|
configuration. This is not ideal. But for now, make sure no settings
|
||||||
except for DEBUG are overridden when testing.
|
except for DEBUG are overridden when testing.
|
||||||
|
|
||||||
- Coding style is enforced by the Git pre-commit hooks. These will
|
- Coding style is enforced by the Git pre-commit hooks. These will
|
||||||
ensure your code is formatted and do some linting when you do a [git
|
ensure your code is formatted and do some linting when you do a `git commit`.
|
||||||
commit]{.title-ref}.
|
|
||||||
|
|
||||||
- You can also run `black` manually to format your code
|
- You can also run `black` manually to format your code
|
||||||
|
|
||||||
!!! note
|
- The `pre-commit` hooks will modify files and interact with each other.
|
||||||
|
It may take a couple of `git add`, `git commit` cycle to satisfy them.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
The line length rule E501 is generally useful for getting multiple
|
The line length rule E501 is generally useful for getting multiple
|
||||||
source files next to each other on the screen. However, in some
|
source files next to each other on the screen. However, in some
|
||||||
cases, its just not possible to make some lines fit, especially
|
cases, its just not possible to make some lines fit, especially
|
||||||
complicated IF cases. Append `# NOQA: E501` to disable this check
|
complicated IF cases. Append `# noqa: E501` to disable this check
|
||||||
for certain lines.
|
for certain lines.
|
||||||
|
|
||||||
## Front end development
|
## Front end development
|
||||||
@@ -353,7 +357,8 @@ LANGUAGES = [
|
|||||||
|
|
||||||
## Building the documentation
|
## Building the documentation
|
||||||
|
|
||||||
The documentation is built using material-mkdocs, see their [documentation](https://squidfunk.github.io/mkdocs-material/reference/). If you want to build the documentation locally, this is how you do it:
|
The documentation is built using material-mkdocs, see their [documentation](https://squidfunk.github.io/mkdocs-material/reference/).
|
||||||
|
If you want to build the documentation locally, this is how you do it:
|
||||||
|
|
||||||
1. Install python dependencies.
|
1. Install python dependencies.
|
||||||
|
|
||||||
@@ -366,7 +371,7 @@ The documentation is built using material-mkdocs, see their [documentation](http
|
|||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ cd /path/to/paperless
|
$ cd /path/to/paperless
|
||||||
$ pipenv mkdocs build
|
$ pipenv mkdocs build --config-file mkdocs.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the Docker image
|
## Building the Docker image
|
||||||
@@ -379,9 +384,9 @@ helper script `build-docker-image.sh`.
|
|||||||
|
|
||||||
Building the docker image from source:
|
Building the docker image from source:
|
||||||
|
|
||||||
> ```shell-session
|
```shell-session
|
||||||
> ./build-docker-image.sh Dockerfile -t <your-tag>
|
./build-docker-image.sh Dockerfile -t <your-tag>
|
||||||
> ```
|
```
|
||||||
|
|
||||||
## Extending Paperless
|
## Extending Paperless
|
||||||
|
|
||||||
@@ -428,7 +433,7 @@ class MyCustomParser(DocumentParser):
|
|||||||
def get_thumbnail(self, document_path, mime_type):
|
def get_thumbnail(self, document_path, mime_type):
|
||||||
# This should return the path to a thumbnail you created for this
|
# This should return the path to a thumbnail you created for this
|
||||||
# document.
|
# document.
|
||||||
return os.path.join(self.tempdir, "thumb.png")
|
return os.path.join(self.tempdir, "thumb.webp")
|
||||||
```
|
```
|
||||||
|
|
||||||
If you encounter any issues during parsing, raise a
|
If you encounter any issues during parsing, raise a
|
||||||
|
38
docs/faq.md
38
docs/faq.md
@@ -1,6 +1,6 @@
|
|||||||
# Frequently Asked Questions
|
# Frequently Asked Questions
|
||||||
|
|
||||||
### _What's the general plan for Paperless-ngx?_
|
## _What's the general plan for Paperless-ngx?_
|
||||||
|
|
||||||
**A:** While Paperless-ngx is already considered largely
|
**A:** While Paperless-ngx is already considered largely
|
||||||
"feature-complete" it is a community-driven project and development
|
"feature-complete" it is a community-driven project and development
|
||||||
@@ -9,7 +9,7 @@ discussions and "up-voted" by the community but this is not a
|
|||||||
guarantee the feature will be implemented. This project will always be
|
guarantee the feature will be implemented. This project will always be
|
||||||
open to collaboration in the form of PRs, ideas etc.
|
open to collaboration in the form of PRs, ideas etc.
|
||||||
|
|
||||||
### _I'm using docker. Where are my documents?_
|
## _I'm using docker. Where are my documents?_
|
||||||
|
|
||||||
**A:** Your documents are stored inside the docker volume
|
**A:** Your documents are stored inside the docker volume
|
||||||
`paperless_media`. Docker manages this volume automatically for you. It
|
`paperless_media`. Docker manages this volume automatically for you. It
|
||||||
@@ -27,9 +27,7 @@ system. On Linux, chances are high that this location is
|
|||||||
files around manually. This folder is meant to be entirely managed by
|
files around manually. This folder is meant to be entirely managed by
|
||||||
docker and paperless.
|
docker and paperless.
|
||||||
|
|
||||||
### Let's say I want to switch tools in a year. Can I easily move
|
## Let's say I want to switch tools in a year. Can I easily move to other systems?
|
||||||
|
|
||||||
to other systems?\*
|
|
||||||
|
|
||||||
**A:** Your documents are stored as plain files inside the media folder.
|
**A:** Your documents are stored as plain files inside the media folder.
|
||||||
You can always drag those files out of that folder to use them
|
You can always drag those files out of that folder to use them
|
||||||
@@ -41,27 +39,27 @@ elsewhere. Here are a couple notes about that.
|
|||||||
- By default, paperless uses the internal ID of each document as its
|
- By default, paperless uses the internal ID of each document as its
|
||||||
filename. This might not be very convenient for export. However, you
|
filename. This might not be very convenient for export. However, you
|
||||||
can adjust the way files are stored in paperless by
|
can adjust the way files are stored in paperless by
|
||||||
[configuring the filename format](advanced_usage#file_name_handling).
|
[configuring the filename format](/advanced_usage#file-name-handling).
|
||||||
- [The exporter](administration#exporter) is
|
- [The exporter](/administration#exporter) is
|
||||||
another easy way to get your files out of paperless with reasonable
|
another easy way to get your files out of paperless with reasonable
|
||||||
file names.
|
file names.
|
||||||
|
|
||||||
### _What file types does paperless-ngx support?_
|
## _What file types does paperless-ngx support?_
|
||||||
|
|
||||||
**A:** Currently, the following files are supported:
|
**A:** Currently, the following files are supported:
|
||||||
|
|
||||||
- PDF documents, PNG images, JPEG images, TIFF images and GIF images
|
- PDF documents, PNG images, JPEG images, TIFF images, GIF images and
|
||||||
are processed with OCR and converted into PDF documents.
|
WebP images are processed with OCR and converted into PDF documents.
|
||||||
- Plain text documents are supported as well and are added verbatim to
|
- Plain text documents are supported as well and are added verbatim to
|
||||||
paperless.
|
paperless.
|
||||||
- With the optional Tika integration enabled (see [Tika configuration](configuration#tika),
|
- With the optional Tika integration enabled (see [Tika configuration](/configuration#tika),
|
||||||
Paperless also supports various Office documents (.docx, .doc, odt,
|
Paperless also supports various Office documents (.docx, .doc, odt,
|
||||||
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
|
||||||
|
|
||||||
Paperless-ngx determines the type of a file by inspecting its content.
|
Paperless-ngx determines the type of a file by inspecting its content.
|
||||||
The file extensions do not matter.
|
The file extensions do not matter.
|
||||||
|
|
||||||
### _Will paperless-ngx run on Raspberry Pi?_
|
## _Will paperless-ngx run on Raspberry Pi?_
|
||||||
|
|
||||||
**A:** The short answer is yes. I've tested it on a Raspberry Pi 3 B.
|
**A:** The short answer is yes. I've tested it on a Raspberry Pi 3 B.
|
||||||
The long answer is that certain parts of Paperless will run very slow,
|
The long answer is that certain parts of Paperless will run very slow,
|
||||||
@@ -73,11 +71,11 @@ has to do much less work to serve the data.
|
|||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
You can adjust some of the settings so that paperless uses less
|
You can adjust some of the settings so that paperless uses less
|
||||||
processing power. See [setup](setup#less_powerful_devices) for details.
|
processing power. See [setup](/setup#less-powerful-devices) for details.
|
||||||
|
|
||||||
### _How do I install paperless-ngx on Raspberry Pi?_
|
## _How do I install paperless-ngx on Raspberry Pi?_
|
||||||
|
|
||||||
**A:** Docker images are available for arm and arm64 hardware, so just
|
**A:** Docker images are available for armv7 and arm64 hardware, so just
|
||||||
follow the docker-compose instructions. Apart from more required disk
|
follow the docker-compose instructions. Apart from more required disk
|
||||||
space compared to a bare metal installation, docker comes with close to
|
space compared to a bare metal installation, docker comes with close to
|
||||||
zero overhead, even on Raspberry Pi.
|
zero overhead, even on Raspberry Pi.
|
||||||
@@ -87,13 +85,13 @@ the python requirements do not have precompiled packages for ARM /
|
|||||||
ARM64. Installation of these will require additional development
|
ARM64. Installation of these will require additional development
|
||||||
libraries and compilation will take a long time.
|
libraries and compilation will take a long time.
|
||||||
|
|
||||||
### _How do I run this on Unraid?_
|
## _How do I run this on Unraid?_
|
||||||
|
|
||||||
**A:** Paperless-ngx is available as [community
|
**A:** Paperless-ngx is available as [community
|
||||||
app](https://unraid.net/community/apps?q=paperless-ngx) in Unraid. [Uli
|
app](https://unraid.net/community/apps?q=paperless-ngx) in Unraid. [Uli
|
||||||
Fahrer](https://github.com/Tooa) created a container template for that.
|
Fahrer](https://github.com/Tooa) created a container template for that.
|
||||||
|
|
||||||
### _How do I run this on my toaster?_
|
## _How do I run this on my toaster?_
|
||||||
|
|
||||||
**A:** I honestly don't know! As for all other devices that might be
|
**A:** I honestly don't know! As for all other devices that might be
|
||||||
able to run paperless, you're a bit on your own. If you can't run the
|
able to run paperless, you're a bit on your own. If you can't run the
|
||||||
@@ -103,11 +101,11 @@ This is also what I use to test new releases with. Apart from that, I
|
|||||||
also have a Raspberry Pi, which I occasionally build the image on and
|
also have a Raspberry Pi, which I occasionally build the image on and
|
||||||
see if it works.
|
see if it works.
|
||||||
|
|
||||||
### _How do I proxy this with NGINX?_
|
## _How do I proxy this with NGINX?_
|
||||||
|
|
||||||
**A:** See [here](setup#nginx).
|
**A:** See [here](/setup#nginx).
|
||||||
|
|
||||||
### _How do I get WebSocket support with Apache mod_wsgi_?
|
## _How do I get WebSocket support with Apache mod_wsgi_?
|
||||||
|
|
||||||
**A:** `mod_wsgi` by itself does not support ASGI. Paperless will
|
**A:** `mod_wsgi` by itself does not support ASGI. Paperless will
|
||||||
continue to work with WSGI, but certain features such as status
|
continue to work with WSGI, but certain features such as status
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
**Paperless-ngx** is a _community-supported_ open-source document management system that transforms your
|
**Paperless-ngx** is a _community-supported_ open-source document management system that transforms your
|
||||||
physical documents into a searchable online archive so you can keep, well, _less paper_.
|
physical documents into a searchable online archive so you can keep, well, _less paper_.
|
||||||
|
|
||||||
[Get started](/setup/){ .md-button .md-button--primary .index-callout }
|
[Get started](/setup){ .md-button .md-button--primary .index-callout }
|
||||||
[Demo](https://demo.paperless-ngx.com){ .md-button .md-button--secondary target=\_blank }
|
[Demo](https://demo.paperless-ngx.com){ .md-button .md-button--secondary target=\_blank }
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -50,12 +50,12 @@ If you want to learn about what's different in paperless-ngx from
|
|||||||
Paperless, check out these resources in the documentation:
|
Paperless, check out these resources in the documentation:
|
||||||
|
|
||||||
- [Some screenshots](#screenshots) of the new UI are available.
|
- [Some screenshots](#screenshots) of the new UI are available.
|
||||||
- Read [this section](/advanced_usage/#advanced-automatic_matching) if you want to learn about how paperless automates all
|
- Read [this section](/advanced_usage#automatic-matching) if you want to learn about how paperless automates all
|
||||||
tagging using machine learning.
|
tagging using machine learning.
|
||||||
- Paperless now comes with a [proper email consumer](/usage/#usage-email) that's fully tested and production ready.
|
- Paperless now comes with a [proper email consumer](/usage#usage-email) that's fully tested and production ready.
|
||||||
- Paperless creates searchable PDF/A documents from whatever you put into the consumption directory. This means
|
- Paperless creates searchable PDF/A documents from whatever you put into the consumption directory. This means
|
||||||
that you can select text in image-only documents coming from your scanner.
|
that you can select text in image-only documents coming from your scanner.
|
||||||
- See [this note](/administration/#utilities-encyption) about GnuPG encryption in paperless-ngx.
|
- See [this note](/administration#encryption) about GnuPG encryption in paperless-ngx.
|
||||||
- Paperless is now integrated with a
|
- Paperless is now integrated with a
|
||||||
[task processing queue](/setup#task_processor) that tells you at a glance when and why something is not working.
|
[task processing queue](/setup#task_processor) that tells you at a glance when and why something is not working.
|
||||||
- The [changelog](/changelog) contains a detailed list of all changes in paperless-ngx.
|
- The [changelog](/changelog) contains a detailed list of all changes in paperless-ngx.
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
You can go multiple routes to setup and run Paperless:
|
You can go multiple routes to setup and run Paperless:
|
||||||
|
|
||||||
- [Use the easy install docker script](/setup#docker_script)
|
- [Use the easy install docker script](#docker_script)
|
||||||
- [Pull the image from Docker Hub](/setup#docker_hub)
|
- [Pull the image from Docker Hub](#docker_hub)
|
||||||
- [Build the Docker image yourself](/setup#docker_build)
|
- [Build the Docker image yourself](#docker_build)
|
||||||
- [Install Paperless directly on your system manually (bare metal)](/setup#bare_metal)
|
- [Install Paperless directly on your system manually (bare metal)](#bare_metal)
|
||||||
|
|
||||||
The Docker routes are quick & easy. These are the recommended routes.
|
The Docker routes are quick & easy. These are the recommended routes.
|
||||||
This configures all the stuff from the above automatically so that it
|
This configures all the stuff from the above automatically so that it
|
||||||
@@ -234,9 +234,8 @@ steps described in [Docker setup](#docker_hub) automatically.
|
|||||||
certain, more updated software. If you want to build these images
|
certain, more updated software. If you want to build these images
|
||||||
your self, that is possible, but beyond the scope of these steps.
|
your self, that is possible, but beyond the scope of these steps.
|
||||||
|
|
||||||
4. Follow steps 3 to 8 of [Docker Setup](setup#docker_hub)
|
4. Follow steps 3 to 8 of [Docker Setup](#docker_hub). When asked to run
|
||||||
role="ref"}. When asked to run `docker-compose pull` to pull the
|
`docker-compose pull` to pull the image, do
|
||||||
image, do
|
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
$ docker-compose build
|
$ docker-compose build
|
||||||
@@ -316,20 +315,21 @@ supported.
|
|||||||
enabled. This is usually the case, but not always.
|
enabled. This is usually the case, but not always.
|
||||||
|
|
||||||
4. Get the release archive from
|
4. Get the release archive from
|
||||||
<https://github.com/paperless-ngx/paperless-ngx/releases>. If you
|
<https://github.com/paperless-ngx/paperless-ngx/releases>. Extract the
|
||||||
clone the git repo as it is, you also have to compile the front end
|
archive to a place from where you wish to execute it, such as
|
||||||
by yourself. Extract the archive to a place from where you wish to
|
`/opt/paperless`. If you clone the git repo as it is, you also have to
|
||||||
execute it, such as `/opt/paperless`.
|
compile the frontend yourself, see [here](/development#front-end-development)
|
||||||
|
and use the `build` step, not `serve`.
|
||||||
|
|
||||||
5. Configure paperless. See [configuration](configuration) for details.
|
5. Configure paperless. See [configuration](/configuration) for details.
|
||||||
Edit the included `paperless.conf` and adjust the settings to your
|
Edit the included `paperless.conf` and adjust the settings to your
|
||||||
needs. Required settings for getting
|
needs. Required settings for getting
|
||||||
paperless running are:
|
paperless running are:
|
||||||
|
|
||||||
- `PAPERLESS_REDIS` should point to your redis server, such as
|
- `PAPERLESS_REDIS` should point to your redis server, such as
|
||||||
<redis://localhost:6379>.
|
<redis://localhost:6379>.
|
||||||
- `PAPERLESS_DBENGINE` optional, and should be one of [postgres,
|
- `PAPERLESS_DBENGINE` optional, and should be one of `postgres`,
|
||||||
mariadb, or sqlite]{.title-ref}
|
`mariadb`, or `sqlite`
|
||||||
- `PAPERLESS_DBHOST` should be the hostname on which your
|
- `PAPERLESS_DBHOST` should be the hostname on which your
|
||||||
PostgreSQL server is running. Do not configure this to use
|
PostgreSQL server is running. Do not configure this to use
|
||||||
SQLite instead. Also configure port, database name, user and
|
SQLite instead. Also configure port, database name, user and
|
||||||
@@ -344,7 +344,7 @@ supported.
|
|||||||
allows third parties to forge authentication credentials.
|
allows third parties to forge authentication credentials.
|
||||||
- `PAPERLESS_URL` if you are behind a reverse proxy. This should
|
- `PAPERLESS_URL` if you are behind a reverse proxy. This should
|
||||||
point to your domain. Please see
|
point to your domain. Please see
|
||||||
[configuration](configuration) for more
|
[configuration](/configuration) for more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
Many more adjustments can be made to paperless, especially the OCR
|
Many more adjustments can be made to paperless, especially the OCR
|
||||||
@@ -387,17 +387,17 @@ supported.
|
|||||||
9. Go to `/opt/paperless/src`, and execute the following commands:
|
9. Go to `/opt/paperless/src`, and execute the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
\# This creates the database schema.
|
# This creates the database schema.
|
||||||
sudo -Hu paperless python3 manage.py migrate
|
sudo -Hu paperless python3 manage.py migrate
|
||||||
|
|
||||||
\# This creates your first paperless user
|
# This creates your first paperless user
|
||||||
sudo -Hu paperless python3 manage.py createsuperuser
|
sudo -Hu paperless python3 manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
10. Optional: Test that paperless is working by executing
|
10. Optional: Test that paperless is working by executing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
\# This collects static files from paperless and django.
|
# This collects static files from paperless and django.
|
||||||
sudo -Hu paperless python3 manage.py runserver
|
sudo -Hu paperless python3 manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -480,7 +480,7 @@ supported.
|
|||||||
not available for most distributions.
|
not available for most distributions.
|
||||||
|
|
||||||
15. Optional: If using the NLTK machine learning processing (see
|
15. Optional: If using the NLTK machine learning processing (see
|
||||||
`PAPERLESS_ENABLE_NLTK` in [configuration](configuration#software_tweaks) for details),
|
`PAPERLESS_ENABLE_NLTK` in [configuration](/configuration#software_tweaks) for details),
|
||||||
download the NLTK data for the Snowball
|
download the NLTK data for the Snowball
|
||||||
Stemmer, Stopwords and Punkt tokenizer to your
|
Stemmer, Stopwords and Punkt tokenizer to your
|
||||||
`PAPERLESS_DATA_DIR/nltk`. Refer to the [NLTK
|
`PAPERLESS_DATA_DIR/nltk`. Refer to the [NLTK
|
||||||
@@ -562,7 +562,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
|||||||
the docker-compose files from
|
the docker-compose files from
|
||||||
[here](https://github.com/paperless-ngx/paperless-ngx/tree/master/docker/compose)
|
[here](https://github.com/paperless-ngx/paperless-ngx/tree/master/docker/compose)
|
||||||
or clone the repository to build the image yourself (see
|
or clone the repository to build the image yourself (see
|
||||||
[above](/setup#docker_build)). You can
|
[above](#docker_build)). You can
|
||||||
either replace your current paperless folder or put paperless-ngx in
|
either replace your current paperless folder or put paperless-ngx in
|
||||||
a different location.
|
a different location.
|
||||||
|
|
||||||
@@ -586,7 +586,7 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
|||||||
after you migrated your existing SQLite database.
|
after you migrated your existing SQLite database.
|
||||||
|
|
||||||
5. Adjust `docker-compose.yml` and `docker-compose.env` to your needs.
|
5. Adjust `docker-compose.yml` and `docker-compose.env` to your needs.
|
||||||
See [Docker setup](setup#docker_hub) details on
|
See [Docker setup](#docker_hub) details on
|
||||||
which edits are advised.
|
which edits are advised.
|
||||||
|
|
||||||
6. [Update paperless.](/administration#updating)
|
6. [Update paperless.](/administration#updating)
|
||||||
@@ -676,7 +676,7 @@ below use PostgreSQL, but are applicable to MySQL/MariaDB with the
|
|||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
MySQL is case insensitive by default, treating values like "Name" and
|
MySQL is case insensitive by default, treating values like "Name" and
|
||||||
"NAME" as identical. See [MySQL caveats](advanced##mysql-caveats) for details.
|
"NAME" as identical. See [MySQL caveats](/advanced_usage#mysql-caveats) for details.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
@@ -691,7 +691,7 @@ below use PostgreSQL, but are applicable to MySQL/MariaDB with the
|
|||||||
file to `docker-compose.yml`. Remember to adjust the consumption
|
file to `docker-compose.yml`. Remember to adjust the consumption
|
||||||
directory, if necessary.
|
directory, if necessary.
|
||||||
b) Without docker, configure the database in your `paperless.conf`
|
b) Without docker, configure the database in your `paperless.conf`
|
||||||
file. See [configuration](configuration) for
|
file. See [configuration](/configuration) for
|
||||||
details.
|
details.
|
||||||
|
|
||||||
3. Open a shell and initialize the database:
|
3. Open a shell and initialize the database:
|
||||||
@@ -766,7 +766,7 @@ After that, you need to clear your cookies (Paperless-ngx comes with
|
|||||||
updated dependencies that do cookie-processing differently) and probably
|
updated dependencies that do cookie-processing differently) and probably
|
||||||
your cache as well.
|
your cache as well.
|
||||||
|
|
||||||
# Considerations for less powerful devices {#less_powerful_devices}
|
# Considerations for less powerful devices {#less-powerful-devices}
|
||||||
|
|
||||||
Paperless runs on Raspberry Pi. However, some things are rather slow on
|
Paperless runs on Raspberry Pi. However, some things are rather slow on
|
||||||
the Pi and configuring some options in paperless can help improve
|
the Pi and configuring some options in paperless can help improve
|
||||||
@@ -797,12 +797,12 @@ performance immensely:
|
|||||||
more advanced language processing, which can take more memory and
|
more advanced language processing, which can take more memory and
|
||||||
processing time.
|
processing time.
|
||||||
|
|
||||||
For details, refer to [configuration](configuration).
|
For details, refer to [configuration](/configuration).
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
Updating the
|
Updating the
|
||||||
[automatic matching algorithm](/advanced_usage#automatic_matching) takes quite a bit of time. However, the update mechanism
|
[automatic matching algorithm](/advanced_usage#automatic-matching) takes quite a bit of time. However, the update mechanism
|
||||||
checks if your data has changed before doing the heavy lifting. If you
|
checks if your data has changed before doing the heavy lifting. If you
|
||||||
experience the algorithm taking too much cpu time, consider changing the
|
experience the algorithm taking too much cpu time, consider changing the
|
||||||
schedule in the admin interface to daily. You can also manually invoke
|
schedule in the admin interface to daily. You can also manually invoke
|
||||||
@@ -849,7 +849,7 @@ http {
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `PAPERLESS_URL` configuration variable is also required when using a
|
The `PAPERLESS_URL` configuration variable is also required when using a
|
||||||
reverse proxy. Please refer to the [hosting and security](configuration#hosting-and-security) docs.
|
reverse proxy. Please refer to the [hosting and security](/configuration#hosting-and-security) docs.
|
||||||
|
|
||||||
Also read
|
Also read
|
||||||
[this](https://channels.readthedocs.io/en/stable/deploying.html#nginx-supervisor-ubuntu),
|
[this](https://channels.readthedocs.io/en/stable/deploying.html#nginx-supervisor-ubuntu),
|
||||||
|
@@ -32,7 +32,7 @@ If you find the OCR accuracy to be too low, and/or the document consumer
|
|||||||
warns that
|
warns that
|
||||||
`OCR for XX failed, but we're going to stick with what we've got since FORGIVING_OCR is enabled`,
|
`OCR for XX failed, but we're going to stick with what we've got since FORGIVING_OCR is enabled`,
|
||||||
then you might need to install the [Tesseract language
|
then you might need to install the [Tesseract language
|
||||||
files](http://packages.ubuntu.com/search?keywords=tesseract-ocr)
|
files](https://packages.ubuntu.com/search?keywords=tesseract-ocr)
|
||||||
marching your document's languages.
|
marching your document's languages.
|
||||||
|
|
||||||
As an example, if you are running Paperless-ngx from any Ubuntu or
|
As an example, if you are running Paperless-ngx from any Ubuntu or
|
||||||
@@ -125,12 +125,12 @@ using docker-compose, this is achieved by the following configuration
|
|||||||
change in the `docker-compose.yml` file:
|
change in the `docker-compose.yml` file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
gotenberg:
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
image: gotenberg/gotenberg:7.6
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
restart: unless-stopped
|
command:
|
||||||
command:
|
|
||||||
- 'gotenberg'
|
- 'gotenberg'
|
||||||
- '--chromium-disable-routes=true'
|
- '--chromium-disable-javascript=true'
|
||||||
|
- '--chromium-allow-list=file:///tmp/.*'
|
||||||
- '--api-timeout=60'
|
- '--api-timeout=60'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ The following error occured while consuming document.pdf: [Errno 13] Permission
|
|||||||
This happens when paperless does not have permission to delete files
|
This happens when paperless does not have permission to delete files
|
||||||
inside the consumption directory. Ensure that `USERMAP_UID` and
|
inside the consumption directory. Ensure that `USERMAP_UID` and
|
||||||
`USERMAP_GID` are set to the user id and group id you use on the host
|
`USERMAP_GID` are set to the user id and group id you use on the host
|
||||||
operating system, if these are different from `1000`. See [Docker setup](setup#docker_hub).
|
operating system, if these are different from `1000`. See [Docker setup](/setup#docker_hub).
|
||||||
|
|
||||||
Also ensure that you are able to read and write to the consumption
|
Also ensure that you are able to read and write to the consumption
|
||||||
directory on the host.
|
directory on the host.
|
||||||
@@ -222,7 +222,7 @@ This might have multiple reasons.
|
|||||||
SENDFILE=0
|
SENDFILE=0
|
||||||
```
|
```
|
||||||
|
|
||||||
to your [docker-compose.env]{.title-ref} file.
|
to your `docker-compose.env` file.
|
||||||
|
|
||||||
## Error while reading metadata
|
## Error while reading metadata
|
||||||
|
|
||||||
@@ -326,9 +326,9 @@ unlock. This may have minor performance implications.
|
|||||||
## gunicorn fails to start with "is not a valid port number"
|
## gunicorn fails to start with "is not a valid port number"
|
||||||
|
|
||||||
You are likely running using Kubernetes, which automatically creates an
|
You are likely running using Kubernetes, which automatically creates an
|
||||||
environment variable named [\${serviceName}\_PORT]{.title-ref}. This is
|
environment variable named `${serviceName}_PORT`. This is
|
||||||
the same environment variable which is used by Paperless to optionally
|
the same environment variable which is used by Paperless to optionally
|
||||||
change the port gunicorn listens on.
|
change the port gunicorn listens on.
|
||||||
|
|
||||||
To fix this, set [PAPERLESS_PORT]{.title-ref} again to your desired
|
To fix this, set `PAPERLESS_PORT` again to your desired port, or the
|
||||||
port, or the default of 8000.
|
default of 8000.
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
# Usage Overview
|
# Usage Overview
|
||||||
|
|
||||||
Paperless is an application that manages your personal documents. With
|
Paperless is an application that manages your personal documents. With
|
||||||
the help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), paperless transforms your wieldy physical document binders
|
the help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)),
|
||||||
into a searchable archive and provides many utilities for finding and
|
paperless transforms your unwieldy physical document binders into a searchable archive
|
||||||
managing your documents.
|
and provides many utilities for finding and managing your documents.
|
||||||
|
|
||||||
## Terms and definitions
|
## Terms and definitions
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ Each document has a couple of fields that you can assign to them:
|
|||||||
date you signed a contract, or the date a letter was sent to you.
|
date you signed a contract, or the date a letter was sent to you.
|
||||||
- The _archive serial number_ (short: ASN) of a document is the
|
- The _archive serial number_ (short: ASN) of a document is the
|
||||||
identifier of the document in your physical document binders. See
|
identifier of the document in your physical document binders. See
|
||||||
[recommended workflow](#usage-reccomended_workflow) below.
|
[recommended workflow](#usage-recommended-workflow) below.
|
||||||
- The _content_ of a document is the text that was OCR'ed from the
|
- The _content_ of a document is the text that was OCR'ed from the
|
||||||
document. This text is fed into the search engine and is used for
|
document. This text is fed into the search engine and is used for
|
||||||
matching tags, correspondents and document types.
|
matching tags, correspondents and document types.
|
||||||
@@ -74,8 +74,8 @@ following operations on your documents:
|
|||||||
### The consumption directory
|
### The consumption directory
|
||||||
|
|
||||||
The primary method of getting documents into your database is by putting
|
The primary method of getting documents into your database is by putting
|
||||||
them in the consumption directory. The consumer runs in an infinite
|
them in the consumption directory. The consumer waits patiently, looking
|
||||||
loop, looking for new additions to this directory. When it finds them,
|
for new additions to this directory. When it finds them,
|
||||||
the consumer goes about the process of parsing them with the OCR,
|
the consumer goes about the process of parsing them with the OCR,
|
||||||
indexing what it finds, and storing it in the media directory.
|
indexing what it finds, and storing it in the media directory.
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ scanner to automatically push files to this directory, you'll need to
|
|||||||
setup some sort of service to accept the files from the scanner.
|
setup some sort of service to accept the files from the scanner.
|
||||||
Typically, you're looking at an FTP server like
|
Typically, you're looking at an FTP server like
|
||||||
[Proftpd](http://www.proftpd.org/) or a Windows folder share with
|
[Proftpd](http://www.proftpd.org/) or a Windows folder share with
|
||||||
[Samba](http://www.samba.org/).
|
[Samba](https://www.samba.org/).
|
||||||
|
|
||||||
### Web UI Upload
|
### Web UI Upload
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ dragging-and-dropping files into your browser window.
|
|||||||
|
|
||||||
### Mobile upload {#usage-mobile_upload}
|
### Mobile upload {#usage-mobile_upload}
|
||||||
|
|
||||||
The mobile app over at <https://github.com/qcasey/paperless_share>
|
The mobile app over at [https://github.com/qcasey/paperless_share](https://github.com/qcasey/paperless_share)
|
||||||
allows Android users to share any documents with paperless. This can be
|
allows Android users to share any documents with paperless. This can be
|
||||||
combined with any of the mobile scanning apps out there, such as Office
|
combined with any of the mobile scanning apps out there, such as Office
|
||||||
Lens.
|
Lens.
|
||||||
@@ -195,7 +195,7 @@ configured on the 'Scheduled tasks' page in the admin.
|
|||||||
|
|
||||||
### REST API
|
### REST API
|
||||||
|
|
||||||
You can also submit a document using the REST API, see [docs][api#file-uploads]
|
You can also submit a document using the REST API, see [POSTing documents](/api#file-uploads)
|
||||||
for details.
|
for details.
|
||||||
|
|
||||||
## Best practices {#basic-searching}
|
## Best practices {#basic-searching}
|
||||||
@@ -325,7 +325,7 @@ language](https://whoosh.readthedocs.io/en/latest/querylang.html). For
|
|||||||
details on what date parsing utilities are available, see [Date
|
details on what date parsing utilities are available, see [Date
|
||||||
parsing](https://whoosh.readthedocs.io/en/latest/dates.html#parsing-date-queries).
|
parsing](https://whoosh.readthedocs.io/en/latest/dates.html#parsing-date-queries).
|
||||||
|
|
||||||
## The recommended workflow {#usage-recommended_workflow}
|
## The recommended workflow {#usage-recommended-workflow}
|
||||||
|
|
||||||
Once you have familiarized yourself with paperless and are ready to use
|
Once you have familiarized yourself with paperless and are ready to use
|
||||||
it for all your documents, the recommended workflow for managing your
|
it for all your documents, the recommended workflow for managing your
|
||||||
@@ -398,7 +398,7 @@ Once you have scanned in a document, proceed in paperless as follows.
|
|||||||
paperless will assign them automatically. After consuming a couple
|
paperless will assign them automatically. After consuming a couple
|
||||||
documents, you can even ask paperless to *learn* when to assign tags and
|
documents, you can even ask paperless to *learn* when to assign tags and
|
||||||
correspondents by itself. For details on this feature, see
|
correspondents by itself. For details on this feature, see
|
||||||
[advanced matching](advanced_usage#matching).
|
[advanced matching](/advanced_usage#matching).
|
||||||
|
|
||||||
### Task management
|
### Task management
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ how regularly you intend to scan documents and use paperless.
|
|||||||
performed the task associated with the document, move it to the
|
performed the task associated with the document, move it to the
|
||||||
inbox.
|
inbox.
|
||||||
|
|
||||||
## Architectue
|
## Architecture
|
||||||
|
|
||||||
Paperless-ngx consists of the following components:
|
Paperless-ngx consists of the following components:
|
||||||
|
|
||||||
|
@@ -23,6 +23,7 @@ theme:
|
|||||||
- navigation.tabs
|
- navigation.tabs
|
||||||
- navigation.top
|
- navigation.top
|
||||||
- toc.integrate
|
- toc.integrate
|
||||||
|
- content.code.annotate
|
||||||
icon:
|
icon:
|
||||||
repo: fontawesome/brands/github
|
repo: fontawesome/brands/github
|
||||||
favicon: assets/favicon.png
|
favicon: assets/favicon.png
|
||||||
@@ -39,6 +40,8 @@ markdown_extensions:
|
|||||||
- pymdownx.highlight:
|
- pymdownx.highlight:
|
||||||
anchor_linenums: true
|
anchor_linenums: true
|
||||||
- pymdownx.superfences
|
- pymdownx.superfences
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
strict: true
|
||||||
nav:
|
nav:
|
||||||
- index.md
|
- index.md
|
||||||
- setup.md
|
- setup.md
|
||||||
|
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -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 gotenberg/gotenberg:7.6
|
docker run -p 3000:3000 -d gotenberg/gotenberg:7.6 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
|
||||||
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest
|
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest
|
||||||
|
51
src-ui/.eslintrc.json
Normal file
51
src-ui/.eslintrc.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"ignorePatterns": [
|
||||||
|
"projects/**/*"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": [
|
||||||
|
"tsconfig.json",
|
||||||
|
"e2e/tsconfig.json"
|
||||||
|
],
|
||||||
|
"createDefaultProgram": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:@angular-eslint/recommended",
|
||||||
|
"plugin:@angular-eslint/template/process-inline-templates"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@angular-eslint/directive-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "attribute",
|
||||||
|
"prefix": "app",
|
||||||
|
"style": "camelCase"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@angular-eslint/component-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"prefix": "app",
|
||||||
|
"style": "kebab-case"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.html"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@angular-eslint/template/recommended"
|
||||||
|
],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -53,7 +53,8 @@
|
|||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/apple-touch-icon.png",
|
"src/apple-touch-icon.png",
|
||||||
"src/assets",
|
"src/assets",
|
||||||
"src/manifest.webmanifest", {
|
"src/manifest.webmanifest",
|
||||||
|
{
|
||||||
"glob": "pdf.worker.min.js",
|
"glob": "pdf.worker.min.js",
|
||||||
"input": "node_modules/pdfjs-dist/build/",
|
"input": "node_modules/pdfjs-dist/build/",
|
||||||
"output": "/assets/js/"
|
"output": "/assets/js/"
|
||||||
@@ -103,7 +104,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"en-US": {
|
"en-US": {
|
||||||
"localize": ["en-US"]
|
"localize": [
|
||||||
|
"en-US"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": ""
|
"defaultConfiguration": ""
|
||||||
@@ -171,9 +174,23 @@
|
|||||||
"watch": true,
|
"watch": true,
|
||||||
"headless": false
|
"headless": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-eslint/builder:lint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "paperless-ui"
|
"defaultProject": "paperless-ui",
|
||||||
|
"cli": {
|
||||||
|
"schematicCollections": [
|
||||||
|
"@angular-eslint/schematics"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,16 @@ describe('settings', () => {
|
|||||||
req.reply(response)
|
req.reply(response)
|
||||||
}
|
}
|
||||||
).as('savedViews')
|
).as('savedViews')
|
||||||
|
|
||||||
|
cy.intercept('http://localhost:8000/api/mail_accounts/*', {
|
||||||
|
fixture: 'mail_accounts/mail_accounts.json',
|
||||||
|
})
|
||||||
|
cy.intercept('http://localhost:8000/api/mail_rules/*', {
|
||||||
|
fixture: 'mail_rules/mail_rules.json',
|
||||||
|
}).as('mailRules')
|
||||||
|
cy.intercept('http://localhost:8000/api/tasks/', {
|
||||||
|
fixture: 'tasks/tasks.json',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.fixture('documents/documents.json').then((documentsJson) => {
|
cy.fixture('documents/documents.json').then((documentsJson) => {
|
||||||
@@ -48,7 +58,6 @@ describe('settings', () => {
|
|||||||
|
|
||||||
cy.viewport(1024, 1600)
|
cy.viewport(1024, 1600)
|
||||||
cy.visit('/settings')
|
cy.visit('/settings')
|
||||||
cy.wait('@savedViews')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should activate / deactivate save button when settings change and are saved', () => {
|
it('should activate / deactivate save button when settings change and are saved', () => {
|
||||||
@@ -64,7 +73,7 @@ describe('settings', () => {
|
|||||||
cy.contains('a', 'Dashboard').click()
|
cy.contains('a', 'Dashboard').click()
|
||||||
cy.contains('You have unsaved changes')
|
cy.contains('You have unsaved changes')
|
||||||
cy.contains('button', 'Cancel').click()
|
cy.contains('button', 'Cancel').click()
|
||||||
cy.contains('button', 'Save').click().wait('@savedViews')
|
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||||
cy.contains('a', 'Dashboard').click()
|
cy.contains('a', 'Dashboard').click()
|
||||||
cy.contains('You have unsaved changes').should('not.exist')
|
cy.contains('You have unsaved changes').should('not.exist')
|
||||||
})
|
})
|
||||||
@@ -77,16 +86,16 @@ describe('settings', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should remove saved view from sidebar when unset', () => {
|
it('should remove saved view from sidebar when unset', () => {
|
||||||
cy.contains('a', 'Saved views').click()
|
cy.contains('a', 'Saved views').click().wait(2000)
|
||||||
cy.get('#show_in_sidebar_1').click()
|
cy.get('#show_in_sidebar_1').click()
|
||||||
cy.contains('button', 'Save').click().wait('@savedViews')
|
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||||
cy.contains('li', 'Inbox').should('not.exist')
|
cy.contains('li', 'Inbox').should('not.exist')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should remove saved view from dashboard when unset', () => {
|
it('should remove saved view from dashboard when unset', () => {
|
||||||
cy.contains('a', 'Saved views').click()
|
cy.contains('a', 'Saved views').click()
|
||||||
cy.get('#show_on_dashboard_1').click()
|
cy.get('#show_on_dashboard_1').click()
|
||||||
cy.contains('button', 'Save').click().wait('@savedViews')
|
cy.contains('button', 'Save').click().wait('@savedViews').wait(2000)
|
||||||
cy.visit('/dashboard')
|
cy.visit('/dashboard')
|
||||||
cy.get('app-saved-view-widget').contains('Inbox').should('not.exist')
|
cy.get('app-saved-view-widget').contains('Inbox').should('not.exist')
|
||||||
})
|
})
|
||||||
|
27
src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
Normal file
27
src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"count": 2,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "IMAP Server",
|
||||||
|
"imap_server": "imap.example.com",
|
||||||
|
"imap_port": 993,
|
||||||
|
"imap_security": 2,
|
||||||
|
"username": "inbox@example.com",
|
||||||
|
"password": "pass",
|
||||||
|
"character_set": "UTF-8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Gmail",
|
||||||
|
"imap_server": "imap.gmail.com",
|
||||||
|
"imap_port": 993,
|
||||||
|
"imap_security": 2,
|
||||||
|
"username": "user@gmail.com",
|
||||||
|
"password": "pass",
|
||||||
|
"character_set": "UTF-8"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
29
src-ui/cypress/fixtures/mail_rules/mail_rules.json
Normal file
29
src-ui/cypress/fixtures/mail_rules/mail_rules.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Gmail",
|
||||||
|
"account": 2,
|
||||||
|
"folder": "INBOX",
|
||||||
|
"filter_from": null,
|
||||||
|
"filter_subject": "[paperless]",
|
||||||
|
"filter_body": null,
|
||||||
|
"filter_attachment_filename": null,
|
||||||
|
"maximum_age": 30,
|
||||||
|
"action": 3,
|
||||||
|
"action_parameter": null,
|
||||||
|
"assign_title_from": 1,
|
||||||
|
"assign_tags": [
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"assign_correspondent_from": 1,
|
||||||
|
"assign_correspondent": 2,
|
||||||
|
"assign_document_type": null,
|
||||||
|
"order": 0,
|
||||||
|
"attachment_type": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -1 +1,44 @@
|
|||||||
{"count":3,"next":null,"previous":null,"results":[{"id":1,"name":"Inbox","show_on_dashboard":true,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"18"}]},{"id":2,"name":"Recently Added","show_on_dashboard":true,"show_in_sidebar":false,"sort_field":"created","sort_reverse":true,"filter_rules":[]},{"id":11,"name":"Taxes","show_on_dashboard":false,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"39"}]}]}
|
{
|
||||||
|
"count": 3,
|
||||||
|
"next": null,
|
||||||
|
"previous": null,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Inbox",
|
||||||
|
"show_on_dashboard": true,
|
||||||
|
"show_in_sidebar": true,
|
||||||
|
"sort_field": "created",
|
||||||
|
"sort_reverse": true,
|
||||||
|
"filter_rules": [
|
||||||
|
{
|
||||||
|
"rule_type": 6,
|
||||||
|
"value": "18"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Recently Added",
|
||||||
|
"show_on_dashboard": true,
|
||||||
|
"show_in_sidebar": false,
|
||||||
|
"sort_field": "created",
|
||||||
|
"sort_reverse": true,
|
||||||
|
"filter_rules": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"name": "Taxes",
|
||||||
|
"show_on_dashboard": false,
|
||||||
|
"show_in_sidebar": true,
|
||||||
|
"sort_field": "created",
|
||||||
|
"sort_reverse": true,
|
||||||
|
"filter_rules": [
|
||||||
|
{
|
||||||
|
"rule_type": 6,
|
||||||
|
"value": "39"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
1018
src-ui/messages.xlf
1018
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
3239
src-ui/package-lock.json
generated
3239
src-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -40,17 +40,23 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/jest": "14.1.0",
|
"@angular-builders/jest": "14.1.0",
|
||||||
"@angular-devkit/build-angular": "~14.2.7",
|
"@angular-devkit/build-angular": "~14.2.7",
|
||||||
|
"@angular-eslint/builder": "14.4.0",
|
||||||
|
"@angular-eslint/eslint-plugin": "14.4.0",
|
||||||
|
"@angular-eslint/eslint-plugin-template": "14.4.0",
|
||||||
|
"@angular-eslint/schematics": "14.4.0",
|
||||||
|
"@angular-eslint/template-parser": "14.4.0",
|
||||||
"@angular/cli": "~14.2.7",
|
"@angular/cli": "~14.2.7",
|
||||||
"@angular/compiler-cli": "~14.2.8",
|
"@angular/compiler-cli": "~14.2.8",
|
||||||
"@types/jest": "28.1.6",
|
"@types/jest": "28.1.6",
|
||||||
"@types/node": "^18.7.23",
|
"@types/node": "^18.7.23",
|
||||||
"codelyzer": "^6.0.2",
|
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||||
|
"@typescript-eslint/parser": "5.43.0",
|
||||||
"concurrently": "7.4.0",
|
"concurrently": "7.4.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
"jest": "28.1.3",
|
"jest": "28.1.3",
|
||||||
"jest-environment-jsdom": "^29.2.2",
|
"jest-environment-jsdom": "^29.2.2",
|
||||||
"jest-preset-angular": "^12.2.3",
|
"jest-preset-angular": "^12.2.3",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"tslint": "~6.1.3",
|
|
||||||
"typescript": "~4.8.4",
|
"typescript": "~4.8.4",
|
||||||
"wait-on": "~6.0.1"
|
"wait-on": "~6.0.1"
|
||||||
},
|
},
|
||||||
|
@@ -47,6 +47,11 @@ const routes: Routes = [
|
|||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
canDeactivate: [DirtyFormGuard],
|
canDeactivate: [DirtyFormGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'settings/:section',
|
||||||
|
component: SettingsComponent,
|
||||||
|
canDeactivate: [DirtyFormGuard],
|
||||||
|
},
|
||||||
{ path: 'tasks', component: TasksComponent },
|
{ path: 'tasks', component: TasksComponent },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -191,21 +191,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
anchorId: 'tour.settings',
|
anchorId: 'tour.settings',
|
||||||
content: $localize`Check out the settings for various tweaks to the web app or to toggle settings for saved views.`,
|
content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`,
|
||||||
route: '/settings',
|
route: '/settings',
|
||||||
enableBackdrop: true,
|
enableBackdrop: true,
|
||||||
prevBtnTitle,
|
prevBtnTitle,
|
||||||
nextBtnTitle,
|
nextBtnTitle,
|
||||||
endBtnTitle,
|
endBtnTitle,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
anchorId: 'tour.admin',
|
|
||||||
content: $localize`The Admin area contains more advanced controls as well as the settings for automatic e-mail fetching.`,
|
|
||||||
enableBackdrop: true,
|
|
||||||
prevBtnTitle,
|
|
||||||
nextBtnTitle,
|
|
||||||
endBtnTitle,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
anchorId: 'tour.outro',
|
anchorId: 'tour.outro',
|
||||||
title: $localize`Thank you! 🙏`,
|
title: $localize`Thank you! 🙏`,
|
||||||
|
@@ -24,7 +24,7 @@ import { CorrespondentEditDialogComponent } from './components/common/edit-dialo
|
|||||||
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
import { TagEditDialogComponent } from './components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from './components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { TagComponent } from './components/common/tag/tag.component'
|
import { TagComponent } from './components/common/tag/tag.component'
|
||||||
import { ClearableBadge } from './components/common/clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from './components/common/clearable-badge/clearable-badge.component'
|
||||||
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
|
import { PageHeaderComponent } from './components/common/page-header/page-header.component'
|
||||||
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
import { AppFrameComponent } from './components/app-frame/app-frame.component'
|
||||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||||
@@ -39,6 +39,7 @@ import { NgxFileDropModule } from 'ngx-file-drop'
|
|||||||
import { TextComponent } from './components/common/input/text/text.component'
|
import { TextComponent } from './components/common/input/text/text.component'
|
||||||
import { SelectComponent } from './components/common/input/select/select.component'
|
import { SelectComponent } from './components/common/input/select/select.component'
|
||||||
import { CheckComponent } from './components/common/input/check/check.component'
|
import { CheckComponent } from './components/common/input/check/check.component'
|
||||||
|
import { PasswordComponent } from './components/common/input/password/password.component'
|
||||||
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'
|
||||||
import { TagsComponent } from './components/common/input/tags/tags.component'
|
import { TagsComponent } from './components/common/input/tags/tags.component'
|
||||||
import { SortableDirective } from './directives/sortable.directive'
|
import { SortableDirective } from './directives/sortable.directive'
|
||||||
@@ -76,6 +77,8 @@ import { StoragePathEditDialogComponent } from './components/common/edit-dialog/
|
|||||||
import { SettingsService } from './services/settings.service'
|
import { SettingsService } from './services/settings.service'
|
||||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||||
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
|
import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
|
||||||
|
import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
|
||||||
|
|
||||||
import localeBe from '@angular/common/locales/be'
|
import localeBe from '@angular/common/locales/be'
|
||||||
import localeCs from '@angular/common/locales/cs'
|
import localeCs from '@angular/common/locales/cs'
|
||||||
@@ -143,7 +146,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
DocumentTypeEditDialogComponent,
|
DocumentTypeEditDialogComponent,
|
||||||
StoragePathEditDialogComponent,
|
StoragePathEditDialogComponent,
|
||||||
TagComponent,
|
TagComponent,
|
||||||
ClearableBadge,
|
ClearableBadgeComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
AppFrameComponent,
|
AppFrameComponent,
|
||||||
ToastsComponent,
|
ToastsComponent,
|
||||||
@@ -157,6 +160,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
TextComponent,
|
TextComponent,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
CheckComponent,
|
CheckComponent,
|
||||||
|
PasswordComponent,
|
||||||
SaveViewConfigDialogComponent,
|
SaveViewConfigDialogComponent,
|
||||||
TagsComponent,
|
TagsComponent,
|
||||||
SortableDirective,
|
SortableDirective,
|
||||||
@@ -180,6 +184,8 @@ function initializeApp(settings: SettingsService) {
|
|||||||
DocumentAsnComponent,
|
DocumentAsnComponent,
|
||||||
DocumentCommentsComponent,
|
DocumentCommentsComponent,
|
||||||
TasksComponent,
|
TasksComponent,
|
||||||
|
MailAccountEditDialogComponent,
|
||||||
|
MailRuleEditDialogComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@@ -174,13 +174,6 @@
|
|||||||
</svg><span> <ng-container i18n>Settings</ng-container></span>
|
</svg><span> <ng-container i18n>Settings</ng-container></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" tourAnchor="tour.admin">
|
|
||||||
<a class="nav-link" href="admin/" ngbPopover="Admin" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
|
||||||
<svg class="sidebaricon" fill="currentColor">
|
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#toggles"/>
|
|
||||||
</svg><span> <ng-container i18n>Admin</ng-container></span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted">
|
<h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted">
|
||||||
|
@@ -220,6 +220,12 @@ main {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.navbar-brand.slim {
|
||||||
|
max-width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown.show .dropdown-toggle,
|
.dropdown.show .dropdown-toggle,
|
||||||
.dropdown-toggle:hover {
|
.dropdown-toggle:hover {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
@@ -5,7 +5,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'
|
|||||||
templateUrl: './clearable-badge.component.html',
|
templateUrl: './clearable-badge.component.html',
|
||||||
styleUrls: ['./clearable-badge.component.scss'],
|
styleUrls: ['./clearable-badge.component.scss'],
|
||||||
})
|
})
|
||||||
export class ClearableBadge {
|
export class ClearableBadgeComponent {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
|
@@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
|||||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-correspondent-edit-dialog',
|
selector: 'app-correspondent-edit-dialog',
|
||||||
@@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service'
|
|||||||
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
styleUrls: ['./correspondent-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
|
||||||
constructor(
|
constructor(service: CorrespondentService, activeModal: NgbActiveModal) {
|
||||||
service: CorrespondentService,
|
super(service, activeModal)
|
||||||
activeModal: NgbActiveModal,
|
|
||||||
toastService: ToastService
|
|
||||||
) {
|
|
||||||
super(service, activeModal, toastService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
|
@@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
|||||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-document-type-edit-dialog',
|
selector: 'app-document-type-edit-dialog',
|
||||||
@@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service'
|
|||||||
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
styleUrls: ['./document-type-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
|
||||||
constructor(
|
constructor(service: DocumentTypeService, activeModal: NgbActiveModal) {
|
||||||
service: DocumentTypeService,
|
super(service, activeModal)
|
||||||
activeModal: NgbActiveModal,
|
|
||||||
toastService: ToastService
|
|
||||||
) {
|
|
||||||
super(service, activeModal, toastService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
|
@@ -2,11 +2,9 @@ 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 { map } from 'rxjs/operators'
|
|
||||||
import { MATCHING_ALGORITHMS, MATCH_AUTO } 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'
|
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class EditDialogComponent<T extends ObjectWithId>
|
export abstract class EditDialogComponent<T extends ObjectWithId>
|
||||||
@@ -14,8 +12,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
|||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
private service: AbstractPaperlessService<T>,
|
private service: AbstractPaperlessService<T>,
|
||||||
private activeModal: NgbActiveModal,
|
private activeModal: NgbActiveModal
|
||||||
private toastService: ToastService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
@@ -25,7 +22,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
|||||||
object: T
|
object: T
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
success = new EventEmitter()
|
succeeded = new EventEmitter()
|
||||||
|
|
||||||
networkActive = false
|
networkActive = false
|
||||||
|
|
||||||
@@ -95,16 +92,16 @@ export abstract class EditDialogComponent<T extends ObjectWithId>
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
this.networkActive = true
|
this.networkActive = true
|
||||||
serverResponse.subscribe(
|
serverResponse.subscribe({
|
||||||
(result) => {
|
next: (result) => {
|
||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
this.success.emit(result)
|
this.succeeded.emit(result)
|
||||||
},
|
},
|
||||||
(error) => {
|
error: (error) => {
|
||||||
this.error = error.error
|
this.error = error.error
|
||||||
this.networkActive = false
|
this.networkActive = false
|
||||||
}
|
},
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
|
@@ -0,0 +1,26 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></app-input-select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
|
||||||
|
<app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
|
||||||
|
<app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@@ -0,0 +1,50 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import {
|
||||||
|
IMAPSecurity,
|
||||||
|
PaperlessMailAccount,
|
||||||
|
} from 'src/app/data/paperless-mail-account'
|
||||||
|
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||||
|
|
||||||
|
const IMAP_SECURITY_OPTIONS = [
|
||||||
|
{ id: IMAPSecurity.None, name: $localize`No encryption` },
|
||||||
|
{ id: IMAPSecurity.SSL, name: $localize`SSL` },
|
||||||
|
{ id: IMAPSecurity.STARTTLS, name: $localize`STARTTLS` },
|
||||||
|
]
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-mail-account-edit-dialog',
|
||||||
|
templateUrl: './mail-account-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./mail-account-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> {
|
||||||
|
constructor(service: MailAccountService, activeModal: NgbActiveModal) {
|
||||||
|
super(service, activeModal)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new mail account`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit mail account`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(null),
|
||||||
|
imap_server: new FormControl(null),
|
||||||
|
imap_port: new FormControl(null),
|
||||||
|
imap_security: new FormControl(IMAPSecurity.SSL),
|
||||||
|
username: new FormControl(null),
|
||||||
|
password: new FormControl(null),
|
||||||
|
character_set: new FormControl('UTF-8'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get imapSecurityOptions() {
|
||||||
|
return IMAP_SECURITY_OPTIONS
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,39 @@
|
|||||||
|
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="Account" [items]="accounts" formControlName="account"></app-input-select>
|
||||||
|
<app-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></app-input-text>
|
||||||
|
<app-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></app-input-number>
|
||||||
|
<app-input-select i18n-title title="Attachment type" [items]="attachmentTypeOptions" formControlName="attachment_type"></app-input-select>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the filters specified below.</p>
|
||||||
|
<app-input-text i18n-title title="Filter from" formControlName="filter_from" [error]="error?.filter_from"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></app-input-text>
|
||||||
|
<app-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></app-input-text>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<app-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></app-input-select>
|
||||||
|
<app-input-text i18n-title title="Action parameter" *ngIf="showActionParamField" formControlName="action_parameter" [error]="error?.action_parameter"></app-input-text>
|
||||||
|
<app-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></app-input-select>
|
||||||
|
<app-input-tags [allowCreate]="false" formControlName="assign_tags"></app-input-tags>
|
||||||
|
<app-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></app-input-select>
|
||||||
|
<app-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></app-input-select>
|
||||||
|
<app-input-select *ngIf="showCorrespondentField" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></app-input-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@@ -0,0 +1,180 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { first } from 'rxjs'
|
||||||
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
|
import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account'
|
||||||
|
import {
|
||||||
|
MailAction,
|
||||||
|
MailFilterAttachmentType,
|
||||||
|
MailMetadataCorrespondentOption,
|
||||||
|
MailMetadataTitleOption,
|
||||||
|
PaperlessMailRule,
|
||||||
|
} from 'src/app/data/paperless-mail-rule'
|
||||||
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
|
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||||
|
import { MailRuleService } from 'src/app/services/rest/mail-rule.service'
|
||||||
|
|
||||||
|
const ATTACHMENT_TYPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailFilterAttachmentType.Attachments,
|
||||||
|
name: $localize`Only process attachments.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailFilterAttachmentType.Everything,
|
||||||
|
name: $localize`Process all files, including 'inline' attachments.`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const ACTION_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailAction.Delete,
|
||||||
|
name: $localize`Delete`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailAction.Move,
|
||||||
|
name: $localize`Move to specified folder`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailAction.MarkRead,
|
||||||
|
name: $localize`Mark as read, don't process read mails`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailAction.Flag,
|
||||||
|
name: $localize`Flag the mail, don't process flagged mails`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailAction.Tag,
|
||||||
|
name: $localize`Tag the mail with specified tag, don't process tagged mails`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const METADATA_TITLE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailMetadataTitleOption.FromSubject,
|
||||||
|
name: $localize`Use subject as title`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailMetadataTitleOption.FromFilename,
|
||||||
|
name: $localize`Use attachment filename as title`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const METADATA_CORRESPONDENT_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: MailMetadataCorrespondentOption.FromNothing,
|
||||||
|
name: $localize`Do not assign a correspondent`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailMetadataCorrespondentOption.FromEmail,
|
||||||
|
name: $localize`Use mail address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailMetadataCorrespondentOption.FromName,
|
||||||
|
name: $localize`Use name (or mail address if not available)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: MailMetadataCorrespondentOption.FromCustom,
|
||||||
|
name: $localize`Use correspondent selected below`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-mail-rule-edit-dialog',
|
||||||
|
templateUrl: './mail-rule-edit-dialog.component.html',
|
||||||
|
styleUrls: ['./mail-rule-edit-dialog.component.scss'],
|
||||||
|
})
|
||||||
|
export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMailRule> {
|
||||||
|
accounts: PaperlessMailAccount[]
|
||||||
|
correspondents: PaperlessCorrespondent[]
|
||||||
|
documentTypes: PaperlessDocumentType[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
service: MailRuleService,
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
accountService: MailAccountService,
|
||||||
|
correspondentService: CorrespondentService,
|
||||||
|
documentTypeService: DocumentTypeService
|
||||||
|
) {
|
||||||
|
super(service, activeModal)
|
||||||
|
|
||||||
|
accountService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.accounts = result.results))
|
||||||
|
|
||||||
|
correspondentService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.correspondents = result.results))
|
||||||
|
|
||||||
|
documentTypeService
|
||||||
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe((result) => (this.documentTypes = result.results))
|
||||||
|
}
|
||||||
|
|
||||||
|
getCreateTitle() {
|
||||||
|
return $localize`Create new mail rule`
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditTitle() {
|
||||||
|
return $localize`Edit mail rule`
|
||||||
|
}
|
||||||
|
|
||||||
|
getForm(): FormGroup {
|
||||||
|
return new FormGroup({
|
||||||
|
name: new FormControl(null),
|
||||||
|
account: new FormControl(null),
|
||||||
|
folder: new FormControl('INBOX'),
|
||||||
|
filter_from: new FormControl(null),
|
||||||
|
filter_subject: new FormControl(null),
|
||||||
|
filter_body: new FormControl(null),
|
||||||
|
filter_attachment_filename: new FormControl(null),
|
||||||
|
maximum_age: new FormControl(null),
|
||||||
|
attachment_type: new FormControl(MailFilterAttachmentType.Attachments),
|
||||||
|
action: new FormControl(MailAction.MarkRead),
|
||||||
|
action_parameter: new FormControl(null),
|
||||||
|
assign_title_from: new FormControl(MailMetadataTitleOption.FromSubject),
|
||||||
|
assign_tags: new FormControl([]),
|
||||||
|
assign_document_type: new FormControl(null),
|
||||||
|
assign_correspondent_from: new FormControl(
|
||||||
|
MailMetadataCorrespondentOption.FromNothing
|
||||||
|
),
|
||||||
|
assign_correspondent: new FormControl(null),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get showCorrespondentField(): boolean {
|
||||||
|
return (
|
||||||
|
this.objectForm?.get('assign_correspondent_from')?.value ==
|
||||||
|
MailMetadataCorrespondentOption.FromCustom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get showActionParamField(): boolean {
|
||||||
|
return (
|
||||||
|
this.objectForm?.get('action')?.value == MailAction.Move ||
|
||||||
|
this.objectForm?.get('action')?.value == MailAction.Tag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
get attachmentTypeOptions() {
|
||||||
|
return ATTACHMENT_TYPE_OPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
get actionOptions() {
|
||||||
|
return ACTION_OPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
get metadataTitleOptions() {
|
||||||
|
return METADATA_TITLE_OPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
get metadataCorrespondentOptions() {
|
||||||
|
return METADATA_CORRESPONDENT_OPTIONS
|
||||||
|
}
|
||||||
|
}
|
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<p *ngIf="this.dialogMode == 'edit'" i18n>
|
<p *ngIf="this.dialogMode === 'edit'" i18n>
|
||||||
<em>Note that editing a path does not apply changes to stored files until you have run the 'document_renamer' utility. See the <a target="_blank" href="https://docs.paperless-ngx.com/administration/#renamer">documentation</a>.</em>
|
<em>Note that editing a path does not apply changes to stored files until you have run the 'document_renamer' utility. See the <a target="_blank" href="https://docs.paperless-ngx.com/administration/#renamer">documentation</a>.</em>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-
|
|||||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-storage-path-edit-dialog',
|
selector: 'app-storage-path-edit-dialog',
|
||||||
@@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service'
|
|||||||
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
||||||
constructor(
|
constructor(service: StoragePathService, activeModal: NgbActiveModal) {
|
||||||
service: StoragePathService,
|
super(service, activeModal)
|
||||||
activeModal: NgbActiveModal,
|
|
||||||
toastService: ToastService
|
|
||||||
) {
|
|
||||||
super(service, activeModal, toastService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get pathHint() {
|
get pathHint() {
|
||||||
|
@@ -4,7 +4,6 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
|||||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
|
||||||
import { randomColor } from 'src/app/utils/color'
|
import { randomColor } from 'src/app/utils/color'
|
||||||
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
||||||
|
|
||||||
@@ -14,12 +13,8 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model'
|
|||||||
styleUrls: ['./tag-edit-dialog.component.scss'],
|
styleUrls: ['./tag-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
|
||||||
constructor(
|
constructor(service: TagService, activeModal: NgbActiveModal) {
|
||||||
service: TagService,
|
super(service, activeModal)
|
||||||
activeModal: NgbActiveModal,
|
|
||||||
toastService: ToastService
|
|
||||||
) {
|
|
||||||
super(service, activeModal, toastService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCreateTitle() {
|
getCreateTitle() {
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
<use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="d-none d-sm-inline"> {{title}}</div>
|
<div class="d-none d-sm-inline"> {{title}}</div>
|
||||||
<ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
|
<ng-container *ngIf="!editing && selectionModel.totalCount > 0">
|
||||||
<app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
<app-clearable-badge [number]="multiple ? selectionModel.totalCount : undefined" [selected]="!multiple && selectionModel.selectionSize() > 0" (cleared)="reset()"></app-clearable-badge>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -321,7 +321,7 @@ export class FilterableDropdownComponent {
|
|||||||
apply = new EventEmitter<ChangedItems>()
|
apply = new EventEmitter<ChangedItems>()
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
open = new EventEmitter()
|
opened = new EventEmitter()
|
||||||
|
|
||||||
get operatorToggleEnabled(): boolean {
|
get operatorToggleEnabled(): boolean {
|
||||||
return (
|
return (
|
||||||
@@ -356,7 +356,7 @@ export class FilterableDropdownComponent {
|
|||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
this.selectionModel.reset()
|
this.selectionModel.reset()
|
||||||
}
|
}
|
||||||
this.open.next(this)
|
this.opened.next(this)
|
||||||
} else {
|
} else {
|
||||||
this.filterText = ''
|
this.filterText = ''
|
||||||
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
if (this.applyOnClose && this.selectionModel.isDirty()) {
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||||
<div class="input-group" [class.is-invalid]="error">
|
<div class="input-group" [class.is-invalid]="error">
|
||||||
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error">
|
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error">
|
||||||
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
|
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{{error}}
|
{{error}}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, forwardRef } from '@angular/core'
|
import { Component, forwardRef, Input } from '@angular/core'
|
||||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type'
|
import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
@@ -17,6 +17,9 @@ import { AbstractInputComponent } from '../abstract-input'
|
|||||||
styleUrls: ['./number.component.scss'],
|
styleUrls: ['./number.component.scss'],
|
||||||
})
|
})
|
||||||
export class NumberComponent extends AbstractInputComponent<number> {
|
export class NumberComponent extends AbstractInputComponent<number> {
|
||||||
|
@Input()
|
||||||
|
showAdd: boolean = true
|
||||||
|
|
||||||
constructor(private documentService: DocumentService) {
|
constructor(private documentService: DocumentService) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||||
|
<input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
||||||
|
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
{{error}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, forwardRef } from '@angular/core'
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { AbstractInputComponent } from '../abstract-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => PasswordComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'app-input-password',
|
||||||
|
templateUrl: './password.component.html',
|
||||||
|
styleUrls: ['./password.component.scss'],
|
||||||
|
})
|
||||||
|
export class PasswordComponent extends AbstractInputComponent<string> {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@
|
|||||||
[closeOnSelect]="false"
|
[closeOnSelect]="false"
|
||||||
[clearSearchOnAdd]="true"
|
[clearSearchOnAdd]="true"
|
||||||
[hideSelected]="true"
|
[hideSelected]="true"
|
||||||
[addTag]="createTagRef"
|
[addTag]="allowCreate ? createTagRef : false"
|
||||||
addTagText="Add tag"
|
addTagText="Add tag"
|
||||||
i18n-addTagText
|
i18n-addTagText
|
||||||
(change)="onChange(value)"
|
(change)="onChange(value)"
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-select>
|
</ng-select>
|
||||||
|
|
||||||
<button class="btn btn-outline-secondary" type="button" (click)="createTag()">
|
<button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()">
|
||||||
<svg class="buttonicon" fill="currentColor">
|
<svg class="buttonicon" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@@ -54,6 +54,9 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
|||||||
@Input()
|
@Input()
|
||||||
suggestions: number[]
|
suggestions: number[]
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
allowCreate: boolean = true
|
||||||
|
|
||||||
value: number[]
|
value: number[]
|
||||||
|
|
||||||
tags: PaperlessTag[]
|
tags: PaperlessTag[]
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ import { ObjectWithId } from 'src/app/data/object-with-id'
|
|||||||
templateUrl: './select-dialog.component.html',
|
templateUrl: './select-dialog.component.html',
|
||||||
styleUrls: ['./select-dialog.component.scss'],
|
styleUrls: ['./select-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class SelectDialogComponent implements OnInit {
|
export class SelectDialogComponent {
|
||||||
constructor(public activeModal: NgbActiveModal) {}
|
constructor(public activeModal: NgbActiveModal) {}
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
@@ -24,8 +24,6 @@ export class SelectDialogComponent implements OnInit {
|
|||||||
|
|
||||||
selected: number
|
selected: number
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
|
|
||||||
cancelClicked() {
|
cancelClicked() {
|
||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -6,7 +6,7 @@ import { PaperlessTag } from 'src/app/data/paperless-tag'
|
|||||||
templateUrl: './tag.component.html',
|
templateUrl: './tag.component.html',
|
||||||
styleUrls: ['./tag.component.scss'],
|
styleUrls: ['./tag.component.scss'],
|
||||||
})
|
})
|
||||||
export class TagComponent implements OnInit {
|
export class TagComponent {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
@@ -17,6 +17,4 @@ export class TagComponent implements OnInit {
|
|||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
clickable: boolean = false
|
clickable: boolean = false
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
}
|
}
|
||||||
|
@@ -11,9 +11,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let doc of documents" (click)="openDocumentsService.openDocument(doc)">
|
<tr *ngFor="let doc of documents">
|
||||||
<td>{{doc.created_date | customDate}}</td>
|
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.created_date | customDate}}</a></td>
|
||||||
<td>{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t); $event.stopPropagation();"></app-tag></td>
|
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.title | documentTitle}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></app-tag></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -7,6 +7,6 @@ th:first-child {
|
|||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr {
|
tbody app-tag {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@@ -72,7 +72,9 @@ export class SavedViewWidgetComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clickTag(tag: PaperlessTag) {
|
clickTag(tag: PaperlessTag, event: MouseEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
this.list.quickFilter([
|
this.list.quickFilter([
|
||||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
{ rule_type: FILTER_HAS_TAGS_ALL, value: tag.id.toString() },
|
||||||
])
|
])
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<app-widget-frame title="Statistics" [loading]="loading" i18n-title>
|
<app-widget-frame title="Statistics" [loading]="loading" i18n-title>
|
||||||
<ng-container content>
|
<ng-container content>
|
||||||
<p class="card-text" i18n *ngIf="statistics?.documents_inbox != null">Documents in inbox: {{statistics?.documents_inbox}}</p>
|
<p class="card-text" i18n *ngIf="statistics?.documents_inbox !== null">Documents in inbox: {{statistics?.documents_inbox}}</p>
|
||||||
<p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p>
|
<p class="card-text" i18n>Total documents: {{statistics?.documents_total}}</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-widget-frame>
|
</app-widget-frame>
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { HttpEventType } from '@angular/common/http'
|
import { Component } from '@angular/core'
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { NgxFileDropEntry } from 'ngx-file-drop'
|
||||||
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'
|
|
||||||
import {
|
import {
|
||||||
ConsumerStatusService,
|
ConsumerStatusService,
|
||||||
FileStatus,
|
FileStatus,
|
||||||
@@ -15,7 +14,7 @@ const MAX_ALERTS = 5
|
|||||||
templateUrl: './upload-file-widget.component.html',
|
templateUrl: './upload-file-widget.component.html',
|
||||||
styleUrls: ['./upload-file-widget.component.scss'],
|
styleUrls: ['./upload-file-widget.component.scss'],
|
||||||
})
|
})
|
||||||
export class UploadFileWidgetComponent implements OnInit {
|
export class UploadFileWidgetComponent {
|
||||||
alertsExpanded = false
|
alertsExpanded = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -109,8 +108,6 @@ export class UploadFileWidgetComponent implements OnInit {
|
|||||||
this.consumerStatusService.dismissCompleted()
|
this.consumerStatusService.dismissCompleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
|
|
||||||
public fileOver(event) {}
|
public fileOver(event) {}
|
||||||
|
|
||||||
public fileLeave(event) {}
|
public fileLeave(event) {}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -6,8 +6,6 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
|||||||
templateUrl: './welcome-widget.component.html',
|
templateUrl: './welcome-widget.component.html',
|
||||||
styleUrls: ['./welcome-widget.component.scss'],
|
styleUrls: ['./welcome-widget.component.scss'],
|
||||||
})
|
})
|
||||||
export class WelcomeWidgetComponent implements OnInit {
|
export class WelcomeWidgetComponent {
|
||||||
constructor(public readonly tourService: TourService) {}
|
constructor(public readonly tourService: TourService) {}
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-widget-frame',
|
selector: 'app-widget-frame',
|
||||||
templateUrl: './widget-frame.component.html',
|
templateUrl: './widget-frame.component.html',
|
||||||
styleUrls: ['./widget-frame.component.scss'],
|
styleUrls: ['./widget-frame.component.scss'],
|
||||||
})
|
})
|
||||||
export class WidgetFrameComponent implements OnInit {
|
export class WidgetFrameComponent {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
@@ -13,6 +13,4 @@ export class WidgetFrameComponent implements OnInit {
|
|||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
loading: boolean = false
|
loading: boolean = false
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<app-page-header [(title)]="title">
|
<app-page-header [(title)]="title">
|
||||||
<div class="input-group input-group-sm me-5 d-none d-md-flex" *ngIf="getContentType() == 'application/pdf' && !useNativePdfViewer">
|
<div class="input-group input-group-sm me-5 d-none d-md-flex" *ngIf="getContentType() === 'application/pdf' && !useNativePdfViewer">
|
||||||
<div class="input-group-text" i18n>Page</div>
|
<div class="input-group-text" i18n>Page</div>
|
||||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
||||||
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||||
@@ -149,9 +149,9 @@
|
|||||||
|
|
||||||
<li [ngbNavItem]="4" class="d-md-none">
|
<li [ngbNavItem]="4" class="d-md-none">
|
||||||
<a ngbNavLink>Preview</a>
|
<a ngbNavLink>Preview</a>
|
||||||
<ng-template ngbNavContent *ngIf="pdfPreview.offsetParent == undefined">
|
<ng-template ngbNavContent *ngIf="pdfPreview.offsetParent === undefined">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<ng-container *ngIf="getContentType() == 'application/pdf'">
|
<ng-container *ngIf="getContentType() === 'application/pdf'">
|
||||||
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
|
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
|
||||||
<pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
<pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="getContentType() == 'text/plain'">
|
<ng-container *ngIf="getContentType() === 'text/plain'">
|
||||||
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
|
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div *ngIf="requiresPassword" class="password-prompt">
|
<div *ngIf="requiresPassword" class="password-prompt">
|
||||||
@@ -180,14 +180,14 @@
|
|||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || !(isDirty$ | async)">Discard</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive || (isDirty$ | async) === false">Discard</button>
|
||||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save & next</button>
|
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive || (isDirty$ | async) === false || error">Save & next</button>
|
||||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || !(isDirty$ | async) || error">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || (isDirty$ | async) === false || error">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
|
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
|
||||||
<ng-container *ngIf="getContentType() == 'application/pdf'">
|
<ng-container *ngIf="getContentType() === 'application/pdf'">
|
||||||
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
|
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
|
||||||
<pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
<pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="getContentType() == 'text/plain'">
|
<ng-container *ngIf="getContentType() === 'text/plain'">
|
||||||
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
|
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div *ngIf="requiresPassword" class="password-prompt">
|
<div *ngIf="requiresPassword" class="password-prompt">
|
||||||
|
@@ -184,7 +184,7 @@ export class DocumentDetailComponent
|
|||||||
this.openDocumentService.getOpenDocument(this.documentId)
|
this.openDocumentService.getOpenDocument(this.documentId)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.openDocumentService.openDocument(doc, false)
|
this.openDocumentService.openDocument(doc)
|
||||||
this.updateComponent(doc)
|
this.updateComponent(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-metadata-collapse',
|
selector: 'app-metadata-collapse',
|
||||||
templateUrl: './metadata-collapse.component.html',
|
templateUrl: './metadata-collapse.component.html',
|
||||||
styleUrls: ['./metadata-collapse.component.scss'],
|
styleUrls: ['./metadata-collapse.component.scss'],
|
||||||
})
|
})
|
||||||
export class MetadataCollapseComponent implements OnInit {
|
export class MetadataCollapseComponent {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
expand = false
|
expand = false
|
||||||
@@ -15,6 +15,4 @@ export class MetadataCollapseComponent implements OnInit {
|
|||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title = $localize`Metadata`
|
title = $localize`Metadata`
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
}
|
}
|
||||||
|
@@ -66,7 +66,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
|
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
|
||||||
<div class="btn-group btn-group-sm me-2">
|
<div class="btn-group btn-group-sm me-2">
|
||||||
|
|
||||||
<div ngbDropdown class="me-2 d-flex">
|
<div ngbDropdown class="me-2 d-flex">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||||
<svg class="toolbaricon" fill="currentColor">
|
<svg class="toolbaricon" fill="currentColor">
|
||||||
@@ -75,26 +74,57 @@
|
|||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||||
<button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected()" i18n>
|
|
||||||
Download
|
|
||||||
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
|
||||||
<span class="visually-hidden">Preparing download...</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected('originals')" i18n>
|
|
||||||
Download originals
|
|
||||||
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
|
||||||
<span class="visually-hidden">Preparing download...</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
|
<button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group btn-group-sm me-2">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
|
||||||
|
<svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-down" />
|
||||||
|
</svg>
|
||||||
|
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
|
||||||
|
<span class="visually-hidden">Preparing download...</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-none d-sm-inline"> <ng-container i18n>Download</ng-container></div>
|
||||||
|
</button>
|
||||||
|
<div ngbDropdown class="me-2 d-flex btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||||
|
<form [formGroup]="downloadForm" class="px-3 py-1">
|
||||||
|
<p class="mb-1" i18n>Include:</p>
|
||||||
|
<div class="form-group ps-3 mb-2">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
|
||||||
|
<label class="form-check-label" for="downloadFileType_archive" i18n>
|
||||||
|
Archived files
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
|
||||||
|
<label class="form-check-label" for="downloadFileType_originals" i18n>
|
||||||
|
Original files
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
|
||||||
|
<label class="form-check-label" for="downloadUseFormatting" i18n>
|
||||||
|
Use formatted filename
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group btn-group-sm me-2">
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||||
</svg> <ng-container i18n>Delete</ng-container>
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
.dropdown-toggle-split {
|
||||||
|
--bs-border-radius: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu{
|
||||||
|
--bs-dropdown-min-width: 12rem;
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||||
@@ -25,13 +25,15 @@ import { saveAs } from 'file-saver'
|
|||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
|
import { first, Subject, takeUntil } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bulk-editor',
|
selector: 'app-bulk-editor',
|
||||||
templateUrl: './bulk-editor.component.html',
|
templateUrl: './bulk-editor.component.html',
|
||||||
styleUrls: ['./bulk-editor.component.scss'],
|
styleUrls: ['./bulk-editor.component.scss'],
|
||||||
})
|
})
|
||||||
export class BulkEditorComponent {
|
export class BulkEditorComponent implements OnInit, OnDestroy {
|
||||||
tags: PaperlessTag[]
|
tags: PaperlessTag[]
|
||||||
correspondents: PaperlessCorrespondent[]
|
correspondents: PaperlessCorrespondent[]
|
||||||
documentTypes: PaperlessDocumentType[]
|
documentTypes: PaperlessDocumentType[]
|
||||||
@@ -43,6 +45,14 @@ export class BulkEditorComponent {
|
|||||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||||
awaitingDownload: boolean
|
awaitingDownload: boolean
|
||||||
|
|
||||||
|
unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
|
|
||||||
|
downloadForm = new FormGroup({
|
||||||
|
downloadFileTypeArchive: new FormControl(true),
|
||||||
|
downloadFileTypeOriginals: new FormControl(false),
|
||||||
|
downloadUseFormatting: new FormControl(false),
|
||||||
|
})
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private documentTypeService: DocumentTypeService,
|
private documentTypeService: DocumentTypeService,
|
||||||
private tagService: TagService,
|
private tagService: TagService,
|
||||||
@@ -66,16 +76,46 @@ export class BulkEditorComponent {
|
|||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.tagService
|
this.tagService
|
||||||
.listAll()
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
.subscribe((result) => (this.tags = result.results))
|
.subscribe((result) => (this.tags = result.results))
|
||||||
this.correspondentService
|
this.correspondentService
|
||||||
.listAll()
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
.subscribe((result) => (this.correspondents = result.results))
|
.subscribe((result) => (this.correspondents = result.results))
|
||||||
this.documentTypeService
|
this.documentTypeService
|
||||||
.listAll()
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
.subscribe((result) => (this.documentTypes = result.results))
|
.subscribe((result) => (this.documentTypes = result.results))
|
||||||
this.storagePathService
|
this.storagePathService
|
||||||
.listAll()
|
.listAll()
|
||||||
|
.pipe(first())
|
||||||
.subscribe((result) => (this.storagePaths = result.results))
|
.subscribe((result) => (this.storagePaths = result.results))
|
||||||
|
|
||||||
|
this.downloadForm
|
||||||
|
.get('downloadFileTypeArchive')
|
||||||
|
.valueChanges.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe((newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
this.downloadForm
|
||||||
|
.get('downloadFileTypeOriginals')
|
||||||
|
.patchValue(true, { emitEvent: false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.downloadForm
|
||||||
|
.get('downloadFileTypeOriginals')
|
||||||
|
.valueChanges.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe((newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
this.downloadForm
|
||||||
|
.get('downloadFileTypeArchive')
|
||||||
|
.patchValue(true, { emitEvent: false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.unsubscribeNotifier.next(this)
|
||||||
|
this.unsubscribeNotifier.complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
private executeBulkOperation(modal, method: string, args) {
|
private executeBulkOperation(modal, method: string, args) {
|
||||||
@@ -84,8 +124,9 @@ export class BulkEditorComponent {
|
|||||||
}
|
}
|
||||||
this.documentService
|
this.documentService
|
||||||
.bulkEdit(Array.from(this.list.selected), method, args)
|
.bulkEdit(Array.from(this.list.selected), method, args)
|
||||||
.subscribe(
|
.pipe(first())
|
||||||
(response) => {
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
this.list.reload()
|
this.list.reload()
|
||||||
this.list.reduceSelectionToFilter()
|
this.list.reduceSelectionToFilter()
|
||||||
this.list.selected.forEach((id) => {
|
this.list.selected.forEach((id) => {
|
||||||
@@ -95,7 +136,7 @@ export class BulkEditorComponent {
|
|||||||
modal.close()
|
modal.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
error: (error) => {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled = true
|
||||||
}
|
}
|
||||||
@@ -104,8 +145,8 @@ export class BulkEditorComponent {
|
|||||||
error.error
|
error.error
|
||||||
)}`
|
)}`
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private applySelectionData(
|
private applySelectionData(
|
||||||
@@ -126,6 +167,7 @@ export class BulkEditorComponent {
|
|||||||
openTagsDropdown() {
|
openTagsDropdown() {
|
||||||
this.documentService
|
this.documentService
|
||||||
.getSelectionData(Array.from(this.list.selected))
|
.getSelectionData(Array.from(this.list.selected))
|
||||||
|
.pipe(first())
|
||||||
.subscribe((s) => {
|
.subscribe((s) => {
|
||||||
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
|
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
|
||||||
})
|
})
|
||||||
@@ -134,6 +176,7 @@ export class BulkEditorComponent {
|
|||||||
openDocumentTypeDropdown() {
|
openDocumentTypeDropdown() {
|
||||||
this.documentService
|
this.documentService
|
||||||
.getSelectionData(Array.from(this.list.selected))
|
.getSelectionData(Array.from(this.list.selected))
|
||||||
|
.pipe(first())
|
||||||
.subscribe((s) => {
|
.subscribe((s) => {
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
s.selected_document_types,
|
s.selected_document_types,
|
||||||
@@ -145,6 +188,7 @@ export class BulkEditorComponent {
|
|||||||
openCorrespondentDropdown() {
|
openCorrespondentDropdown() {
|
||||||
this.documentService
|
this.documentService
|
||||||
.getSelectionData(Array.from(this.list.selected))
|
.getSelectionData(Array.from(this.list.selected))
|
||||||
|
.pipe(first())
|
||||||
.subscribe((s) => {
|
.subscribe((s) => {
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
s.selected_correspondents,
|
s.selected_correspondents,
|
||||||
@@ -156,6 +200,7 @@ export class BulkEditorComponent {
|
|||||||
openStoragePathDropdown() {
|
openStoragePathDropdown() {
|
||||||
this.documentService
|
this.documentService
|
||||||
.getSelectionData(Array.from(this.list.selected))
|
.getSelectionData(Array.from(this.list.selected))
|
||||||
|
.pipe(first())
|
||||||
.subscribe((s) => {
|
.subscribe((s) => {
|
||||||
this.applySelectionData(
|
this.applySelectionData(
|
||||||
s.selected_storage_paths,
|
s.selected_storage_paths,
|
||||||
@@ -232,7 +277,9 @@ export class BulkEditorComponent {
|
|||||||
|
|
||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
this.executeBulkOperation(modal, 'modify_tags', {
|
this.executeBulkOperation(modal, 'modify_tags', {
|
||||||
add_tags: changedTags.itemsToAdd.map((t) => t.id),
|
add_tags: changedTags.itemsToAdd.map((t) => t.id),
|
||||||
remove_tags: changedTags.itemsToRemove.map((t) => t.id),
|
remove_tags: changedTags.itemsToRemove.map((t) => t.id),
|
||||||
@@ -270,7 +317,9 @@ export class BulkEditorComponent {
|
|||||||
}
|
}
|
||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
this.executeBulkOperation(modal, 'set_correspondent', {
|
this.executeBulkOperation(modal, 'set_correspondent', {
|
||||||
correspondent: correspondent ? correspondent.id : null,
|
correspondent: correspondent ? correspondent.id : null,
|
||||||
})
|
})
|
||||||
@@ -306,7 +355,9 @@ export class BulkEditorComponent {
|
|||||||
}
|
}
|
||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
this.executeBulkOperation(modal, 'set_document_type', {
|
this.executeBulkOperation(modal, 'set_document_type', {
|
||||||
document_type: documentType ? documentType.id : null,
|
document_type: documentType ? documentType.id : null,
|
||||||
})
|
})
|
||||||
@@ -342,7 +393,9 @@ export class BulkEditorComponent {
|
|||||||
}
|
}
|
||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
this.executeBulkOperation(modal, 'set_storage_path', {
|
this.executeBulkOperation(modal, 'set_storage_path', {
|
||||||
storage_path: storagePath ? storagePath.id : null,
|
storage_path: storagePath ? storagePath.id : null,
|
||||||
})
|
})
|
||||||
@@ -364,16 +417,30 @@ export class BulkEditorComponent {
|
|||||||
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)`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled = false
|
||||||
this.executeBulkOperation(modal, 'delete', {})
|
this.executeBulkOperation(modal, 'delete', {})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadSelected(content = 'archive') {
|
downloadSelected() {
|
||||||
this.awaitingDownload = true
|
this.awaitingDownload = true
|
||||||
|
let downloadFileType: string =
|
||||||
|
this.downloadForm.get('downloadFileTypeArchive').value &&
|
||||||
|
this.downloadForm.get('downloadFileTypeOriginals').value
|
||||||
|
? 'both'
|
||||||
|
: this.downloadForm.get('downloadFileTypeArchive').value
|
||||||
|
? 'archive'
|
||||||
|
: 'originals'
|
||||||
this.documentService
|
this.documentService
|
||||||
.bulkDownload(Array.from(this.list.selected), content)
|
.bulkDownload(
|
||||||
|
Array.from(this.list.selected),
|
||||||
|
downloadFileType,
|
||||||
|
this.downloadForm.get('downloadUseFormatting').value
|
||||||
|
)
|
||||||
|
.pipe(first())
|
||||||
.subscribe((result: any) => {
|
.subscribe((result: any) => {
|
||||||
saveAs(result, 'documents.zip')
|
saveAs(result, 'documents.zip')
|
||||||
this.awaitingDownload = false
|
this.awaitingDownload = false
|
||||||
@@ -389,7 +456,9 @@ export class BulkEditorComponent {
|
|||||||
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`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled = false
|
||||||
this.executeBulkOperation(modal, 'redo_ocr', {})
|
this.executeBulkOperation(modal, 'redo_ocr', {})
|
||||||
})
|
})
|
||||||
|
@@ -37,7 +37,7 @@
|
|||||||
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
|
<use xlink:href="assets/bootstrap-icons.svg#diagram-3"/>
|
||||||
</svg> <span class="d-none d-md-inline" i18n>More like this</span>
|
</svg> <span class="d-none d-md-inline" i18n>More like this</span>
|
||||||
</a>
|
</a>
|
||||||
<a (click)="openDocumentsService.openDocument(document)" class="btn btn-sm btn-outline-secondary">
|
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
||||||
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
<svg class="sidebaricon" fill="currentColor" class="sidebaricon">
|
||||||
<use xlink:href="assets/bootstrap-icons.svg#pencil"/>
|
<use xlink:href="assets/bootstrap-icons.svg#pencil"/>
|
||||||
</svg> <span class="d-none d-md-inline" i18n>Edit</span>
|
</svg> <span class="d-none d-md-inline" i18n>Edit</span>
|
||||||
|
@@ -2,7 +2,6 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnInit,
|
|
||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
@@ -10,9 +9,6 @@ import { PaperlessDocument } from 'src/app/data/paperless-document'
|
|||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
|
||||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
|
||||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,11 +19,10 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
|||||||
'../popover-preview/popover-preview.scss',
|
'../popover-preview/popover-preview.scss',
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentCardLargeComponent implements OnInit {
|
export class DocumentCardLargeComponent {
|
||||||
constructor(
|
constructor(
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService
|
||||||
public openDocumentsService: OpenDocumentsService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
@@ -75,8 +70,6 @@ export class DocumentCardLargeComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
|
|
||||||
getIsThumbInverted() {
|
getIsThumbInverted() {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
|
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
|
||||||
}
|
}
|
||||||
@@ -119,6 +112,9 @@ export class DocumentCardLargeComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get contentTrimmed() {
|
get contentTrimmed() {
|
||||||
return this.document.content.substr(0, 500)
|
return (
|
||||||
|
this.document.content.substr(0, 500) +
|
||||||
|
(this.document.content.length > 500 ? '...' : '')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -67,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="btn-group w-100">
|
<div class="btn-group w-100">
|
||||||
<a (click)="openDocumentsService.openDocument(document)" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title>
|
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit" i18n-title>
|
||||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
@@ -2,7 +2,6 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnInit,
|
|
||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
@@ -11,7 +10,6 @@ import { PaperlessDocument } from 'src/app/data/paperless-document'
|
|||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -22,11 +20,10 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
|||||||
'../popover-preview/popover-preview.scss',
|
'../popover-preview/popover-preview.scss',
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentCardSmallComponent implements OnInit {
|
export class DocumentCardSmallComponent {
|
||||||
constructor(
|
constructor(
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService
|
||||||
public openDocumentsService: OpenDocumentsService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
@@ -57,8 +54,6 @@ export class DocumentCardSmallComponent implements OnInit {
|
|||||||
mouseOnPreview = false
|
mouseOnPreview = false
|
||||||
popoverHidden = true
|
popoverHidden = true
|
||||||
|
|
||||||
ngOnInit(): void {}
|
|
||||||
|
|
||||||
getIsThumbInverted() {
|
getIsThumbInverted() {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
|
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
|
||||||
}
|
}
|
||||||
|
@@ -53,7 +53,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)"
|
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSortField(f.field)"
|
||||||
[class.active]="list.sortField == f.field">{{f.name}}
|
[class.active]="list.sortField === f.field">{{f.name}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<span 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}}</span>
|
<span 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}}</span>
|
||||||
<ng-container *ngIf="!list.isReloading">
|
<ng-container *ngIf="!list.isReloading">
|
||||||
<span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span>
|
<span i18n *ngIf="list.selected.size === 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> <span i18n *ngIf="isFiltered">(filtered)</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</p>
|
</p>
|
||||||
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
<ngb-pagination *ngIf="list.collectionSize" [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
|
||||||
@@ -111,52 +111,52 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #documentListNoError>
|
<ng-template #documentListNoError>
|
||||||
<div *ngIf="displayMode == 'largeCards'">
|
<div *ngIf="displayMode === 'largeCards'">
|
||||||
<app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
<app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
||||||
</app-document-card-large>
|
</app-document-card-large>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-sm align-middle border shadow-sm" *ngIf="displayMode == 'details'">
|
<table class="table table-sm align-middle border shadow-sm" *ngIf="displayMode === 'details'">
|
||||||
<thead>
|
<thead>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="d-none d-lg-table-cell"
|
<th class="d-none d-lg-table-cell"
|
||||||
sortable="archive_serial_number"
|
appSortable="archive_serial_number"
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>ASN</th>
|
i18n>ASN</th>
|
||||||
<th class="d-none d-md-table-cell"
|
<th class="d-none d-md-table-cell"
|
||||||
sortable="correspondent__name"
|
appSortable="correspondent__name"
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Correspondent</th>
|
i18n>Correspondent</th>
|
||||||
<th
|
<th
|
||||||
sortable="title"
|
appSortable="title"
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Title</th>
|
i18n>Title</th>
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
sortable="document_type__name"
|
appSortable="document_type__name"
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Document type</th>
|
i18n>Document type</th>
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
sortable="storage_path__name"
|
appSortable="storage_path__name"
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Storage path</th>
|
i18n>Storage path</th>
|
||||||
<th
|
<th
|
||||||
sortable="created"
|
appSortable="created"
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
i18n>Created</th>
|
i18n>Created</th>
|
||||||
<th class="d-none d-xl-table-cell"
|
<th class="d-none d-xl-table-cell"
|
||||||
sortable="added"
|
appSortable="added"
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
[currentSortReverse]="list.sortReverse"
|
[currentSortReverse]="list.sortReverse"
|
||||||
(sort)="onSort($event)"
|
(sort)="onSort($event)"
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a (click)="openDocumentsService.openDocument(d)" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||||
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
|
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></app-tag>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-xl-table-cell">
|
<td class="d-none d-xl-table-cell">
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
<div class="row row-cols-paperless-cards" *ngIf="displayMode === 'smallCards'">
|
||||||
<app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small>
|
<app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="list.documents?.length > 15" class="mt-3">
|
<div *ngIf="list.documents?.length > 15" class="mt-3">
|
||||||
|
@@ -5,10 +5,10 @@
|
|||||||
<div ngbDropdown>
|
<div ngbDropdown>
|
||||||
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
|
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
|
||||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||||
<button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget == t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button>
|
<button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget === t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<select *ngIf="textFilterTarget == 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()">
|
<select *ngIf="textFilterTarget === 'asn'" class="form-select flex-grow-0 w-auto" [(ngModel)]="textFilterModifier" (change)="textFilterModifierChange()">
|
||||||
<option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option>
|
<option *ngFor="let m of textFilterModifiers" ngbDropdownItem [value]="m.id">{{m.label}}</option>
|
||||||
</select>
|
</select>
|
||||||
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
|
<button *ngIf="_textFilter" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0 z-10" (click)="resetTextField()">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget == 'fulltext-morelike'">
|
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,10 +16,10 @@
|
|||||||
<table class="table table-striped align-middle border shadow-sm">
|
<table class="table table-striped align-middle border shadow-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
<th scope="col" appSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||||
<th scope="col" class="d-none d-sm-table-cell" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
<th scope="col" class="d-none d-sm-table-cell" appSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||||
<th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
<th scope="col" appSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||||
<th scope="col" *ngFor="let column of extraColumns" sortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
<th scope="col" *ngFor="let column of extraColumns" appSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||||
<th scope="col" i18n>Actions</th>
|
<th scope="col" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@@ -120,8 +120,20 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
|||||||
backdrop: 'static',
|
backdrop: 'static',
|
||||||
})
|
})
|
||||||
activeModal.componentInstance.dialogMode = 'create'
|
activeModal.componentInstance.dialogMode = 'create'
|
||||||
activeModal.componentInstance.success.subscribe((o) => {
|
activeModal.componentInstance.success.subscribe({
|
||||||
|
next: () => {
|
||||||
this.reloadData()
|
this.reloadData()
|
||||||
|
this.toastService.showInfo(
|
||||||
|
$localize`Successfully created ${this.typeName}.`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.toastService.showInfo(
|
||||||
|
$localize`Error occurred while creating ${
|
||||||
|
this.typeName
|
||||||
|
} : ${e.toString()}.`
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,8 +143,20 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
|||||||
})
|
})
|
||||||
activeModal.componentInstance.object = object
|
activeModal.componentInstance.object = object
|
||||||
activeModal.componentInstance.dialogMode = 'edit'
|
activeModal.componentInstance.dialogMode = 'edit'
|
||||||
activeModal.componentInstance.success.subscribe((o) => {
|
activeModal.componentInstance.success.subscribe({
|
||||||
|
next: () => {
|
||||||
this.reloadData()
|
this.reloadData()
|
||||||
|
this.toastService.showInfo(
|
||||||
|
$localize`Successfully updated ${this.typeName}.`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.toastService.showInfo(
|
||||||
|
$localize`Error occurred while saving ${
|
||||||
|
this.typeName
|
||||||
|
} : ${e.toString()}.`
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,12 +1,17 @@
|
|||||||
<app-page-header title="Settings" i18n-title>
|
<app-page-header title="Settings" i18n-title>
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
||||||
|
<a class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
||||||
|
<ng-container i18n>Open Django Admin</ng-container>
|
||||||
|
<svg class="sidebaricon ms-1" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</app-page-header>
|
</app-page-header>
|
||||||
|
|
||||||
<!-- <p>items per page, documents per view type</p> -->
|
|
||||||
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
|
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
|
||||||
|
|
||||||
<ul ngbNav #nav="ngbNav" class="nav-tabs">
|
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-tabs">
|
||||||
<li [ngbNavItem]="1">
|
<li [ngbNavItem]="SettingsNavIDs.General">
|
||||||
<a ngbNavLink i18n>General</a>
|
<a ngbNavLink i18n>General</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
@@ -19,7 +24,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
||||||
<select class="form-select" formControlName="displayLanguage">
|
<select class="form-select" formControlName="displayLanguage">
|
||||||
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale != 'en-US'"> - {{lang.englishName}}</span></option>
|
<option *ngFor="let lang of displayLanguageOptions" [ngValue]="lang.code">{{lang.name}}<span *ngIf="lang.code && currentLocale !== 'en-US'"> - {{lang.englishName}}</span></option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<small *ngIf="displayLanguageIsDirty" class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
|
<small *ngIf="displayLanguageIsDirty" class="form-text text-primary" i18n>You need to reload the page after applying a new language.</small>
|
||||||
@@ -162,7 +167,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="2">
|
<li [ngbNavItem]="SettingsNavIDs.Notifications">
|
||||||
<a ngbNavLink i18n>Notifications</a>
|
<a ngbNavLink i18n>Notifications</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
@@ -180,7 +185,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li [ngbNavItem]="3">
|
<li [ngbNavItem]="SettingsNavIDs.SavedViews" (mouseover)="maybeInitializeTab(SettingsNavIDs.SavedViews)" (focusin)="maybeInitializeTab(SettingsNavIDs.SavedViews)">
|
||||||
<a ngbNavLink i18n>Saved views</a>
|
<a ngbNavLink i18n>Saved views</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
@@ -210,8 +215,97 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="savedViews.length == 0" i18n>No saved views defined.</div>
|
<div *ngIf="savedViews && savedViews.length === 0" i18n>No saved views defined.</div>
|
||||||
|
|
||||||
|
<div *ngIf="!savedViews">
|
||||||
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)">
|
||||||
|
<a ngbNavLink i18n>Mail</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
|
||||||
|
<ng-container *ngIf="mailAccounts && mailRules">
|
||||||
|
<h4>
|
||||||
|
<ng-container i18n>Mail accounts</ng-container>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()">
|
||||||
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
|
</svg>
|
||||||
|
<ng-container i18n>Add Account</ng-container>
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
<ul class="list-group" formGroupName="mailAccounts">
|
||||||
|
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" i18n>Name</div>
|
||||||
|
<div class="col" i18n>Server</div>
|
||||||
|
<div class="col" i18n>Actions</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div>
|
||||||
|
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<div *ngIf="mailAccounts.length === 0" i18n>No mail accounts defined.</div>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4 class="mt-4">
|
||||||
|
<ng-container i18n>Mail rules</ng-container>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()">
|
||||||
|
<svg class="sidebaricon me-1" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||||
|
</svg>
|
||||||
|
<ng-container i18n>Add Rule</ng-container>
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
<ul class="list-group" formGroupName="mailRules">
|
||||||
|
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" i18n>Name</div>
|
||||||
|
<div class="col" i18n>Account</div>
|
||||||
|
<div class="col" i18n>Actions</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div>
|
||||||
|
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<div *ngIf="mailRules.length === 0" i18n>No mail rules defined.</div>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div *ngIf="!mailAccounts || !mailRules">
|
||||||
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -220,5 +314,5 @@
|
|||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary mb-2" [disabled]="!(isDirty$ | async)" i18n>Save</button>
|
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
</form>
|
</form>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user