mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-29 02:55:43 +00:00
Compare commits
290
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5561ba06f | ||
|
|
1692c916f8 | ||
|
|
ffd886eae0 | ||
|
|
12c0dc635e | ||
|
|
1e01ce42c0 | ||
|
|
e46f4a5aaa | ||
|
|
1ba6c31385 | ||
|
|
f3ee820fa4 | ||
|
|
2f5bcdf66e | ||
|
|
501cdd92d2 | ||
|
|
66c5c46913 | ||
|
|
782634d912 | ||
|
|
3cfe9fa2a8 | ||
|
|
784fed447f | ||
|
|
3292a0e7cc | ||
|
|
9646b8c67d | ||
|
|
e590d7df69 | ||
|
|
cc71aad058 | ||
|
|
3cbdf5d0b7 | ||
|
|
f84e0097e5 | ||
|
|
7dbf8bdd4a | ||
|
|
d2a752a196 | ||
|
|
2cb155e717 | ||
|
|
9e9fc6213c | ||
|
|
0f7c02de5e | ||
|
|
95dea787f2 | ||
|
|
b6501b0c47 | ||
|
|
87ebd13abc | ||
|
|
a86c9d32fe | ||
|
|
7942edfdf4 | ||
|
|
1e00ad5f30 | ||
|
|
5f26c01c6f | ||
|
|
92e133eeb0 | ||
|
|
06b2d5102c | ||
|
|
40255cfdbb | ||
|
|
d919c341b1 | ||
|
|
ba0a80a8ad | ||
|
|
60319c6d37 | ||
|
|
1e8622494d | ||
|
|
d6a316b1df | ||
|
|
8f311c4b6b | ||
|
|
f25322600d | ||
|
|
615f27e6fb | ||
|
|
5b809122b5 | ||
|
|
8b8307571a | ||
|
|
0e97419e0e | ||
|
|
7ff51452f0 | ||
|
|
a6c974589f | ||
|
|
47f9f642a9 | ||
|
|
8bfebc3b9b | ||
|
|
c7f83212a3 | ||
|
|
b010f65ae7 | ||
|
|
89d3a53603 | ||
|
|
9601b3d597 | ||
|
|
13e07844fe | ||
|
|
be82fcb70a | ||
|
|
98298e37cd | ||
|
|
35be0850ec | ||
|
|
1bb4b9b473 | ||
|
|
f85094dc2b | ||
|
|
65ca78e9e7 | ||
|
|
5c1bbcd06d | ||
|
|
bc734798e3 | ||
|
|
5ecbfc9df7 | ||
|
|
e63b62d531 | ||
|
|
dd3ec83569 | ||
|
|
7a23356898 | ||
|
|
afaf39e43a | ||
|
|
0dc58cf686 | ||
|
|
505ff31748 | ||
|
|
3c51b3f9cd | ||
|
|
5b45b89d35 | ||
|
|
1188a89369 | ||
|
|
5b9bb147cf | ||
|
|
c278f52fb2 | ||
|
|
d27a5f688a | ||
|
|
c5bb5b237b | ||
|
|
11a5714cba | ||
|
|
4363567fa7 | ||
|
|
3e41d99a82 | ||
|
|
5cc3c087d9 | ||
|
|
c8c4c7c749 | ||
|
|
836c81e037 | ||
|
|
e4b861d76f | ||
|
|
6913f9d79c | ||
|
|
891f4a2faf | ||
|
|
2312314aa7 | ||
|
|
72e8b73108 | ||
|
|
5c9ff367e3 | ||
|
|
94f6b8d36d | ||
|
|
32d04e1fd3 | ||
|
|
56c744fd56 | ||
|
|
1f432a3378 | ||
|
|
d1aa76e4ce | ||
|
|
5381bc5907 | ||
|
|
16cc704539 | ||
|
|
245d9fb4a1 | ||
|
|
771f3f150a | ||
|
|
ecfeff5054 | ||
|
|
37477d391e | ||
|
|
2f1cd31e31 | ||
|
|
742c136773 | ||
|
|
71ecdc528e | ||
|
|
00ec8a577b | ||
|
|
3618c50b62 | ||
|
|
6f4497185e | ||
|
|
e816269db5 | ||
|
|
d4e60e13bf | ||
|
|
cb091665e2 | ||
|
|
00bb92e3e1 | ||
|
|
11ec676909 | ||
|
|
7c457466b7 | ||
|
|
65aed2405c | ||
|
|
985dc9be31 | ||
|
|
305d764805 | ||
|
|
eca2ba3657 | ||
|
|
220c70b27d | ||
|
|
ccaebabe0a | ||
|
|
598540fda0 | ||
|
|
b1a75b0166 | ||
|
|
bf38ae98f1 | ||
|
|
84c59f45da | ||
|
|
d8397ac77e | ||
|
|
7c0a13b339 | ||
|
|
7c8db78a62 | ||
|
|
04f81bf17d | ||
|
|
f96a29db5d | ||
|
|
5af3039d62 | ||
|
|
078cba4bd1 | ||
|
|
43e29598b3 | ||
|
|
d9a596d67a | ||
|
|
a1026f03db | ||
|
|
6c8a9b0373 | ||
|
|
7130c0bd06 | ||
|
|
d391fdec64 | ||
|
|
4d7aa8e1a2 | ||
|
|
9bdbfd362f | ||
|
|
9ba1d93e15 | ||
|
|
a9c73e2846 | ||
|
|
332136df8b | ||
|
|
3a1d33225e | ||
|
|
e770ff572e | ||
|
|
402f2ead59 | ||
|
|
3b4d958b97 | ||
|
|
3f81b432ec | ||
|
|
66d363bdc5 | ||
|
|
c845cf0a19 | ||
|
|
317f239d09 | ||
|
|
128c3539d5 | ||
|
|
26975868a0 | ||
|
|
f3fc3febf1 | ||
|
|
8efc998687 | ||
|
|
3f47900f06 | ||
|
|
963a519e5c | ||
|
|
59e5d15cf0 | ||
|
|
ef2f65fcb8 | ||
|
|
555ba8bb19 | ||
|
|
01992bb5c6 | ||
|
|
21032ac008 | ||
|
|
b63e095a60 | ||
|
|
ce642409e8 | ||
|
|
2e5bd02e7e | ||
|
|
7032da53c5 | ||
|
|
6f3451bce0 | ||
|
|
8c5b5cd77b | ||
|
|
919c54c6ba | ||
|
|
4632ad3a36 | ||
|
|
0c43b50f01 | ||
|
|
67d079fe14 | ||
|
|
ca674e5a02 | ||
|
|
71e08a1e98 | ||
|
|
1e61a6cd6a | ||
|
|
a76731ca89 | ||
|
|
ffc56bddda | ||
|
|
4c2cc373f2 | ||
|
|
76bb6d3422 | ||
|
|
85a2a0a416 | ||
|
|
5036aa1ea3 | ||
|
|
f7da273ab7 | ||
|
|
93338a0a82 | ||
|
|
a96db50b0a | ||
|
|
c5e80a7e4f | ||
|
|
bc622d67fc | ||
|
|
4a8d3c858c | ||
|
|
8c335321cd | ||
|
|
27966858fd | ||
|
|
d3bfb186e0 | ||
|
|
cf5ac596ed | ||
|
|
25b5e8fede | ||
|
|
80be6793cf | ||
|
|
7b175ec1b3 | ||
|
|
36d45ecf4d | ||
|
|
4bf681387a | ||
|
|
c05d75dab0 | ||
|
|
7a50157164 | ||
|
|
a93d83119e | ||
|
|
addaf92a61 | ||
|
|
8c7fa4e165 | ||
|
|
22a47a28dc | ||
|
|
20d921142e | ||
|
|
1ed8f1d086 | ||
|
|
46853e10dc | ||
|
|
c31c244b54 | ||
|
|
56493d6640 | ||
|
|
f7f94762b6 | ||
|
|
beb5fe2232 | ||
|
|
c924213f32 | ||
|
|
b053b35332 | ||
|
|
a45692aa0f | ||
|
|
c3ac102eba | ||
|
|
0e5ab7f3e0 | ||
|
|
533b64cb70 | ||
|
|
b3d6359afc | ||
|
|
b6e3827ab1 | ||
|
|
7cd802cf48 | ||
|
|
7470b799a3 | ||
|
|
b5df90156e | ||
|
|
733d2e19a0 | ||
|
|
fe7419484b | ||
|
|
0d827e8511 | ||
|
|
69514d8d70 | ||
|
|
dd6f7fad32 | ||
|
|
c5ad148dc7 | ||
|
|
b12f1e757c | ||
|
|
0cbab1ae80 | ||
|
|
0219df5b67 | ||
|
|
005ef4fce6 | ||
|
|
44f0191bfb | ||
|
|
e9f846ca24 | ||
|
|
2049497b76 | ||
|
|
2a9d1fce0d | ||
|
|
808c074f48 | ||
|
|
7927e5c436 | ||
|
|
cac48c9855 | ||
|
|
3fda648f37 | ||
|
|
95736eebc4 | ||
|
|
85027dbffd | ||
|
|
74f72e417d | ||
|
|
fe3c424d7d | ||
|
|
a0172a2754 | ||
|
|
810bf3d612 | ||
|
|
846cc47565 | ||
|
|
1d396d9160 | ||
|
|
2a4e8f9acd | ||
|
|
a9dfe8f3f7 | ||
|
|
906e841ded | ||
|
|
6684e80ffc | ||
|
|
3dc7cf3da1 | ||
|
|
819f606335 | ||
|
|
ad45e3f747 | ||
|
|
74b10db028 | ||
|
|
cffb9c34f0 | ||
|
|
6f52614817 | ||
|
|
a0d3527d20 | ||
|
|
4e64ca7ca6 | ||
|
|
e9511bd3da | ||
|
|
8b9ca75a90 | ||
|
|
9f0a4ac19d | ||
|
|
8f969ecab5 | ||
|
|
245e52a4eb | ||
|
|
a8c75d95d8 | ||
|
|
d6e2456baf | ||
|
|
3b75d3271e | ||
|
|
e88816d141 | ||
|
|
e5bd4713ac | ||
|
|
b9aced07fb | ||
|
|
6b55740f56 | ||
|
|
9aee063347 | ||
|
|
7fe411bb1a | ||
|
|
34b5f4c565 | ||
|
|
3808a4e14a | ||
|
|
3bd4135aba | ||
|
|
b60fb8ed82 | ||
|
|
3f32ed319a | ||
|
|
03e6d58f86 | ||
|
|
c197487374 | ||
|
|
d718d7d29f | ||
|
|
ce112cda0e | ||
|
|
d904aaef60 | ||
|
|
35bc673648 | ||
|
|
d0bd111eab | ||
|
|
cd81f750b4 | ||
|
|
48d21da13b | ||
|
|
701aafce06 | ||
|
|
cbe8bc35d6 | ||
|
|
1c4fa7237c | ||
|
|
63dab0ab09 | ||
|
|
276dc31abe | ||
|
|
a11a2ec13f | ||
|
|
df9136e7d4 |
No files matched your search
+10
-30
@@ -1,6 +1,6 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim as main-app
|
FROM --platform=$BUILDPLATFORM docker.io/node:20-trixie-slim as main-app
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
@@ -8,16 +8,17 @@ ARG DEBIAN_FRONTEND=noninteractive
|
|||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
# Can be workflow provided, defaults set for manual building
|
# Can be workflow provided, defaults set for manual building
|
||||||
ARG JBIG2ENC_VERSION=0.29
|
ARG JBIG2ENC_VERSION=0.30
|
||||||
ARG QPDF_VERSION=11.9.0
|
|
||||||
ARG GS_VERSION=10.03.1
|
|
||||||
|
|
||||||
# Set Python environment variables
|
# Set Python environment variables
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
# Ignore warning from Whitenoise
|
# Ignore warning from Whitenoise
|
||||||
PYTHONWARNINGS="ignore:::django.http.response:517" \
|
PYTHONWARNINGS="ignore:::django.http.response:517" \
|
||||||
PNGX_CONTAINERIZED=1
|
PNGX_CONTAINERIZED=1 \
|
||||||
|
# https://docs.astral.sh/uv/reference/settings/#link-mode
|
||||||
|
UV_LINK_MODE=copy \
|
||||||
|
UV_CACHE_DIR=/cache/uv/
|
||||||
|
|
||||||
#
|
#
|
||||||
# Begin installation and configuration
|
# Begin installation and configuration
|
||||||
@@ -83,37 +84,15 @@ RUN set -eux \
|
|||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install --yes --quiet ${PYTHON_PACKAGES}
|
&& apt-get install --yes --quiet ${PYTHON_PACKAGES}
|
||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.7.8 /uv /bin/uv
|
COPY --from=ghcr.io/astral-sh/uv:0.9.10 /uv /bin/uv
|
||||||
|
|
||||||
|
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& echo "Installing pre-built updates" \
|
&& echo "Installing pre-built updates" \
|
||||||
&& echo "Installing qpdf ${QPDF_VERSION}" \
|
|
||||||
&& curl --fail --silent --show-error --location \
|
|
||||||
--output libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
&& curl --fail --silent --show-error --location \
|
|
||||||
--output qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
&& echo "Installing Ghostscript ${GS_VERSION}" \
|
|
||||||
&& curl --fail --silent --show-error --location \
|
|
||||||
--output libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
&& curl --fail --silent --show-error --location \
|
|
||||||
--output ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
&& curl --fail --silent --show-error --location \
|
|
||||||
--output libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
|
||||||
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
|
||||||
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
&& echo "Installing jbig2enc" \
|
&& echo "Installing jbig2enc" \
|
||||||
&& curl --fail --silent --show-error --location \
|
&& curl --fail --silent --show-error --location \
|
||||||
--output jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
--output jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||||
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-trixie-v${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||||
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb
|
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb
|
||||||
|
|
||||||
# setup docker-specific things
|
# setup docker-specific things
|
||||||
@@ -127,6 +106,7 @@ COPY [ \
|
|||||||
|
|
||||||
RUN set -eux \
|
RUN set -eux \
|
||||||
&& echo "Configuring ImageMagick" \
|
&& echo "Configuring ImageMagick" \
|
||||||
|
&& mkdir -p /etc/ImageMagick-6 \
|
||||||
&& mv paperless-policy.xml /etc/ImageMagick-6/policy.xml
|
&& mv paperless-policy.xml /etc/ImageMagick-6/policy.xml
|
||||||
|
|
||||||
|
|
||||||
@@ -142,7 +122,7 @@ ARG BUILD_PACKAGES="\
|
|||||||
pkg-config"
|
pkg-config"
|
||||||
|
|
||||||
# hadolint ignore=DL3042
|
# hadolint ignore=DL3042
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv,id=pip-cache \
|
RUN --mount=type=cache,target=/cache/uv/,id=uv-cache \
|
||||||
set -eux \
|
set -eux \
|
||||||
&& echo "Installing build system packages" \
|
&& echo "Installing build system packages" \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
|
|||||||
@@ -116,9 +116,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Maintenance: Build Documentation",
|
"label": "Maintenance: Build Documentation",
|
||||||
"description": "Build the documentation with MkDocs",
|
"description": "Build the documentation with Zensical",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "uv run mkdocs build --config-file mkdocs.yml && uv run mkdocs serve",
|
"command": "uv run zensical build && uv run zensical serve",
|
||||||
"group": "none",
|
"group": "none",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"echo": true,
|
"echo": true,
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ end_of_line = lf
|
|||||||
charset = utf-8
|
charset = utf-8
|
||||||
max_line_length = 79
|
max_line_length = 79
|
||||||
|
|
||||||
|
[*.sh]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 1
|
||||||
|
|
||||||
[{*.html,*.css,*.js}]
|
[{*.html,*.css,*.js}]
|
||||||
max_line_length = off
|
max_line_length = off
|
||||||
|
|
||||||
|
|||||||
@@ -51,5 +51,5 @@ body:
|
|||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Relevant logs or output
|
label: Relevant logs or output
|
||||||
description: If you have logs, errors that might help, paste it here.
|
description: If you have logs, errors that might help, paste it here. For example other containers or services (database, redis, etc).
|
||||||
render: bash
|
render: bash
|
||||||
@@ -6,8 +6,8 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
### ⚠️ Please remember: issues are for *bugs*
|
### ⚠️ Please remember: issues are for *bugs* only! ⚠️
|
||||||
That is, something you believe affects every single user of Paperless-ngx, not just you. If you're not sure, start with one of the other options below.
|
That is, something you believe affects every single user of Paperless-ngx (and the demo, for example), not just you. If you are not sure, start with one of the other options below.
|
||||||
|
|
||||||
Also, note that **Paperless-ngx does not perform OCR or archive file creation itself**, those are handled by other tools. Problems with OCR or archive versions of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues
|
Also, note that **Paperless-ngx does not perform OCR or archive file creation itself**, those are handled by other tools. Problems with OCR or archive versions of specific files should likely be raised 'upstream', see https://github.com/ocrmypdf/OCRmyPDF/issues or https://github.com/tesseract-ocr/tesseract/issues
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@@ -59,6 +59,12 @@ body:
|
|||||||
label: Browser logs
|
label: Browser logs
|
||||||
description: Logs from the web browser related to your issue, if needed
|
description: Logs from the web browser related to your issue, if needed
|
||||||
render: bash
|
render: bash
|
||||||
|
- type: textarea
|
||||||
|
id: logs_services
|
||||||
|
attributes:
|
||||||
|
label: Services logs
|
||||||
|
description: Logs from other services (or containers) related to your issue, if needed. For example, the database or redis logs.
|
||||||
|
render: bash
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ NOTE: PRs that do not address the following will not be merged, please do not sk
|
|||||||
|
|
||||||
- [ ] I have read & agree with the [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
|
- [ ] I have read & agree with the [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
|
||||||
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
||||||
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
- [ ] If applicable, I have tested my code for breaking changes & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||||
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
||||||
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
||||||
- [ ] I have made corresponding changes to the documentation as needed.
|
- [ ] I have made corresponding changes to the documentation as needed.
|
||||||
- [ ] I have checked my modifications for any breaking changes.
|
- [ ] In the description of the PR above I have disclosed the use of AI tools in the coding of this PR.
|
||||||
+39
-13
@@ -41,30 +41,56 @@ updates:
|
|||||||
- "backend"
|
- "backend"
|
||||||
- "dependencies"
|
- "dependencies"
|
||||||
groups:
|
groups:
|
||||||
|
# Development & CI/CD Tooling
|
||||||
development:
|
development:
|
||||||
patterns:
|
patterns:
|
||||||
- "*pytest*"
|
- "*pytest*"
|
||||||
- "ruff"
|
- "ruff"
|
||||||
- "mkdocs-material"
|
- "zensical"
|
||||||
- "pre-commit*"
|
- "pre-commit*"
|
||||||
django:
|
# Django & DRF Ecosystem
|
||||||
|
django-ecosystem:
|
||||||
patterns:
|
patterns:
|
||||||
- "*django*"
|
- "*django*"
|
||||||
- "drf-*"
|
- "drf-*"
|
||||||
major-versions:
|
- "djangorestframework"
|
||||||
|
- "whitenoise"
|
||||||
|
- "bleach"
|
||||||
|
- "jinja2"
|
||||||
|
# Async, Task Queuing & Caching
|
||||||
|
async-tasks:
|
||||||
|
patterns:
|
||||||
|
- "celery*"
|
||||||
|
- "channels*"
|
||||||
|
- "flower"
|
||||||
|
- "redis"
|
||||||
|
# Document, PDF, and OCR Processing
|
||||||
|
document-processing:
|
||||||
|
patterns:
|
||||||
|
- "ocrmypdf"
|
||||||
|
- "pdf2image"
|
||||||
|
- "pyzbar"
|
||||||
|
- "zxing-cpp"
|
||||||
|
- "tika-client"
|
||||||
|
- "gotenberg-client"
|
||||||
|
- "python-magic"
|
||||||
|
- "python-gnupg"
|
||||||
|
# Data, NLP, and Search
|
||||||
|
data-nlp-search:
|
||||||
|
patterns:
|
||||||
|
- "nltk"
|
||||||
|
- "scikit-learn"
|
||||||
|
- "langdetect"
|
||||||
|
- "rapidfuzz"
|
||||||
|
- "whoosh-reloaded"
|
||||||
|
# Utilities (Patch Updates)
|
||||||
|
utilities-patch:
|
||||||
update-types:
|
update-types:
|
||||||
- "major"
|
- "patch"
|
||||||
small-changes:
|
# Utilities (Minor Updates)
|
||||||
|
utilities-minor:
|
||||||
update-types:
|
update-types:
|
||||||
- "minor"
|
- "minor"
|
||||||
- "patch"
|
|
||||||
exclude-patterns:
|
|
||||||
- "*django*"
|
|
||||||
- "drf-*"
|
|
||||||
pre-built:
|
|
||||||
patterns:
|
|
||||||
- psycopg*
|
|
||||||
- zxing-cpp
|
|
||||||
# Enable updates for GitHub Actions
|
# Enable updates for GitHub Actions
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
target-branch: "dev"
|
target-branch: "dev"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ include-labels:
|
|||||||
- 'notable'
|
- 'notable'
|
||||||
exclude-labels:
|
exclude-labels:
|
||||||
- 'skip-changelog'
|
- 'skip-changelog'
|
||||||
|
filter-by-commitish: true
|
||||||
category-template: '### $TITLE'
|
category-template: '### $TITLE'
|
||||||
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
change-template: '- $TITLE @$AUTHOR ([#$NUMBER]($URL))'
|
||||||
change-title-escapes: '\<*_&#@'
|
change-title-escapes: '\<*_&#@'
|
||||||
|
|||||||
+100
-59
@@ -11,6 +11,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'translations**'
|
- 'translations**'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
env:
|
env:
|
||||||
DEFAULT_UV_VERSION: "0.9.x"
|
DEFAULT_UV_VERSION: "0.9.x"
|
||||||
# This is the default version of Python to use in most steps which aren't specific
|
# This is the default version of Python to use in most steps which aren't specific
|
||||||
@@ -67,7 +71,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
@@ -75,20 +79,21 @@ jobs:
|
|||||||
- name: Check files
|
- name: Check files
|
||||||
uses: pre-commit/action@v3.0.1
|
uses: pre-commit/action@v3.0.1
|
||||||
documentation:
|
documentation:
|
||||||
name: "Build & Deploy Documentation"
|
name: "Build Documentation"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/configure-pages@v5
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -102,24 +107,30 @@ jobs:
|
|||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
--python ${{ steps.setup-python.outputs.python-version }} \
|
||||||
--dev \
|
--dev \
|
||||||
--frozen \
|
--frozen \
|
||||||
mkdocs build --config-file ./mkdocs.yml
|
zensical build --clean
|
||||||
- name: Deploy documentation
|
- name: Upload documentation artifact
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
uses: actions/upload-artifact@v5
|
||||||
run: |
|
|
||||||
echo "docs.paperless-ngx.com" > "${{ github.workspace }}/docs/CNAME"
|
|
||||||
git config --global user.name "${{ github.actor }}"
|
|
||||||
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
|
|
||||||
uv run \
|
|
||||||
--python ${{ steps.setup-python.outputs.python-version }} \
|
|
||||||
--dev \
|
|
||||||
--frozen \
|
|
||||||
mkdocs gh-deploy --force --no-history
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: documentation
|
name: documentation
|
||||||
path: site/
|
path: site/
|
||||||
retention-days: 7
|
- uses: actions/upload-pages-artifact@v4
|
||||||
|
with:
|
||||||
|
path: site
|
||||||
|
name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
deploy-documentation:
|
||||||
|
name: "Deploy Documentation"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- documentation
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/deploy-pages@v4
|
||||||
|
id: deployment
|
||||||
|
with:
|
||||||
|
artifact_name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
tests-backend:
|
tests-backend:
|
||||||
name: "Backend Tests (Python ${{ matrix.python-version }})"
|
name: "Backend Tests (Python ${{ matrix.python-version }})"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -131,7 +142,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: Start containers
|
- name: Start containers
|
||||||
run: |
|
run: |
|
||||||
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
|
docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml pull --quiet
|
||||||
@@ -142,7 +153,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "${{ matrix.python-version }}"
|
python-version: "${{ matrix.python-version }}"
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -181,10 +192,11 @@ jobs:
|
|||||||
pytest
|
pytest
|
||||||
- name: Upload backend test results to Codecov
|
- name: Upload backend test results to Codecov
|
||||||
if: always()
|
if: always()
|
||||||
uses: codecov/test-results-action@v1
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: backend-python-${{ matrix.python-version }}
|
flags: backend-python-${{ matrix.python-version }}
|
||||||
files: junit.xml
|
files: junit.xml
|
||||||
|
report_type: test_results
|
||||||
- name: Upload backend coverage to Codecov
|
- name: Upload backend coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -201,13 +213,13 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -234,13 +246,13 @@ jobs:
|
|||||||
shard-index: [1, 2, 3, 4]
|
shard-index: [1, 2, 3, 4]
|
||||||
shard-count: [4]
|
shard-count: [4]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -260,11 +272,12 @@ jobs:
|
|||||||
- name: Run Jest unit tests
|
- name: Run Jest unit tests
|
||||||
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
- name: Upload frontend test results to Codecov
|
- name: Upload frontend test results to Codecov
|
||||||
uses: codecov/test-results-action@v1
|
|
||||||
if: always()
|
if: always()
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
flags: frontend-node-${{ matrix.node-version }}
|
flags: frontend-node-${{ matrix.node-version }}
|
||||||
directory: src-ui/
|
directory: src-ui/
|
||||||
|
report_type: test_results
|
||||||
- name: Upload frontend coverage to Codecov
|
- name: Upload frontend coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
@@ -273,8 +286,12 @@ jobs:
|
|||||||
tests-frontend-e2e:
|
tests-frontend-e2e:
|
||||||
name: "Frontend E2E Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
name: "Frontend E2E Tests (Node ${{ matrix.node-version }} - ${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
container: mcr.microsoft.com/playwright:v1.57.0-noble
|
||||||
needs:
|
needs:
|
||||||
- install-frontend-dependencies
|
- install-frontend-dependencies
|
||||||
|
env:
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -282,13 +299,13 @@ jobs:
|
|||||||
shard-index: [1, 2]
|
shard-index: [1, 2]
|
||||||
shard-count: [2]
|
shard-count: [2]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -303,19 +320,8 @@ jobs:
|
|||||||
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-frontenddeps-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||||
- name: Re-link Angular cli
|
- name: Re-link Angular cli
|
||||||
run: cd src-ui && pnpm link @angular/cli
|
run: cd src-ui && pnpm link @angular/cli
|
||||||
- name: Cache Playwright browsers
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cache/ms-playwright
|
|
||||||
key: ${{ runner.os }}-playwright-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-playwright-
|
|
||||||
- name: Install Playwright system dependencies
|
|
||||||
run: npx playwright install-deps
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd src-ui && pnpm install --no-frozen-lockfile
|
run: cd src-ui && pnpm install --no-frozen-lockfile
|
||||||
- name: Install Playwright
|
|
||||||
run: cd src-ui && pnpm exec playwright install
|
|
||||||
- name: Run Playwright e2e tests
|
- name: Run Playwright e2e tests
|
||||||
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||||
frontend-bundle-analysis:
|
frontend-bundle-analysis:
|
||||||
@@ -325,13 +331,13 @@ jobs:
|
|||||||
- tests-frontend
|
- tests-frontend
|
||||||
- tests-frontend-e2e
|
- tests-frontend-e2e
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -351,9 +357,12 @@ jobs:
|
|||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
run: cd src-ui && pnpm run build --configuration=production
|
run: cd src-ui && pnpm run build --configuration=production
|
||||||
build-docker-image:
|
build-docker-image:
|
||||||
name: Build Docker image for ${{ github.ref_name }}
|
name: Build Docker image for ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/l10n_'))
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
if: (github.event_name == 'push' && (startsWith(github.ref, 'refs/heads/feature-') || startsWith(github.ref, 'refs/heads/fix-') || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/beta' || contains(github.ref, 'beta.rc') || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/l10n_'))) || (github.event_name == 'pull_request' && (startsWith(github.head_ref, 'feature-') || startsWith(github.head_ref, 'fix-') || github.head_ref == 'dev' || github.head_ref == 'beta' || contains(github.head_ref, 'beta.rc') || startsWith(github.head_ref, 'l10n_')))
|
||||||
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
|
||||||
@@ -362,6 +371,23 @@ jobs:
|
|||||||
- tests-frontend
|
- tests-frontend
|
||||||
- tests-frontend-e2e
|
- tests-frontend-e2e
|
||||||
steps:
|
steps:
|
||||||
|
- name: Prepare build variables
|
||||||
|
id: build-vars
|
||||||
|
uses: actions/github-script@v8
|
||||||
|
with:
|
||||||
|
result-encoding: string
|
||||||
|
script: |
|
||||||
|
const isPR = context.eventName === 'pull_request';
|
||||||
|
const defaultRefName = context.ref.replace('refs/heads/', '');
|
||||||
|
const headRef = isPR ? context.payload.pull_request.head.ref : defaultRefName;
|
||||||
|
const buildRef = isPR ? `refs/heads/${headRef}` : context.ref;
|
||||||
|
const buildCacheKey = headRef.split('/').join('-');
|
||||||
|
const canPush = context.eventName === 'push' || (isPR && context.payload.pull_request.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}`);
|
||||||
|
|
||||||
|
core.setOutput('build-ref', buildRef);
|
||||||
|
core.setOutput('build-ref-name', headRef);
|
||||||
|
core.setOutput('build-cache-key', buildCacheKey);
|
||||||
|
core.setOutput('can-push', canPush ? 'true' : 'false');
|
||||||
- name: Check pushing to Docker Hub
|
- name: Check pushing to Docker Hub
|
||||||
id: push-other-places
|
id: push-other-places
|
||||||
# Only push to Dockerhub from the main repo AND the ref is either:
|
# Only push to Dockerhub from the main repo AND the ref is either:
|
||||||
@@ -370,8 +396,11 @@ jobs:
|
|||||||
# beta
|
# beta
|
||||||
# a tag
|
# a tag
|
||||||
# Otherwise forks would require a Docker Hub account and secrets setup
|
# Otherwise forks would require a Docker Hub account and secrets setup
|
||||||
|
env:
|
||||||
|
BUILD_REF: ${{ steps.build-vars.outputs.build-ref }}
|
||||||
|
BUILD_REF_NAME: ${{ steps.build-vars.outputs.build-ref-name }}
|
||||||
run: |
|
run: |
|
||||||
if [[ ${{ github.repository_owner }} == "paperless-ngx" && ( ${{ github.ref_name }} == "dev" || ${{ github.ref_name }} == "beta" || ${{ startsWith(github.ref, 'refs/tags/v') }} == "true" ) ]] ; then
|
if [[ ${{ github.repository_owner }} == "paperless-ngx" && ( "$BUILD_REF_NAME" == "dev" || "$BUILD_REF_NAME" == "beta" || $BUILD_REF == refs/tags/v* || $BUILD_REF == *beta.rc* ) ]] ; then
|
||||||
echo "Enabling DockerHub image push"
|
echo "Enabling DockerHub image push"
|
||||||
echo "enable=true" >> $GITHUB_OUTPUT
|
echo "enable=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
@@ -395,12 +424,14 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
# Tag branches with branch name
|
# Tag branches with branch name
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
|
# Pull requests need a sanitized branch tag for pushing images
|
||||||
|
type=raw,value=${{ steps.build-vars.outputs.build-cache-key }},enable=${{ github.event_name == 'pull_request' }}
|
||||||
# Process semver tags
|
# Process semver tags
|
||||||
# For a tag x.y.z or vX.Y.Z, output an x.y.z and x.y image tag
|
# For a tag x.y.z or vX.Y.Z, output an x.y.z and x.y image tag
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
# If https://github.com/docker/buildx/issues/1044 is resolved,
|
# If https://github.com/docker/buildx/issues/1044 is resolved,
|
||||||
# the append input with a native arm64 arch could be used to
|
# the append input with a native arm64 arch could be used to
|
||||||
# significantly speed up building
|
# significantly speed up building
|
||||||
@@ -437,7 +468,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ steps.build-vars.outputs.can-push == 'true' }}
|
||||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||||
build-args: |
|
build-args: |
|
||||||
@@ -445,19 +476,21 @@ jobs:
|
|||||||
# Get cache layers from this branch, then dev
|
# Get cache layers from this branch, then dev
|
||||||
# This allows new branches to get at least some cache benefits, generally from dev
|
# This allows new branches to get at least some cache benefits, generally from dev
|
||||||
cache-from: |
|
cache-from: |
|
||||||
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
|
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ steps.build-vars.outputs.build-cache-key }}
|
||||||
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:dev
|
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:dev
|
||||||
cache-to: |
|
cache-to: ${{ steps.build-vars.outputs.can-push == 'true' && format('type=registry,mode=max,ref=ghcr.io/{0}/builder/cache/app:{1}', steps.set-ghcr-repository.outputs.ghcr-repository, steps.build-vars.outputs.build-cache-key) || '' }}
|
||||||
type=registry,mode=max,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
|
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
|
if: steps.build-vars.outputs.can-push == 'true'
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
|
docker buildx imagetools inspect ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
|
||||||
- name: Export frontend artifact from docker
|
- name: Export frontend artifact from docker
|
||||||
|
if: steps.build-vars.outputs.can-push == 'true'
|
||||||
run: |
|
run: |
|
||||||
docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
|
docker create --name frontend-extract ${{ fromJSON(steps.docker-meta.outputs.json).tags[0] }}
|
||||||
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
|
docker cp frontend-extract:/usr/src/paperless/src/documents/static/frontend src/documents/static/frontend/
|
||||||
- name: Upload frontend artifact
|
- name: Upload frontend artifact
|
||||||
uses: actions/upload-artifact@v4
|
if: steps.build-vars.outputs.can-push == 'true'
|
||||||
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: frontend-compiled
|
name: frontend-compiled
|
||||||
path: src/documents/static/frontend/
|
path: src/documents/static/frontend/
|
||||||
@@ -467,17 +500,18 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-docker-image
|
- build-docker-image
|
||||||
- documentation
|
- documentation
|
||||||
|
if: github.event_name == 'push'
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: setup-python
|
id: setup-python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
@@ -490,12 +524,12 @@ jobs:
|
|||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -qq --no-install-recommends gettext liblept5
|
sudo apt-get install -qq --no-install-recommends gettext liblept5
|
||||||
- name: Download frontend artifact
|
- name: Download frontend artifact
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: frontend-compiled
|
name: frontend-compiled
|
||||||
path: src/documents/static/frontend/
|
path: src/documents/static/frontend/
|
||||||
- name: Download documentation artifact
|
- name: Download documentation artifact
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: documentation
|
name: documentation
|
||||||
path: docs/_build/html/
|
path: docs/_build/html/
|
||||||
@@ -558,7 +592,7 @@ jobs:
|
|||||||
sudo chown -R 1000:1000 paperless-ngx/
|
sudo chown -R 1000:1000 paperless-ngx/
|
||||||
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
tar -cJf paperless-ngx.tar.xz paperless-ngx/
|
||||||
- name: Upload release artifact
|
- name: Upload release artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/paperless-ngx.tar.xz
|
path: dist/paperless-ngx.tar.xz
|
||||||
@@ -566,6 +600,8 @@ jobs:
|
|||||||
publish-release:
|
publish-release:
|
||||||
name: "Publish Release"
|
name: "Publish Release"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
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 }}
|
||||||
@@ -575,7 +611,7 @@ jobs:
|
|||||||
if: github.ref_type == 'tag' && (startsWith(github.ref_name, 'v') || contains(github.ref_name, '-beta.rc'))
|
if: github.ref_type == 'tag' && (startsWith(github.ref_name, 'v') || contains(github.ref_name, '-beta.rc'))
|
||||||
steps:
|
steps:
|
||||||
- name: Download release artifact
|
- name: Download release artifact
|
||||||
uses: actions/download-artifact@v5
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: ./
|
path: ./
|
||||||
@@ -597,6 +633,7 @@ jobs:
|
|||||||
version: ${{ steps.get_version.outputs.version }}
|
version: ${{ steps.get_version.outputs.version }}
|
||||||
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
prerelease: ${{ steps.get_version.outputs.prerelease }}
|
||||||
publish: true # ensures release is not marked as draft
|
publish: true # ensures release is not marked as draft
|
||||||
|
commitish: main
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Upload release archive
|
- name: Upload release archive
|
||||||
@@ -611,12 +648,16 @@ jobs:
|
|||||||
append-changelog:
|
append-changelog:
|
||||||
name: "Append Changelog"
|
name: "Append Changelog"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
needs:
|
needs:
|
||||||
- publish-release
|
- publish-release
|
||||||
if: needs.publish-release.outputs.prerelease == 'false'
|
if: needs.publish-release.outputs.prerelease == 'false'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
@@ -625,7 +666,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_UV_VERSION }}
|
version: ${{ env.DEFAULT_UV_VERSION }}
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Clean temporary images
|
- name: Clean temporary images
|
||||||
if: "${{ env.TOKEN != '' }}"
|
if: "${{ env.TOKEN != '' }}"
|
||||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.11.0
|
uses: stumpylog/image-cleaner-action/ephemeral@v0.12.0
|
||||||
with:
|
with:
|
||||||
token: "${{ env.TOKEN }}"
|
token: "${{ env.TOKEN }}"
|
||||||
owner: "${{ github.repository_owner }}"
|
owner: "${{ github.repository_owner }}"
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Clean untagged images
|
- name: Clean untagged images
|
||||||
if: "${{ env.TOKEN != '' }}"
|
if: "${{ env.TOKEN != '' }}"
|
||||||
uses: stumpylog/image-cleaner-action/untagged@v0.11.0
|
uses: stumpylog/image-cleaner-action/untagged@v0.12.0
|
||||||
with:
|
with:
|
||||||
token: "${{ env.TOKEN }}"
|
token: "${{ env.TOKEN }}"
|
||||||
owner: "${{ github.repository_owner }}"
|
owner: "${{ github.repository_owner }}"
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ jobs:
|
|||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -45,4 +45,4 @@ jobs:
|
|||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.PNGX_BOT_PAT }}
|
token: ${{ secrets.PNGX_BOT_PAT }}
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
sudo apt-get update -qq
|
sudo apt-get update -qq
|
||||||
sudo apt-get install -qq --no-install-recommends gettext
|
sudo apt-get install -qq --no-install-recommends gettext
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v6
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
- name: Install backend python dependencies
|
- name: Install backend python dependencies
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: 10
|
version: 10
|
||||||
- name: Use Node.js 20
|
- name: Use Node.js 20
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
cd src-ui
|
cd src-ui
|
||||||
pnpm run ng extract-i18n
|
pnpm run ng extract-i18n
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: stefanzweifel/git-auto-commit-action@v6
|
uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
with:
|
with:
|
||||||
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
||||||
commit_message: "Auto translate strings"
|
commit_message: "Auto translate strings"
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ junit.xml
|
|||||||
# Django stuff:
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# MkDocs documentation
|
# Zensical documentation
|
||||||
site/
|
site/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ repos:
|
|||||||
- 'prettier-plugin-organize-imports@4.1.0'
|
- 'prettier-plugin-organize-imports@4.1.0'
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.0
|
rev: v0.14.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: "v2.11.0"
|
rev: "v2.11.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
# Dockerfile hooks
|
# Dockerfile hooks
|
||||||
@@ -64,11 +64,11 @@ repos:
|
|||||||
- id: hadolint
|
- id: hadolint
|
||||||
# Shell script hooks
|
# Shell script hooks
|
||||||
- repo: https://github.com/lovesegfault/beautysh
|
- repo: https://github.com/lovesegfault/beautysh
|
||||||
rev: v6.2.1
|
rev: v6.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: beautysh
|
- id: beautysh
|
||||||
additional_dependencies:
|
types: [file]
|
||||||
- setuptools
|
files: (\.sh$|/run$|/finish$)
|
||||||
args:
|
args:
|
||||||
- "--tab"
|
- "--tab"
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||||
@@ -76,7 +76,7 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
- repo: https://github.com/google/yamlfmt
|
- repo: https://github.com/google/yamlfmt
|
||||||
rev: v0.18.0
|
rev: v0.20.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: yamlfmt
|
- id: yamlfmt
|
||||||
exclude: "^src-ui/pnpm-lock.yaml"
|
exclude: "^src-ui/pnpm-lock.yaml"
|
||||||
|
|||||||
+6
-19
@@ -5,7 +5,7 @@
|
|||||||
# Purpose: Compiles the frontend
|
# Purpose: Compiles the frontend
|
||||||
# Notes:
|
# Notes:
|
||||||
# - Does PNPM stuff with Typescript and such
|
# - Does PNPM stuff with Typescript and such
|
||||||
FROM --platform=$BUILDPLATFORM docker.io/node:20-bookworm-slim AS compile-frontend
|
FROM --platform=$BUILDPLATFORM docker.io/node:20-trixie-slim AS compile-frontend
|
||||||
|
|
||||||
COPY ./src-ui /src/src-ui
|
COPY ./src-ui /src/src-ui
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ RUN set -eux \
|
|||||||
# Purpose: Installs s6-overlay and rootfs
|
# Purpose: Installs s6-overlay and rootfs
|
||||||
# Comments:
|
# Comments:
|
||||||
# - Don't leave anything extra in here either
|
# - Don't leave anything extra in here either
|
||||||
FROM ghcr.io/astral-sh/uv:0.9.4-python3.12-bookworm-slim AS s6-overlay-base
|
FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-overlay-base
|
||||||
|
|
||||||
WORKDIR /usr/src/s6
|
WORKDIR /usr/src/s6
|
||||||
|
|
||||||
@@ -102,8 +102,6 @@ ARG TARGETARCH
|
|||||||
|
|
||||||
# Can be workflow provided, defaults set for manual building
|
# Can be workflow provided, defaults set for manual building
|
||||||
ARG JBIG2ENC_VERSION=0.30
|
ARG JBIG2ENC_VERSION=0.30
|
||||||
ARG QPDF_VERSION=11.9.0
|
|
||||||
ARG GS_VERSION=10.03.1
|
|
||||||
|
|
||||||
# Set Python environment variables
|
# Set Python environment variables
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
@@ -170,20 +168,8 @@ RUN set -eux \
|
|||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \
|
&& apt-get install --yes --quiet --no-install-recommends ${RUNTIME_PACKAGES} \
|
||||||
&& echo "Installing pre-built updates" \
|
&& echo "Installing pre-built updates" \
|
||||||
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all --parallel --parallel-max 4 \
|
&& curl --fail --silent --no-progress-meter --show-error --location --remote-name-all \
|
||||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-trixie-v${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||||
https://github.com/paperless-ngx/builder/releases/download/qpdf-${QPDF_VERSION}/qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/ghostscript-${GS_VERSION}/libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
|
||||||
https://github.com/paperless-ngx/builder/releases/download/jbig2enc-${JBIG2ENC_VERSION}/jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
&& echo "Installing qpdf ${QPDF_VERSION}" \
|
|
||||||
&& dpkg --install ./libqpdf29_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
&& dpkg --install ./qpdf_${QPDF_VERSION}-1_${TARGETARCH}.deb \
|
|
||||||
&& echo "Installing Ghostscript ${GS_VERSION}" \
|
|
||||||
&& dpkg --install ./libgs10-common_${GS_VERSION}.dfsg-1_all.deb \
|
|
||||||
&& dpkg --install ./libgs10_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
&& dpkg --install ./ghostscript_${GS_VERSION}.dfsg-1_${TARGETARCH}.deb \
|
|
||||||
&& echo "Installing jbig2enc" \
|
&& echo "Installing jbig2enc" \
|
||||||
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
&& dpkg --install ./jbig2enc_${JBIG2ENC_VERSION}-1_${TARGETARCH}.deb \
|
||||||
&& echo "Configuring imagemagick" \
|
&& echo "Configuring imagemagick" \
|
||||||
@@ -254,7 +240,8 @@ RUN set -eux \
|
|||||||
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
|
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
|
||||||
&& echo "Collecting static files" \
|
&& echo "Collecting static files" \
|
||||||
&& s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
|
&& s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
|
||||||
&& s6-setuidgid paperless python3 manage.py compilemessages
|
&& s6-setuidgid paperless python3 manage.py compilemessages \
|
||||||
|
&& /usr/local/bin/deduplicate.py --verbose /usr/src/paperless/static/
|
||||||
|
|
||||||
VOLUME ["/usr/src/paperless/data", \
|
VOLUME ["/usr/src/paperless/data", \
|
||||||
"/usr/src/paperless/media", \
|
"/usr/src/paperless/media", \
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# correct networking for the tests
|
# correct networking for the tests
|
||||||
services:
|
services:
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.24
|
image: docker.io/gotenberg/gotenberg:8.25
|
||||||
hostname: gotenberg
|
hostname: gotenberg
|
||||||
container_name: gotenberg
|
container_name: gotenberg
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.24
|
image: docker.io/gotenberg/gotenberg:8.25
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.24
|
image: docker.io/gotenberg/gotenberg:8.25
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ services:
|
|||||||
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
|
||||||
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: docker.io/gotenberg/gotenberg:8.24
|
image: docker.io/gotenberg/gotenberg:8.25
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# The gotenberg chromium route is used to convert .eml files. We do not
|
# The gotenberg chromium route is used to convert .eml files. We do not
|
||||||
# want to allow external content like tracking pixels or even javascript.
|
# want to allow external content like tracking pixels or even javascript.
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py management_command "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py management_command "$@"
|
s6-setuidgid paperless python3 manage.py management_command "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py management_command "$@"
|
python3 manage.py management_command "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -29,5 +29,5 @@ if find /run/s6/container_environment/*"_FILE" -maxdepth 1 > /dev/null 2>&1; the
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
echo "${log_prefix} No *_FILE environment found"
|
echo "${log_prefix} No *_FILE environment found"
|
||||||
fi
|
fi
|
||||||
@@ -2,6 +2,17 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
declare -r log_prefix="[init-user]"
|
declare -r log_prefix="[init-user]"
|
||||||
|
|
||||||
|
# When the container is started as a non-root user (e.g. via `user: 999:999`
|
||||||
|
# in Docker Compose), usermod/groupmod require root and are meaningless.
|
||||||
|
# USERMAP_* variables only apply to the root-started path.
|
||||||
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
if [[ -n "${USERMAP_UID}" || -n "${USERMAP_GID}" ]]; then
|
||||||
|
echo "${log_prefix} WARNING: USERMAP_UID/USERMAP_GID are set but have no effect when the container is started as a non-root user"
|
||||||
|
fi
|
||||||
|
echo "${log_prefix} Running as non-root user ($(id --user):$(id --group)), skipping UID/GID remapping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
declare -r usermap_original_uid=$(id -u paperless)
|
declare -r usermap_original_uid=$(id -u paperless)
|
||||||
declare -r usermap_original_gid=$(id -g paperless)
|
declare -r usermap_original_gid=$(id -g paperless)
|
||||||
declare -r usermap_new_uid=${USERMAP_UID:-$usermap_original_uid}
|
declare -r usermap_new_uid=${USERMAP_UID:-$usermap_original_uid}
|
||||||
|
|||||||
@@ -1,70 +1,66 @@
|
|||||||
#!/command/with-contenv /usr/bin/bash
|
#!/command/with-contenv /usr/bin/bash
|
||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
|
# vim: set ft=bash ts=4 sw=4 sts=4 et :
|
||||||
|
|
||||||
declare -r log_prefix="[init-db-wait]"
|
set -euo pipefail
|
||||||
|
|
||||||
|
declare -r LOG_PREFIX="[init-db-wait]"
|
||||||
|
|
||||||
|
declare -ri TIMEOUT=60
|
||||||
|
declare -i ATTEMPT=0
|
||||||
|
declare -i DELAY=0
|
||||||
|
declare -i STARTED_AT=${EPOCHSECONDS:?EPOCHSECONDS var unset}
|
||||||
|
|
||||||
|
delay_next_attempt() {
|
||||||
|
local -i elapsed=$(( EPOCHSECONDS - STARTED_AT ))
|
||||||
|
local -ri remaining=$(( TIMEOUT - elapsed ))
|
||||||
|
|
||||||
|
if (( remaining <= 0 )); then
|
||||||
|
echo "${LOG_PREFIX} Unable to connect after $elapsed seconds."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DELAY+=1
|
||||||
|
|
||||||
|
# clamp to remaining time
|
||||||
|
if (( DELAY > remaining )); then
|
||||||
|
DELAY=$remaining
|
||||||
|
fi
|
||||||
|
|
||||||
|
ATTEMPT+=1
|
||||||
|
echo "${LOG_PREFIX} Attempt $ATTEMPT failed! Trying again in $DELAY seconds..."
|
||||||
|
sleep "$DELAY"
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_postgres() {
|
wait_for_postgres() {
|
||||||
local attempt_num=1
|
echo "${LOG_PREFIX} Waiting for PostgreSQL to start..."
|
||||||
local -r max_attempts=5
|
|
||||||
|
|
||||||
echo "${log_prefix} Waiting for PostgreSQL to start..."
|
|
||||||
|
|
||||||
local -r host="${PAPERLESS_DBHOST:-localhost}"
|
local -r host="${PAPERLESS_DBHOST:-localhost}"
|
||||||
local -r port="${PAPERLESS_DBPORT:-5432}"
|
local -r port="${PAPERLESS_DBPORT:-5432}"
|
||||||
local -r user="${PAPERLESS_DBUSER:-paperless}"
|
local -r user="${PAPERLESS_DBUSER:-paperless}"
|
||||||
|
|
||||||
# Disable warning, host and port can't have spaces
|
while ! pg_isready -h "${host}" -p "${port}" --username "${user}"; do
|
||||||
# shellcheck disable=SC2086
|
delay_next_attempt
|
||||||
while [ ! "$(pg_isready -h ${host} -p ${port} --username ${user})" ]; do
|
|
||||||
|
|
||||||
if [ $attempt_num -eq $max_attempts ]; then
|
|
||||||
echo "${log_prefix} Unable to connect to database."
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "${log_prefix} Attempt $attempt_num failed! Trying again in 5 seconds..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
attempt_num=$(("$attempt_num" + 1))
|
|
||||||
sleep 5
|
|
||||||
done
|
done
|
||||||
# Extra in case this is a first start
|
echo "${LOG_PREFIX} Connected to PostgreSQL"
|
||||||
sleep 5
|
|
||||||
echo "Connected to PostgreSQL"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_mariadb() {
|
wait_for_mariadb() {
|
||||||
echo "${log_prefix} Waiting for MariaDB to start..."
|
echo "${LOG_PREFIX} Waiting for MariaDB to start..."
|
||||||
|
|
||||||
local -r host="${PAPERLESS_DBHOST:=localhost}"
|
local -r host="${PAPERLESS_DBHOST:-localhost}"
|
||||||
local -r port="${PAPERLESS_DBPORT:=3306}"
|
local -r port="${PAPERLESS_DBPORT:-3306}"
|
||||||
|
|
||||||
local attempt_num=1
|
while ! mariadb-admin --host="${host}" --port="${port}" --skip-ssl ping --silent >/dev/null 2>&1; do
|
||||||
local -r max_attempts=5
|
delay_next_attempt
|
||||||
|
|
||||||
# Disable warning, host and port can't have spaces
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
while ! true > /dev/tcp/$host/$port; do
|
|
||||||
|
|
||||||
if [ $attempt_num -eq $max_attempts ]; then
|
|
||||||
echo "${log_prefix} Unable to connect to database."
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "${log_prefix} Attempt $attempt_num failed! Trying again in 5 seconds..."
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
attempt_num=$(("$attempt_num" + 1))
|
|
||||||
sleep 5
|
|
||||||
done
|
done
|
||||||
echo "Connected to MariaDB"
|
echo "${LOG_PREFIX} Connected to MariaDB"
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "${PAPERLESS_DBENGINE}" == "mariadb" ]]; then
|
if [[ "${PAPERLESS_DBENGINE:-}" == "mariadb" ]]; then
|
||||||
echo "${log_prefix} Waiting for MariaDB to report ready"
|
|
||||||
wait_for_mariadb
|
wait_for_mariadb
|
||||||
elif [[ -n "${PAPERLESS_DBHOST}" ]]; then
|
elif [[ -n "${PAPERLESS_DBHOST:-}" ]]; then
|
||||||
echo "${log_prefix} Waiting for postgresql to report ready"
|
|
||||||
wait_for_postgres
|
wait_for_postgres
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "${log_prefix} Database is ready"
|
echo "${LOG_PREFIX} Database is ready"
|
||||||
@@ -10,11 +10,11 @@ export GRANIAN_WORKERS=${GRANIAN_WORKERS:-${PAPERLESS_WEBSERVER_WORKERS:-1}}
|
|||||||
|
|
||||||
# Only set GRANIAN_URL_PATH_PREFIX if PAPERLESS_FORCE_SCRIPT_NAME is set
|
# Only set GRANIAN_URL_PATH_PREFIX if PAPERLESS_FORCE_SCRIPT_NAME is set
|
||||||
if [[ -n "${PAPERLESS_FORCE_SCRIPT_NAME}" ]]; then
|
if [[ -n "${PAPERLESS_FORCE_SCRIPT_NAME}" ]]; then
|
||||||
export GRANIAN_URL_PATH_PREFIX=${PAPERLESS_FORCE_SCRIPT_NAME}
|
export GRANIAN_URL_PATH_PREFIX=${PAPERLESS_FORCE_SCRIPT_NAME}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
exec granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
exec granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
||||||
else
|
else
|
||||||
exec s6-setuidgid paperless granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
exec s6-setuidgid paperless granian --interface asginl --ws --loop uvloop "paperless.asgi:application"
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py convert_mariadb_uuid "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@"
|
s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py convert_mariadb_uuid "$@"
|
python3 manage.py convert_mariadb_uuid "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py createsuperuser "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py createsuperuser "$@"
|
s6-setuidgid paperless python3 manage.py createsuperuser "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py createsuperuser "$@"
|
python3 manage.py createsuperuser "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py decrypt_documents "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py decrypt_documents "$@"
|
s6-setuidgid paperless python3 manage.py decrypt_documents "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py decrypt_documents "$@"
|
python3 manage.py decrypt_documents "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
Executable
+167
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
File deduplication script that replaces identical files with symlinks.
|
||||||
|
Uses SHA256 hashing to identify duplicate files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import click
|
||||||
|
import humanize
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_sha256(filepath: Path) -> str | None:
|
||||||
|
sha256_hash = hashlib.sha256()
|
||||||
|
try:
|
||||||
|
with filepath.open("rb") as f:
|
||||||
|
# Read file in chunks to handle large files efficiently
|
||||||
|
while chunk := f.read(65536): # 64KB chunks
|
||||||
|
sha256_hash.update(chunk)
|
||||||
|
return sha256_hash.hexdigest()
|
||||||
|
except OSError as e:
|
||||||
|
click.echo(f"Error reading {filepath}: {e}", err=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_duplicate_files(directory: Path) -> dict[str, list[Path]]:
|
||||||
|
"""
|
||||||
|
Recursively scan directory and group files by their SHA256 hash.
|
||||||
|
Returns a dictionary mapping hash -> list of file paths.
|
||||||
|
"""
|
||||||
|
hash_to_files: dict[str, list[Path]] = defaultdict(list)
|
||||||
|
|
||||||
|
for filepath in directory.rglob("*"):
|
||||||
|
# Skip symlinks
|
||||||
|
if filepath.is_symlink():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip if not a regular file
|
||||||
|
if not filepath.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_hash = calculate_sha256(filepath)
|
||||||
|
if file_hash:
|
||||||
|
hash_to_files[file_hash].append(filepath)
|
||||||
|
|
||||||
|
# Filter to only return hashes with duplicates
|
||||||
|
return {h: files for h, files in hash_to_files.items() if len(files) > 1}
|
||||||
|
|
||||||
|
|
||||||
|
def replace_with_symlinks(
|
||||||
|
duplicate_groups: dict[str, list[Path]],
|
||||||
|
*,
|
||||||
|
dry_run: bool = False,
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Replace duplicate files with symlinks to the first occurrence.
|
||||||
|
Returns (number_of_files_replaced, space_saved_in_bytes).
|
||||||
|
"""
|
||||||
|
total_duplicates = 0
|
||||||
|
space_saved = 0
|
||||||
|
|
||||||
|
for file_hash, file_list in duplicate_groups.items():
|
||||||
|
# Keep the first file as the original, replace others with symlinks
|
||||||
|
original_file = file_list[0]
|
||||||
|
duplicates = file_list[1:]
|
||||||
|
|
||||||
|
click.echo(f"Found {len(duplicates)} duplicate(s) of: {original_file}")
|
||||||
|
|
||||||
|
for duplicate in duplicates:
|
||||||
|
try:
|
||||||
|
# Get file size before deletion
|
||||||
|
file_size = duplicate.stat().st_size
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
click.echo(f" [DRY RUN] Would replace: {duplicate}")
|
||||||
|
else:
|
||||||
|
# Remove the duplicate file
|
||||||
|
duplicate.unlink()
|
||||||
|
|
||||||
|
# Create relative symlink if possible, otherwise absolute
|
||||||
|
try:
|
||||||
|
# Try to create a relative symlink
|
||||||
|
rel_path = original_file.relative_to(duplicate.parent)
|
||||||
|
duplicate.symlink_to(rel_path)
|
||||||
|
click.echo(f" Replaced: {duplicate} -> {rel_path}")
|
||||||
|
except ValueError:
|
||||||
|
# Fall back to absolute path
|
||||||
|
duplicate.symlink_to(original_file.resolve())
|
||||||
|
click.echo(f" Replaced: {duplicate} -> {original_file}")
|
||||||
|
|
||||||
|
space_saved += file_size
|
||||||
|
|
||||||
|
total_duplicates += 1
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
click.echo(f" Error replacing {duplicate}: {e}", err=True)
|
||||||
|
|
||||||
|
return total_duplicates, space_saved
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument(
|
||||||
|
"directory",
|
||||||
|
type=click.Path(
|
||||||
|
exists=True,
|
||||||
|
file_okay=False,
|
||||||
|
dir_okay=True,
|
||||||
|
readable=True,
|
||||||
|
path_type=Path,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dry-run",
|
||||||
|
is_flag=True,
|
||||||
|
help="Show what would be done without making changes",
|
||||||
|
)
|
||||||
|
@click.option("--verbose", "-v", is_flag=True, help="Show verbose output")
|
||||||
|
def deduplicate(directory: Path, *, dry_run: bool, verbose: bool) -> None:
|
||||||
|
"""
|
||||||
|
Recursively search DIRECTORY for identical files and replace them with symlinks.
|
||||||
|
|
||||||
|
Uses SHA256 hashing to identify duplicate files. The first occurrence of each
|
||||||
|
unique file is kept, and all duplicates are replaced with symlinks pointing to it.
|
||||||
|
"""
|
||||||
|
directory = directory.resolve()
|
||||||
|
|
||||||
|
click.echo(f"Scanning directory: {directory}")
|
||||||
|
if dry_run:
|
||||||
|
click.echo("Running in DRY RUN mode - no changes will be made")
|
||||||
|
|
||||||
|
# Find all duplicate files
|
||||||
|
click.echo("Calculating file hashes...")
|
||||||
|
duplicate_groups = find_duplicate_files(directory)
|
||||||
|
|
||||||
|
if not duplicate_groups:
|
||||||
|
click.echo("No duplicate files found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
total_files = sum(len(files) - 1 for files in duplicate_groups.values())
|
||||||
|
click.echo(
|
||||||
|
f"Found {len(duplicate_groups)} group(s) of duplicates "
|
||||||
|
f"({total_files} files to deduplicate)",
|
||||||
|
)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
for file_hash, files in duplicate_groups.items():
|
||||||
|
click.echo(f"Hash: {file_hash}")
|
||||||
|
for f in files:
|
||||||
|
click.echo(f" - {f}")
|
||||||
|
|
||||||
|
# Replace duplicates with symlinks
|
||||||
|
click.echo("Processing duplicates...")
|
||||||
|
num_replaced, space_saved = replace_with_symlinks(duplicate_groups, dry_run=dry_run)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
click.echo(
|
||||||
|
f"{'Would replace' if dry_run else 'Replaced'} "
|
||||||
|
f"{num_replaced} duplicate file(s)",
|
||||||
|
)
|
||||||
|
if not dry_run:
|
||||||
|
click.echo(f"Space saved: {humanize.naturalsize(space_saved, binary=True)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
deduplicate()
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_archiver "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_archiver "$@"
|
s6-setuidgid paperless python3 manage.py document_archiver "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_archiver "$@"
|
python3 manage.py document_archiver "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_create_classifier "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_create_classifier "$@"
|
s6-setuidgid paperless python3 manage.py document_create_classifier "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_create_classifier "$@"
|
python3 manage.py document_create_classifier "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_exporter "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_exporter "$@"
|
s6-setuidgid paperless python3 manage.py document_exporter "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_exporter "$@"
|
python3 manage.py document_exporter "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_fuzzy_match "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@"
|
s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_fuzzy_match "$@"
|
python3 manage.py document_fuzzy_match "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_importer "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_importer "$@"
|
s6-setuidgid paperless python3 manage.py document_importer "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_importer "$@"
|
python3 manage.py document_importer "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_index "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_index "$@"
|
s6-setuidgid paperless python3 manage.py document_index "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_index "$@"
|
python3 manage.py document_index "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_renamer "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_renamer "$@"
|
s6-setuidgid paperless python3 manage.py document_renamer "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_renamer "$@"
|
python3 manage.py document_renamer "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_retagger "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_retagger "$@"
|
s6-setuidgid paperless python3 manage.py document_retagger "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_retagger "$@"
|
python3 manage.py document_retagger "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_sanity_checker "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_sanity_checker "$@"
|
s6-setuidgid paperless python3 manage.py document_sanity_checker "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_sanity_checker "$@"
|
python3 manage.py document_sanity_checker "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py document_thumbnails "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py document_thumbnails "$@"
|
s6-setuidgid paperless python3 manage.py document_thumbnails "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py document_thumbnails "$@"
|
python3 manage.py document_thumbnails "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py mail_fetcher "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py mail_fetcher "$@"
|
s6-setuidgid paperless python3 manage.py mail_fetcher "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py mail_fetcher "$@"
|
python3 manage.py mail_fetcher "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py manage_superuser "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py manage_superuser "$@"
|
s6-setuidgid paperless python3 manage.py manage_superuser "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py manage_superuser "$@"
|
python3 manage.py manage_superuser "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -5,10 +5,13 @@ set -e
|
|||||||
|
|
||||||
cd "${PAPERLESS_SRC_DIR}"
|
cd "${PAPERLESS_SRC_DIR}"
|
||||||
|
|
||||||
if [[ $(id -u) == 0 ]]; then
|
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
|
||||||
|
python3 manage.py prune_audit_logs "$@"
|
||||||
|
elif [[ $(id -u) == 0 ]]; then
|
||||||
s6-setuidgid paperless python3 manage.py prune_audit_logs "$@"
|
s6-setuidgid paperless python3 manage.py prune_audit_logs "$@"
|
||||||
elif [[ $(id -un) == "paperless" ]]; then
|
elif [[ $(id -un) == "paperless" ]]; then
|
||||||
python3 manage.py prune_audit_logs "$@"
|
python3 manage.py prune_audit_logs "$@"
|
||||||
else
|
else
|
||||||
echo "Unknown user."
|
echo "Unknown user."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -431,8 +431,10 @@ This allows for complex logic to be included in the format, including [logical s
|
|||||||
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11) to manipulate the [variables](#filename-format-variables)
|
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11) to manipulate the [variables](#filename-format-variables)
|
||||||
provided. The template is provided as a string, potentially multiline, and rendered into a single line.
|
provided. The template is provided as a string, potentially multiline, and rendered into a single line.
|
||||||
|
|
||||||
In addition, the entire Document instance is available to be utilized in a more advanced way, as well as some variables which only make sense to be accessed
|
In addition, a limited `document` object is available for advanced templates.
|
||||||
with more complex logic.
|
This object includes common metadata fields such as `id`, `pk`, `title`, `content`, `page_count`, `created`, `added`, `modified`, `mime_type`,
|
||||||
|
`checksum`, `archive_checksum`, `archive_serial_number`, `filename`, `archive_filename`, and `original_filename`.
|
||||||
|
Related values are available as nested objects with limited fields, for example document.correspondent.name, etc.
|
||||||
|
|
||||||
#### Custom Jinja2 Filters
|
#### Custom Jinja2 Filters
|
||||||
|
|
||||||
@@ -759,7 +761,7 @@ MariaDB: `mariadb-tzinfo-to-sql /usr/share/zoneinfo | mariadb -u root mysql -p`
|
|||||||
|
|
||||||
## Barcodes {#barcodes}
|
## Barcodes {#barcodes}
|
||||||
|
|
||||||
Paperless is able to utilize barcodes for automatically performing some tasks.
|
Paperless is able to utilize barcodes for automatically performing some tasks. Barcodes are only supported for PDF documents or TIFF, [if enabled](configuration.md#PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT).
|
||||||
|
|
||||||
At this time, the library utilized for detection of barcodes supports the following types:
|
At this time, the library utilized for detection of barcodes supports the following types:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# The REST API
|
# REST API
|
||||||
|
|
||||||
Paperless-ngx now ships with a fully-documented REST API and a browsable
|
Paperless-ngx now ships with a fully-documented REST API and a browsable
|
||||||
web interface to explore it. The API browsable interface is available at
|
web interface to explore it. The API browsable interface is available at
|
||||||
|
|||||||
+25
-22
@@ -1,13 +1,31 @@
|
|||||||
:root > * {
|
:root>* {
|
||||||
--md-primary-fg-color: #17541f;
|
--paperless-green: #17541f;
|
||||||
--md-primary-fg-color--dark: #17541f;
|
--paperless-green-accent: #2b8a38;
|
||||||
--md-primary-fg-color--light: #17541f;
|
--md-primary-fg-color: var(--paperless-green);
|
||||||
--md-accent-fg-color: #2b8a38;
|
--md-primary-fg-color--dark: var(--paperless-green);
|
||||||
|
--md-primary-fg-color--light: var(--paperless-green-accent);
|
||||||
|
--md-accent-fg-color: var(--paperless-green-accent);
|
||||||
--md-typeset-a-color: #21652a;
|
--md-typeset-a-color: #21652a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-header,
|
||||||
|
.md-tabs {
|
||||||
|
background-color: var(--paperless-green);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-tabs__link {
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-tabs__link:hover,
|
||||||
|
.md-tabs__link--active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
[data-md-color-scheme="slate"] {
|
[data-md-color-scheme="slate"] {
|
||||||
--md-hue: 222;
|
--md-hue: 222;
|
||||||
|
--md-default-bg-color: hsla(var(--md-hue), 15%, 10%, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -69,8 +87,8 @@ h4 code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Hide config vars from sidebar, toc and move the border on mobile case their hidden */
|
/* Hide config vars from sidebar, toc and move the border on mobile case their hidden */
|
||||||
.md-nav.md-nav--secondary .md-nav__item .md-nav__link[href*="PAPERLESS_"],
|
.md-nav.md-nav--secondary .md-nav__item:has(> .md-nav__link[href*="PAPERLESS_"]),
|
||||||
.md-nav.md-nav--secondary .md-nav__item .md-nav__link[href*="USERMAP_"] {
|
.md-nav.md-nav--secondary .md-nav__item:has(> .md-nav__link[href*="USERMAP_"]) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,18 +101,3 @@ h4 code {
|
|||||||
border-top: .05rem solid var(--md-default-fg-color--lightest);
|
border-top: .05rem solid var(--md-default-fg-color--lightest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show search shortcut key */
|
|
||||||
[data-md-toggle="search"]:not(:checked) ~ .md-header .md-search__form::after {
|
|
||||||
position: absolute;
|
|
||||||
top: .3rem;
|
|
||||||
right: .3rem;
|
|
||||||
display: block;
|
|
||||||
padding: .1rem .4rem;
|
|
||||||
color: var(--md-default-fg-color--lighter);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: .8rem;
|
|
||||||
border: .05rem solid var(--md-default-fg-color--lighter);
|
|
||||||
border-radius: .1rem;
|
|
||||||
content: "/";
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -1,5 +1,519 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.14
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: do not submit permissions for non-owners [@shamoon](https://github.com/shamoon) ([#12571](https://github.com/paperless-ngx/paperless-ngx/pull/12571))
|
||||||
|
- Fix: prevent duplicate parent tag IDs [@shamoon](https://github.com/shamoon) ([#12522](https://github.com/paperless-ngx/paperless-ngx/pull/12522))
|
||||||
|
- Fix: dont defer tag change application in workflows [@shamoon](https://github.com/shamoon) ([#12478](https://github.com/paperless-ngx/paperless-ngx/pull/12478))
|
||||||
|
- Fix: limit share link viewset actions [@shamoon](https://github.com/shamoon) ([#12461](https://github.com/paperless-ngx/paperless-ngx/pull/12461))
|
||||||
|
- Fix: add fallback ordering for documents by id after created [@shamoon](https://github.com/shamoon) ([#12440](https://github.com/paperless-ngx/paperless-ngx/pull/12440))
|
||||||
|
- Fixhancement: default mail-created correspondent matching to exact [@shamoon](https://github.com/shamoon) ([#12414](https://github.com/paperless-ngx/paperless-ngx/pull/12414))
|
||||||
|
- Fix: validate date CF value in serializer [@shamoon](https://github.com/shamoon) ([#12410](https://github.com/paperless-ngx/paperless-ngx/pull/12410))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>7 changes</summary>
|
||||||
|
|
||||||
|
- Fix: do not submit permissions for non-owners [@shamoon](https://github.com/shamoon) ([#12571](https://github.com/paperless-ngx/paperless-ngx/pull/12571))
|
||||||
|
- Fix: prevent duplicate parent tag IDs [@shamoon](https://github.com/shamoon) ([#12522](https://github.com/paperless-ngx/paperless-ngx/pull/12522))
|
||||||
|
- Fix: dont defer tag change application in workflows [@shamoon](https://github.com/shamoon) ([#12478](https://github.com/paperless-ngx/paperless-ngx/pull/12478))
|
||||||
|
- Fix: limit share link viewset actions [@shamoon](https://github.com/shamoon) ([#12461](https://github.com/paperless-ngx/paperless-ngx/pull/12461))
|
||||||
|
- Fix: add fallback ordering for documents by id after created [@shamoon](https://github.com/shamoon) ([#12440](https://github.com/paperless-ngx/paperless-ngx/pull/12440))
|
||||||
|
- Fixhancement: default mail-created correspondent matching to exact [@shamoon](https://github.com/shamoon) ([#12414](https://github.com/paperless-ngx/paperless-ngx/pull/12414))
|
||||||
|
- Fix: validate date CF value in serializer [@shamoon](https://github.com/shamoon) ([#12410](https://github.com/paperless-ngx/paperless-ngx/pull/12410))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.13
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: suggest corrections only if visible results
|
||||||
|
- Fix: require view permission for more-like search
|
||||||
|
- Fix: validate document link targets
|
||||||
|
- Fix: enforce permissions when attaching accounts to mail rules
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.12
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-96jx-fj7m-qh6x](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-96jx-fj7m-qh6x)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: Scope the workflow saves to prevent clobbering filename/archive_filename [@stumpylog](https://github.com/stumpylog) ([#12390](https://github.com/paperless-ngx/paperless-ngx/pull/12390))
|
||||||
|
- Fix: don't try to usermod/groupmod when non-root + update docs (#<!---->12365) [@stumpylog](https://github.com/stumpylog) ([#12391](https://github.com/paperless-ngx/paperless-ngx/pull/12391))
|
||||||
|
- Fix: avoid moving files if already moved [@shamoon](https://github.com/shamoon) ([#12389](https://github.com/paperless-ngx/paperless-ngx/pull/12389))
|
||||||
|
- Fix: remove pagination from document notes api spec [@shamoon](https://github.com/shamoon) ([#12388](https://github.com/paperless-ngx/paperless-ngx/pull/12388))
|
||||||
|
- Fix: fix file button hover color in dark mode [@shamoon](https://github.com/shamoon) ([#12367](https://github.com/paperless-ngx/paperless-ngx/pull/12367))
|
||||||
|
- Fixhancement: only offer basic auth for appropriate requests [@shamoon](https://github.com/shamoon) ([#12362](https://github.com/paperless-ngx/paperless-ngx/pull/12362))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>5 changes</summary>
|
||||||
|
|
||||||
|
- Fix: Scope the workflow saves to prevent clobbering filename/archive_filename [@stumpylog](https://github.com/stumpylog) ([#12390](https://github.com/paperless-ngx/paperless-ngx/pull/12390))
|
||||||
|
- Fix: avoid moving files if already moved [@shamoon](https://github.com/shamoon) ([#12389](https://github.com/paperless-ngx/paperless-ngx/pull/12389))
|
||||||
|
- Fix: remove pagination from document notes api spec [@shamoon](https://github.com/shamoon) ([#12388](https://github.com/paperless-ngx/paperless-ngx/pull/12388))
|
||||||
|
- Fix: fix file button hover color in dark mode [@shamoon](https://github.com/shamoon) ([#12367](https://github.com/paperless-ngx/paperless-ngx/pull/12367))
|
||||||
|
- Fixhancement: only offer basic auth for appropriate requests [@shamoon](https://github.com/shamoon) ([#12362](https://github.com/paperless-ngx/paperless-ngx/pull/12362))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.11
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-59xh-5vwx-4c4q](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-59xh-5vwx-4c4q)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328))
|
||||||
|
- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326))
|
||||||
|
- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302))
|
||||||
|
- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>4 changes</summary>
|
||||||
|
|
||||||
|
- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328))
|
||||||
|
- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326))
|
||||||
|
- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302))
|
||||||
|
- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.10
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: support string coercion in filepath jinja templates [@shamoon](https://github.com/shamoon) ([#12244](https://github.com/paperless-ngx/paperless-ngx/pull/12244))
|
||||||
|
- Fix: apply ordering after annotating tag document count [@shamoon](https://github.com/shamoon) ([#12238](https://github.com/paperless-ngx/paperless-ngx/pull/12238))
|
||||||
|
- Fix: enforce path limit for db filename fields [@shamoon](https://github.com/shamoon) ([#12235](https://github.com/paperless-ngx/paperless-ngx/pull/12235))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>3 changes</summary>
|
||||||
|
|
||||||
|
- Fix: support string coercion in filepath jinja templates [@shamoon](https://github.com/shamoon) ([#12244](https://github.com/paperless-ngx/paperless-ngx/pull/12244))
|
||||||
|
- Fix: apply ordering after annotating tag document count [@shamoon](https://github.com/shamoon) ([#12238](https://github.com/paperless-ngx/paperless-ngx/pull/12238))
|
||||||
|
- Fix: enforce path limit for db filename fields [@shamoon](https://github.com/shamoon) ([#12235](https://github.com/paperless-ngx/paperless-ngx/pull/12235))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.9
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-386h-chg4-cfw9](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-386h-chg4-cfw9)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixhancement: config option reset [@shamoon](https://github.com/shamoon) ([#12176](https://github.com/paperless-ngx/paperless-ngx/pull/12176))
|
||||||
|
- Fix: correct page count by separating display vs collection sizes for tags [@shamoon](https://github.com/shamoon) ([#12170](https://github.com/paperless-ngx/paperless-ngx/pull/12170))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>2 changes</summary>
|
||||||
|
|
||||||
|
- Fixhancement: config option reset [@shamoon](https://github.com/shamoon) ([#12176](https://github.com/paperless-ngx/paperless-ngx/pull/12176))
|
||||||
|
- Fix: correct page count by separating display vs collection sizes for tags [@shamoon](https://github.com/shamoon) ([#12170](https://github.com/paperless-ngx/paperless-ngx/pull/12170))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.8
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-7qqc-wrcw-2fj9](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-7qqc-wrcw-2fj9)
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.7
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-x395-6h48-wr8v](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-x395-6h48-wr8v)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Performance fix: use subqueries to improve object retrieval in large installs [@shamoon](https://github.com/shamoon) ([#11950](https://github.com/paperless-ngx/paperless-ngx/pull/11950))
|
||||||
|
- Fix: correct user dropdown button icon styling [@shamoon](https://github.com/shamoon) ([#12092](https://github.com/paperless-ngx/paperless-ngx/issues/12092))
|
||||||
|
- Fix: fix broken docker create_classifier command in 2.20.6 [@shamoon](https://github.com/shamoon) ([#11965](https://github.com/paperless-ngx/paperless-ngx/issues/11965))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>3 changes</summary>
|
||||||
|
|
||||||
|
- Performance fix: use subqueries to improve object retrieval in large installs [@shamoon](https://github.com/shamoon) ([#11950](https://github.com/paperless-ngx/paperless-ngx/pull/11950))
|
||||||
|
- Fix: correct user dropdown button icon styling [@shamoon](https://github.com/shamoon) ([#12092](https://github.com/paperless-ngx/paperless-ngx/issues/12092))
|
||||||
|
- Fix: fix broken docker create_classifier command in 2.20.6 [@shamoon](https://github.com/shamoon) ([#11965](https://github.com/paperless-ngx/paperless-ngx/issues/11965))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.6
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-jqwv-hx7q-fxh3](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-jqwv-hx7q-fxh3) and [GHSA-w47q-3m69-84v8](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-w47q-3m69-84v8)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: extract all ids for nested tags [@shamoon](https://github.com/shamoon) ([#11888](https://github.com/paperless-ngx/paperless-ngx/pull/11888))
|
||||||
|
- Fixhancement: change date calculation for 'this year' to include future documents [@shamoon](https://github.com/shamoon) ([#11884](https://github.com/paperless-ngx/paperless-ngx/pull/11884))
|
||||||
|
- Fix: Running management scripts under rootless could fail [@stumpylog](https://github.com/stumpylog) ([#11870](https://github.com/paperless-ngx/paperless-ngx/pull/11870))
|
||||||
|
- Fix: use correct field id for overrides [@shamoon](https://github.com/shamoon) ([#11869](https://github.com/paperless-ngx/paperless-ngx/pull/11869))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>3 changes</summary>
|
||||||
|
|
||||||
|
- Fix: extract all ids for nested tags [@shamoon](https://github.com/shamoon) ([#11888](https://github.com/paperless-ngx/paperless-ngx/pull/11888))
|
||||||
|
- Fixhancement: change date calculation for 'this year' to include future documents [@shamoon](https://github.com/shamoon) ([#11884](https://github.com/paperless-ngx/paperless-ngx/pull/11884))
|
||||||
|
- Fix: use correct field id for overrides [@shamoon](https://github.com/shamoon) ([#11869](https://github.com/paperless-ngx/paperless-ngx/pull/11869))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.5
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent [@shamoon](https://github.com/shamoon) ([#11811](https://github.com/paperless-ngx/paperless-ngx/pull/11811))
|
||||||
|
- Fix: use explicit order field for workflow actions [@shamoon](https://github.com/shamoon) [@stumpylog](https://github.com/stumpylog) ([#11781](https://github.com/paperless-ngx/paperless-ngx/pull/11781))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>2 changes</summary>
|
||||||
|
|
||||||
|
- Fix: ensure horizontal scroll for long tag names in list, wrap tags without parent [@shamoon](https://github.com/shamoon) ([#11811](https://github.com/paperless-ngx/paperless-ngx/pull/11811))
|
||||||
|
- Fix: use explicit order field for workflow actions [@shamoon](https://github.com/shamoon) [@stumpylog](https://github.com/stumpylog) ([#11781](https://github.com/paperless-ngx/paperless-ngx/pull/11781))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.4
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-28cf-xvcf-hw6m](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-28cf-xvcf-hw6m)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659))
|
||||||
|
- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661))
|
||||||
|
- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666))
|
||||||
|
- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731))
|
||||||
|
- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>5 changes</summary>
|
||||||
|
|
||||||
|
- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659))
|
||||||
|
- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661))
|
||||||
|
- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666))
|
||||||
|
- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731))
|
||||||
|
- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.3
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-7cq3-mhxq-w946](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-7cq3-mhxq-w946)
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.2
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Resolve [GHSA-6653-vcx4-69mc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-6653-vcx4-69mc)
|
||||||
|
- Resolve [GHSA-24x5-wp64-9fcc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-24x5-wp64-9fcc)
|
||||||
|
|
||||||
|
### Features / Enhancements
|
||||||
|
|
||||||
|
- Tweakhancement: dim inactive users in users-groups list [@shamoon](https://github.com/shamoon) ([#11537](https://github.com/paperless-ngx/paperless-ngx/pull/11537))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: Expanded SVG validation whitelist and additional checks [@stumpylog](https://github.com/stumpylog) ([#11590](https://github.com/paperless-ngx/paperless-ngx/pull/11590))
|
||||||
|
- Fix: normalize allowed SVG tag and attribute names, add version [@shamoon](https://github.com/shamoon) ([#11586](https://github.com/paperless-ngx/paperless-ngx/pull/11586))
|
||||||
|
- Fix: pass additional arguments to TagSerializer for permissions [@shamoon](https://github.com/shamoon) ([#11576](https://github.com/paperless-ngx/paperless-ngx/pull/11576))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore(deps): Bump actions/checkout from 5 to 6 in the actions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#11515](https://github.com/paperless-ngx/paperless-ngx/pull/11515))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>6 changes</summary>
|
||||||
|
|
||||||
|
- Chore: update Angular dependencies to 20.3.15 [@shamoon](https://github.com/shamoon) ([#11568](https://github.com/paperless-ngx/paperless-ngx/pull/11568))
|
||||||
|
- Chore(deps): Bump actions/checkout from 5 to 6 in the actions group @[dependabot[bot]](https://github.com/apps/dependabot) ([#11515](https://github.com/paperless-ngx/paperless-ngx/pull/11515))
|
||||||
|
- Chore(deps-dev): Bump webpack from 5.102.1 to 5.103.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11513](https://github.com/paperless-ngx/paperless-ngx/pull/11513))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.56.1 to 1.57.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11514](https://github.com/paperless-ngx/paperless-ngx/pull/11514))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11512](https://github.com/paperless-ngx/paperless-ngx/pull/11512))
|
||||||
|
- docker(deps): bump astral-sh/uv from 0.9.14-python3.12-trixie-slim to 0.9.15-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#11533](https://github.com/paperless-ngx/paperless-ngx/pull/11533))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>12 changes</summary>
|
||||||
|
|
||||||
|
- Fix: Expanded SVG validation whitelist and additional checks [@stumpylog](https://github.com/stumpylog) ([#11590](https://github.com/paperless-ngx/paperless-ngx/pull/11590))
|
||||||
|
- Fixhancement: pass ordering to tag children [@shamoon](https://github.com/shamoon) ([#11556](https://github.com/paperless-ngx/paperless-ngx/pull/11556))
|
||||||
|
- Performance: avoid unnecessary filename operations on bulk custom field updates [@shamoon](https://github.com/shamoon) ([#11558](https://github.com/paperless-ngx/paperless-ngx/pull/11558))
|
||||||
|
- Fix: normalize allowed SVG tag and attribute names, add version [@shamoon](https://github.com/shamoon) ([#11586](https://github.com/paperless-ngx/paperless-ngx/pull/11586))
|
||||||
|
- Chore: refactor workflows code [@shamoon](https://github.com/shamoon) ([#11563](https://github.com/paperless-ngx/paperless-ngx/pull/11563))
|
||||||
|
- Fix: pass additional arguments to TagSerializer for permissions [@shamoon](https://github.com/shamoon) ([#11576](https://github.com/paperless-ngx/paperless-ngx/pull/11576))
|
||||||
|
- Chore: update Angular dependencies to 20.3.15 [@shamoon](https://github.com/shamoon) ([#11568](https://github.com/paperless-ngx/paperless-ngx/pull/11568))
|
||||||
|
- Chore(deps-dev): Bump webpack from 5.102.1 to 5.103.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11513](https://github.com/paperless-ngx/paperless-ngx/pull/11513))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.56.1 to 1.57.0 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11514](https://github.com/paperless-ngx/paperless-ngx/pull/11514))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11512](https://github.com/paperless-ngx/paperless-ngx/pull/11512))
|
||||||
|
- Tweakhancement: dim inactive users in users-groups list [@shamoon](https://github.com/shamoon) ([#11537](https://github.com/paperless-ngx/paperless-ngx/pull/11537))
|
||||||
|
- Chore: add some output of social login errors [@shamoon](https://github.com/shamoon) ([#11527](https://github.com/paperless-ngx/paperless-ngx/pull/11527))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.1
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: set search term when using advanced search from global search [@shamoon](https://github.com/shamoon) ([#11503](https://github.com/paperless-ngx/paperless-ngx/pull/11503))
|
||||||
|
- Fix: change async handling of select custom field updates [@shamoon](https://github.com/shamoon) ([#11490](https://github.com/paperless-ngx/paperless-ngx/pull/11490))
|
||||||
|
- Fix: skip SSL for MariaDB ping in init script [@danielrheinbay](https://github.com/danielrheinbay) ([#11491](https://github.com/paperless-ngx/paperless-ngx/pull/11491))
|
||||||
|
- Fix: handle allauth groups location breaking change [@shamoon](https://github.com/shamoon) ([#11471](https://github.com/paperless-ngx/paperless-ngx/pull/11471))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.9.10-python3.12-trixie-slim to 0.9.11-python3.12-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#11450](https://github.com/paperless-ngx/paperless-ngx/pull/11450))
|
||||||
|
- Chore(deps): Bump @angular/common from 20.3.12 to 20.3.14 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11481](https://github.com/paperless-ngx/paperless-ngx/pull/11481))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>4 changes</summary>
|
||||||
|
|
||||||
|
- Fix: set search term when using advanced search from global search [@shamoon](https://github.com/shamoon) ([#11503](https://github.com/paperless-ngx/paperless-ngx/pull/11503))
|
||||||
|
- Fix: change async handling of select custom field updates [@shamoon](https://github.com/shamoon) ([#11490](https://github.com/paperless-ngx/paperless-ngx/pull/11490))
|
||||||
|
- Chore(deps): Bump @angular/common from 20.3.12 to 20.3.14 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11481](https://github.com/paperless-ngx/paperless-ngx/pull/11481))
|
||||||
|
- Fix: handle allauth groups location breaking change [@shamoon](https://github.com/shamoon) ([#11471](https://github.com/paperless-ngx/paperless-ngx/pull/11471))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.20.0
|
||||||
|
|
||||||
|
### Notable Changes
|
||||||
|
|
||||||
|
- Feature: Upgrade underlying Docker image to Trixie [@stumpylog](https://github.com/stumpylog) ([#10562](https://github.com/paperless-ngx/paperless-ngx/pull/10562))
|
||||||
|
|
||||||
|
### Features / Enhancements
|
||||||
|
|
||||||
|
- Feature: Upgrade underlying Docker image to Trixie [@stumpylog](https://github.com/stumpylog) ([#10562](https://github.com/paperless-ngx/paperless-ngx/pull/10562))
|
||||||
|
- Fixhancement: more log viewer improvements [@shamoon](https://github.com/shamoon) ([#11426](https://github.com/paperless-ngx/paperless-ngx/pull/11426))
|
||||||
|
- Performance: Replace duplicated static files with symlinks [@stumpylog](https://github.com/stumpylog) ([#11418](https://github.com/paperless-ngx/paperless-ngx/pull/11418))
|
||||||
|
- Enhancement: add more relative dates, support modified [@shamoon](https://github.com/shamoon) ([#11411](https://github.com/paperless-ngx/paperless-ngx/pull/11411))
|
||||||
|
- Performance: make move files after select custom field change async [@shamoon](https://github.com/shamoon) ([#11391](https://github.com/paperless-ngx/paperless-ngx/pull/11391))
|
||||||
|
- Enhancement: Use a better check for the MariaDB server to be ready [@stumpylog](https://github.com/stumpylog) ([#11396](https://github.com/paperless-ngx/paperless-ngx/pull/11396))
|
||||||
|
- Enhancement: speed-up docker container startup [@flrgh](https://github.com/flrgh) ([#11134](https://github.com/paperless-ngx/paperless-ngx/pull/11134))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: prevent focus loss from change detection in cf query dropdown [@shamoon](https://github.com/shamoon) ([#11409](https://github.com/paperless-ngx/paperless-ngx/pull/11409))
|
||||||
|
- Fix: sort editing filterable dropdowns sooner [@shamoon](https://github.com/shamoon) ([#11404](https://github.com/paperless-ngx/paperless-ngx/pull/11404))
|
||||||
|
- Fix: support for custom field ordering w advanced search [@shamoon](https://github.com/shamoon) ([#11383](https://github.com/paperless-ngx/paperless-ngx/pull/11383))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore(deps): Bump the actions group with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11259](https://github.com/paperless-ngx/paperless-ngx/pull/11259))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>16 changes</summary>
|
||||||
|
|
||||||
|
- Chore: Upgrades psycopg to 3.2.12 [@stumpylog](https://github.com/stumpylog) ([#11420](https://github.com/paperless-ngx/paperless-ngx/pull/11420))
|
||||||
|
- Chore(deps-dev): Bump glob from 10.4.1 to 10.5.0 in /src/paperless_mail/templates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11413](https://github.com/paperless-ngx/paperless-ngx/pull/11413))
|
||||||
|
- docker-compose(deps): bump gotenberg/gotenberg from 8.24 to 8.25 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#11393](https://github.com/paperless-ngx/paperless-ngx/pull/11393))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 21 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11260](https://github.com/paperless-ngx/paperless-ngx/pull/11260))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.55.1 to 1.56.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11263](https://github.com/paperless-ngx/paperless-ngx/pull/11263))
|
||||||
|
- Chore(deps-dev): Bump webpack from 5.102.0 to 5.102.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11264](https://github.com/paperless-ngx/paperless-ngx/pull/11264))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11262](https://github.com/paperless-ngx/paperless-ngx/pull/11262))
|
||||||
|
- Chore(deps-dev): Bump jest-preset-angular from 15.0.2 to 15.0.3 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#11261](https://github.com/paperless-ngx/paperless-ngx/pull/11261))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 24.6.1 to 24.9.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11265](https://github.com/paperless-ngx/paperless-ngx/pull/11265))
|
||||||
|
- Chore(deps): Bump the small-changes group across 1 directory with 11 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11337](https://github.com/paperless-ngx/paperless-ngx/pull/11337))
|
||||||
|
- Chore(deps): Bump django-auditlog from 3.2.1 to 3.3.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11021](https://github.com/paperless-ngx/paperless-ngx/pull/11021))
|
||||||
|
- Chore(deps): Bump the actions group with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11259](https://github.com/paperless-ngx/paperless-ngx/pull/11259))
|
||||||
|
- Chore(deps): Bump drf-spectacular-sidecar from 2025.9.1 to 2025.10.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11019](https://github.com/paperless-ngx/paperless-ngx/pull/11019))
|
||||||
|
- Chore(deps): Bump django-filter from 25.1 to 25.2 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11020](https://github.com/paperless-ngx/paperless-ngx/pull/11020))
|
||||||
|
- Chore(deps): Update django-allauth[mfa,socialaccount] requirement from ~=65.4.0 to ~=65.12.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11198](https://github.com/paperless-ngx/paperless-ngx/pull/11198))
|
||||||
|
- docker(deps): bump astral-sh/uv from 0.9.9-python3.12-bookworm-slim to 0.9.10-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#11394](https://github.com/paperless-ngx/paperless-ngx/pull/11394))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>19 changes</summary>
|
||||||
|
|
||||||
|
- Fixhancement: more log viewer improvements [@shamoon](https://github.com/shamoon) ([#11426](https://github.com/paperless-ngx/paperless-ngx/pull/11426))
|
||||||
|
- Chore: Upgrades psycopg to 3.2.12 [@stumpylog](https://github.com/stumpylog) ([#11420](https://github.com/paperless-ngx/paperless-ngx/pull/11420))
|
||||||
|
- Enhancement: add more relative dates, support modified [@shamoon](https://github.com/shamoon) ([#11411](https://github.com/paperless-ngx/paperless-ngx/pull/11411))
|
||||||
|
- Chore(deps-dev): Bump glob from 10.4.1 to 10.5.0 in /src/paperless_mail/templates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11413](https://github.com/paperless-ngx/paperless-ngx/pull/11413))
|
||||||
|
- Performance: make move files after select custom field change async [@shamoon](https://github.com/shamoon) ([#11391](https://github.com/paperless-ngx/paperless-ngx/pull/11391))
|
||||||
|
- Fix: prevent focus loss from change detection in cf query dropdown [@shamoon](https://github.com/shamoon) ([#11409](https://github.com/paperless-ngx/paperless-ngx/pull/11409))
|
||||||
|
- Fix: sort editing filterable dropdowns sooner [@shamoon](https://github.com/shamoon) ([#11404](https://github.com/paperless-ngx/paperless-ngx/pull/11404))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 21 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11260](https://github.com/paperless-ngx/paperless-ngx/pull/11260))
|
||||||
|
- Chore(deps-dev): Bump @playwright/test from 1.55.1 to 1.56.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11263](https://github.com/paperless-ngx/paperless-ngx/pull/11263))
|
||||||
|
- Chore(deps-dev): Bump webpack from 5.102.0 to 5.102.1 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11264](https://github.com/paperless-ngx/paperless-ngx/pull/11264))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11262](https://github.com/paperless-ngx/paperless-ngx/pull/11262))
|
||||||
|
- Chore(deps-dev): Bump jest-preset-angular from 15.0.2 to 15.0.3 in /src-ui in the frontend-jest-dependencies group @[dependabot[bot]](https://github.com/apps/dependabot) ([#11261](https://github.com/paperless-ngx/paperless-ngx/pull/11261))
|
||||||
|
- Chore(deps-dev): Bump @types/node from 24.6.1 to 24.9.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#11265](https://github.com/paperless-ngx/paperless-ngx/pull/11265))
|
||||||
|
- Chore(deps): Bump the small-changes group across 1 directory with 11 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#11337](https://github.com/paperless-ngx/paperless-ngx/pull/11337))
|
||||||
|
- Chore(deps): Bump django-auditlog from 3.2.1 to 3.3.0 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11021](https://github.com/paperless-ngx/paperless-ngx/pull/11021))
|
||||||
|
- Chore(deps): Bump drf-spectacular-sidecar from 2025.9.1 to 2025.10.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11019](https://github.com/paperless-ngx/paperless-ngx/pull/11019))
|
||||||
|
- Chore(deps): Bump django-filter from 25.1 to 25.2 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11020](https://github.com/paperless-ngx/paperless-ngx/pull/11020))
|
||||||
|
- Chore(deps): Update django-allauth[mfa,socialaccount] requirement from ~=65.4.0 to ~=65.12.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11198](https://github.com/paperless-ngx/paperless-ngx/pull/11198))
|
||||||
|
- Fix: support for custom field ordering w advanced search [@shamoon](https://github.com/shamoon) ([#11383](https://github.com/paperless-ngx/paperless-ngx/pull/11383))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.19.6
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Chore: include password validation on user edit [@shamoon](https://github.com/shamoon) ([#11308](https://github.com/paperless-ngx/paperless-ngx/pull/11308))
|
||||||
|
- Fix: include BASE_URL when constructing for workflows [@ebardsley](https://github.com/ebardsley) ([#11360](https://github.com/paperless-ngx/paperless-ngx/pull/11360))
|
||||||
|
- Fixhancement: refactor email attachment logic [@shamoon](https://github.com/shamoon) ([#11336](https://github.com/paperless-ngx/paperless-ngx/pull/11336))
|
||||||
|
- Fixhancement: trim whitespace for some text searches [@shamoon](https://github.com/shamoon) ([#11357](https://github.com/paperless-ngx/paperless-ngx/pull/11357))
|
||||||
|
- Fix: update Outlook refresh token when refreshed [@shamoon](https://github.com/shamoon) ([#11341](https://github.com/paperless-ngx/paperless-ngx/pull/11341))
|
||||||
|
- Fix: only cache remote version data for version checking [@shamoon](https://github.com/shamoon) ([#11320](https://github.com/paperless-ngx/paperless-ngx/pull/11320))
|
||||||
|
- Fix: include replace none logic in storage path preview, improve jinja conditionals for empty metadata [@shamoon](https://github.com/shamoon) ([#11315](https://github.com/paperless-ngx/paperless-ngx/pull/11315))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- docker(deps): bump astral-sh/uv from 0.9.7-python3.12-bookworm-slim to 0.9.9-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#11338](https://github.com/paperless-ngx/paperless-ngx/pull/11338))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>7 changes</summary>
|
||||||
|
|
||||||
|
- Fix: include BASE_URL when constructing for workflows [@ebardsley](https://github.com/ebardsley) ([#11360](https://github.com/paperless-ngx/paperless-ngx/pull/11360))
|
||||||
|
- Fixhancement: refactor email attachment logic [@shamoon](https://github.com/shamoon) ([#11336](https://github.com/paperless-ngx/paperless-ngx/pull/11336))
|
||||||
|
- Fixhancement: trim whitespace for some text searches [@shamoon](https://github.com/shamoon) ([#11357](https://github.com/paperless-ngx/paperless-ngx/pull/11357))
|
||||||
|
- Fix: update Outlook refresh token when refreshed [@shamoon](https://github.com/shamoon) ([#11341](https://github.com/paperless-ngx/paperless-ngx/pull/11341))
|
||||||
|
- Fix: only cache remote version data for version checking [@shamoon](https://github.com/shamoon) ([#11320](https://github.com/paperless-ngx/paperless-ngx/pull/11320))
|
||||||
|
- Fix: include replace none logic in storage path preview, improve jinja conditionals for empty metadata [@shamoon](https://github.com/shamoon) ([#11315](https://github.com/paperless-ngx/paperless-ngx/pull/11315))
|
||||||
|
- Chore: include password validation on user edit [@shamoon](https://github.com/shamoon) ([#11308](https://github.com/paperless-ngx/paperless-ngx/pull/11308))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.19.5
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: ensure custom field query propagation, change detection [@shamoon](https://github.com/shamoon) ([#11291](https://github.com/paperless-ngx/paperless-ngx/pull/11291))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- docker(deps): Bump astral-sh/uv from 0.9.4-python3.12-bookworm-slim to 0.9.7-python3.12-bookworm-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#11283](https://github.com/paperless-ngx/paperless-ngx/pull/11283))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
- Fix: ensure custom field query propagation, change detection [@shamoon](https://github.com/shamoon) ([#11291](https://github.com/paperless-ngx/paperless-ngx/pull/11291))
|
||||||
|
|
||||||
|
## paperless-ngx 2.19.4
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: use original_file when attaching docs to workflow emails with added trigger [@shamoon](https://github.com/shamoon) ([#11266](https://github.com/paperless-ngx/paperless-ngx/pull/11266))
|
||||||
|
- Fix: mark 'Select' button in doc list for translation [@shamoon](https://github.com/shamoon) ([#11278](https://github.com/paperless-ngx/paperless-ngx/pull/11278))
|
||||||
|
- Fix: respect fields parameter for created field [@shamoon](https://github.com/shamoon) ([#11251](https://github.com/paperless-ngx/paperless-ngx/pull/11251))
|
||||||
|
- Fix: improve legibility of processed mail error popover in light mode [@shamoon](https://github.com/shamoon) ([#11258](https://github.com/paperless-ngx/paperless-ngx/pull/11258))
|
||||||
|
- Fixhancement: truncate large logs, improve auto-scroll [@shamoon](https://github.com/shamoon) ([#11239](https://github.com/paperless-ngx/paperless-ngx/pull/11239))
|
||||||
|
- Chore: add max-height and overflow to processedmail error popover [@shamoon](https://github.com/shamoon) ([#11252](https://github.com/paperless-ngx/paperless-ngx/pull/11252))
|
||||||
|
- Fix: delay iframe DOM removal, handle onafterprint error for print in FF [@shamoon](https://github.com/shamoon) ([#11237](https://github.com/paperless-ngx/paperless-ngx/pull/11237))
|
||||||
|
- Fix: de-deduplicate children in tag list when filtering [@shamoon](https://github.com/shamoon) ([#11229](https://github.com/paperless-ngx/paperless-ngx/pull/11229))
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Performance: re-enable virtual scroll, bump ng-select [@shamoon](https://github.com/shamoon) ([#11279](https://github.com/paperless-ngx/paperless-ngx/pull/11279))
|
||||||
|
- Performance: use virtual scroll container and log level parsing for logs view [@MickLesk](https://github.com/MickLesk) ([#11233](https://github.com/paperless-ngx/paperless-ngx/pull/11233))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>11 changes</summary>
|
||||||
|
|
||||||
|
- Performance: re-enable virtual scroll, bump ng-select [@shamoon](https://github.com/shamoon) ([#11279](https://github.com/paperless-ngx/paperless-ngx/pull/11279))
|
||||||
|
- Fix: use original_file when attaching docs to workflow emails with added trigger [@shamoon](https://github.com/shamoon) ([#11266](https://github.com/paperless-ngx/paperless-ngx/pull/11266))
|
||||||
|
- Fix: mark 'Select' button in doc list for translation [@shamoon](https://github.com/shamoon) ([#11278](https://github.com/paperless-ngx/paperless-ngx/pull/11278))
|
||||||
|
- Fix: respect fields parameter for created field [@shamoon](https://github.com/shamoon) ([#11251](https://github.com/paperless-ngx/paperless-ngx/pull/11251))
|
||||||
|
- Fix: improve legibility of processed mail error popover in light mode [@shamoon](https://github.com/shamoon) ([#11258](https://github.com/paperless-ngx/paperless-ngx/pull/11258))
|
||||||
|
- Fixhancement: truncate large logs, improve auto-scroll [@shamoon](https://github.com/shamoon) ([#11239](https://github.com/paperless-ngx/paperless-ngx/pull/11239))
|
||||||
|
- Chore: add max-height and overflow to processedmail error popover [@shamoon](https://github.com/shamoon) ([#11252](https://github.com/paperless-ngx/paperless-ngx/pull/11252))
|
||||||
|
- Fix: delay iframe DOM removal, handle onafterprint error for print in FF [@shamoon](https://github.com/shamoon) ([#11237](https://github.com/paperless-ngx/paperless-ngx/pull/11237))
|
||||||
|
- Performance: use virtual scroll container and log level parsing for logs view [@MickLesk](https://github.com/MickLesk) ([#11233](https://github.com/paperless-ngx/paperless-ngx/pull/11233))
|
||||||
|
- Chore: cache Github version check for 15 minutes [@shamoon](https://github.com/shamoon) ([#11235](https://github.com/paperless-ngx/paperless-ngx/pull/11235))
|
||||||
|
- Fix: de-deduplicate children in tag list when filtering [@shamoon](https://github.com/shamoon) ([#11229](https://github.com/paperless-ngx/paperless-ngx/pull/11229))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.19.3
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: remove unnecessary permission requirements for new email endpoint [@shamoon](https://github.com/shamoon) ([#11215](https://github.com/paperless-ngx/paperless-ngx/pull/11215))
|
||||||
|
- Fix: refactor nested sorting in filterable dropdowns [@shamoon](https://github.com/shamoon) ([#11214](https://github.com/paperless-ngx/paperless-ngx/pull/11214))
|
||||||
|
- Fix: add root tag filtering for tag list page consistency, fix toggle all [@shamoon](https://github.com/shamoon) ([#11208](https://github.com/paperless-ngx/paperless-ngx/pull/11208))
|
||||||
|
- Fix: support ConsumableDocument in email attachments [@shamoon](https://github.com/shamoon) ([#11196](https://github.com/paperless-ngx/paperless-ngx/pull/11196))
|
||||||
|
- Fix: add missing import for ConfirmButtonComponent in user-edit-dialog [@shamoon](https://github.com/shamoon) ([#11167](https://github.com/paperless-ngx/paperless-ngx/pull/11167))
|
||||||
|
- Fix: resolve migration warning in 2.19.2 [@shamoon](https://github.com/shamoon) ([#11157](https://github.com/paperless-ngx/paperless-ngx/pull/11157))
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Change: make workflow action only title draggable [@shamoon](https://github.com/shamoon) ([#11209](https://github.com/paperless-ngx/paperless-ngx/pull/11209))
|
||||||
|
- Change: change workflowrun to softdeletemodel [@shamoon](https://github.com/shamoon) ([#11194](https://github.com/paperless-ngx/paperless-ngx/pull/11194))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore(deps): Bump django from 5.2.6 to 5.2.7 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11200](https://github.com/paperless-ngx/paperless-ngx/pull/11200))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>9 changes</summary>
|
||||||
|
|
||||||
|
- Chore(deps): Bump django from 5.2.6 to 5.2.7 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11200](https://github.com/paperless-ngx/paperless-ngx/pull/11200))
|
||||||
|
- Fix: remove unnecessary permission requirements for new email endpoint [@shamoon](https://github.com/shamoon) ([#11215](https://github.com/paperless-ngx/paperless-ngx/pull/11215))
|
||||||
|
- Fix: refactor nested sorting in filterable dropdowns [@shamoon](https://github.com/shamoon) ([#11214](https://github.com/paperless-ngx/paperless-ngx/pull/11214))
|
||||||
|
- Fix: add root tag filtering for tag list page consistency, fix toggle all [@shamoon](https://github.com/shamoon) ([#11208](https://github.com/paperless-ngx/paperless-ngx/pull/11208))
|
||||||
|
- Change: make workflow action only title draggable [@shamoon](https://github.com/shamoon) ([#11209](https://github.com/paperless-ngx/paperless-ngx/pull/11209))
|
||||||
|
- Change: change workflowrun to softdeletemodel [@shamoon](https://github.com/shamoon) ([#11194](https://github.com/paperless-ngx/paperless-ngx/pull/11194))
|
||||||
|
- Chore: Minor migration optimization for workflow titles [@stumpylog](https://github.com/stumpylog) ([#11197](https://github.com/paperless-ngx/paperless-ngx/pull/11197))
|
||||||
|
- Fix: support ConsumableDocument in email attachments [@shamoon](https://github.com/shamoon) ([#11196](https://github.com/paperless-ngx/paperless-ngx/pull/11196))
|
||||||
|
- Fix: add missing import for ConfirmButtonComponent in user-edit-dialog [@shamoon](https://github.com/shamoon) ([#11167](https://github.com/paperless-ngx/paperless-ngx/pull/11167))
|
||||||
|
- Fix: resolve migration warning in 2.19.2 [@shamoon](https://github.com/shamoon) ([#11157](https://github.com/paperless-ngx/paperless-ngx/pull/11157))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## paperless-ngx 2.19.2
|
||||||
|
|
||||||
|
### Features / Enhancements
|
||||||
|
|
||||||
|
- Fixhancement: display loading status for tags instead of 'Private' [@shamoon](https://github.com/shamoon) ([#11140](https://github.com/paperless-ngx/paperless-ngx/pull/11140))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: Remove edit requirement for bulk email, show based on setting [@shamoon](https://github.com/shamoon) ([#11149](https://github.com/paperless-ngx/paperless-ngx/pull/11149))
|
||||||
|
- Fix: handle undefined IDs in getOriginalObject [@shamoon](https://github.com/shamoon) ([#11147](https://github.com/paperless-ngx/paperless-ngx/pull/11147))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>3 changes</summary>
|
||||||
|
|
||||||
|
- Fix: Remove edit requirement for bulk email, show based on setting [@shamoon](https://github.com/shamoon) ([#11149](https://github.com/paperless-ngx/paperless-ngx/pull/11149))
|
||||||
|
- Fix: handle undefined IDs in getOriginalObject [@shamoon](https://github.com/shamoon) ([#11147](https://github.com/paperless-ngx/paperless-ngx/pull/11147))
|
||||||
|
- Fixhancement: display loading status for tags instead of 'Private' [@shamoon](https://github.com/shamoon) ([#11140](https://github.com/paperless-ngx/paperless-ngx/pull/11140))
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 2.19.1
|
## paperless-ngx 2.19.1
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+49
-32
@@ -170,11 +170,18 @@ Available options are `postgresql` and `mariadb`.
|
|||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
A small pool is typically sufficient — for example, a size of 4.
|
A pool of 8-10 connections per worker is typically sufficient.
|
||||||
Make sure your PostgreSQL server's max_connections setting is large enough to handle:
|
If you encounter error messages such as `couldn't get a connection`
|
||||||
```(Paperless workers + Celery workers) × pool size + safety margin```
|
or database connection timeouts, you probably need to increase the pool size.
|
||||||
For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4:
|
|
||||||
(4 + 2) × 4 + 10 = 34 connections required.
|
!!! warning
|
||||||
|
Make sure your PostgreSQL `max_connections` setting is large enough to handle the connection pools:
|
||||||
|
`(NB_PAPERLESS_WORKERS + NB_CELERY_WORKERS) × POOL_SIZE + SAFETY_MARGIN`. For example, with
|
||||||
|
4 Paperless workers and 2 Celery workers, and a pool size of 8:``(4 + 2) × 8 + 10 = 58`,
|
||||||
|
so `max_connections = 60` (or even more) is appropriate.
|
||||||
|
|
||||||
|
This assumes only Paperless-ngx connects to your PostgreSQL instance. If you have other applications,
|
||||||
|
you should increase `max_connections` accordingly.
|
||||||
|
|
||||||
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
||||||
|
|
||||||
@@ -1007,7 +1014,7 @@ still perform some basic text pre-processing before matching.
|
|||||||
|
|
||||||
: See also `PAPERLESS_NLTK_DIR`.
|
: See also `PAPERLESS_NLTK_DIR`.
|
||||||
|
|
||||||
Defaults to 1.
|
Defaults to true, enabling the feature.
|
||||||
|
|
||||||
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
|
#### [`PAPERLESS_DATE_PARSER_LANGUAGES=<lang>`](#PAPERLESS_DATE_PARSER_LANGUAGES) {#PAPERLESS_DATE_PARSER_LANGUAGES}
|
||||||
|
|
||||||
@@ -1054,17 +1061,27 @@ should be a valid crontab(5) expression describing when to run.
|
|||||||
|
|
||||||
#### [`PAPERLESS_SANITY_TASK_CRON=<cron expression>`](#PAPERLESS_SANITY_TASK_CRON) {#PAPERLESS_SANITY_TASK_CRON}
|
#### [`PAPERLESS_SANITY_TASK_CRON=<cron expression>`](#PAPERLESS_SANITY_TASK_CRON) {#PAPERLESS_SANITY_TASK_CRON}
|
||||||
|
|
||||||
: Configures the scheduled sanity checker frequency.
|
: Configures the scheduled sanity checker frequency. The value should be a
|
||||||
|
valid crontab(5) expression describing when to run.
|
||||||
|
|
||||||
: If set to the string "disable", the sanity checker will not run automatically.
|
: If set to the string "disable", the sanity checker will not run automatically.
|
||||||
|
|
||||||
Defaults to `30 0 * * sun` or Sunday at 30 minutes past midnight.
|
Defaults to `30 0 * * sun` or Sunday at 30 minutes past midnight.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_WORKFLOW_SCHEDULED_TASK_CRON=<cron expression>`](#PAPERLESS_WORKFLOW_SCHEDULED_TASK_CRON) {#PAPERLESS_WORKFLOW_SCHEDULED_TASK_CRON}
|
||||||
|
|
||||||
|
: Configures the scheduled workflow check frequency. The value should be a
|
||||||
|
valid crontab(5) expression describing when to run.
|
||||||
|
|
||||||
|
: If set to the string "disable", scheduled workflows will not run.
|
||||||
|
|
||||||
|
Defaults to `5 */1 * * *` or every hour at 5 minutes past the hour.
|
||||||
|
|
||||||
#### [`PAPERLESS_ENABLE_COMPRESSION=<bool>`](#PAPERLESS_ENABLE_COMPRESSION) {#PAPERLESS_ENABLE_COMPRESSION}
|
#### [`PAPERLESS_ENABLE_COMPRESSION=<bool>`](#PAPERLESS_ENABLE_COMPRESSION) {#PAPERLESS_ENABLE_COMPRESSION}
|
||||||
|
|
||||||
: Enables compression of the responses from the webserver.
|
: Enables compression of the responses from the webserver.
|
||||||
|
|
||||||
: Defaults to 1, enabling compression.
|
: Defaults to true, enabling compression.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
@@ -1271,30 +1288,6 @@ within your documents.
|
|||||||
|
|
||||||
Defaults to false.
|
Defaults to false.
|
||||||
|
|
||||||
## Workflow webhooks
|
|
||||||
|
|
||||||
#### [`PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES=<str>`](#PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES) {#PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES}
|
|
||||||
|
|
||||||
: A comma-separated list of allowed schemes for webhooks. This setting
|
|
||||||
controls which URL schemes are permitted for webhook URLs.
|
|
||||||
|
|
||||||
Defaults to `http,https`.
|
|
||||||
|
|
||||||
#### [`PAPERLESS_WEBHOOKS_ALLOWED_PORTS=<str>`](#PAPERLESS_WEBHOOKS_ALLOWED_PORTS) {#PAPERLESS_WEBHOOKS_ALLOWED_PORTS}
|
|
||||||
|
|
||||||
: A comma-separated list of allowed ports for webhooks. This setting
|
|
||||||
controls which ports are permitted for webhook URLs. For example, if you
|
|
||||||
set this to `80,443`, webhooks will only be sent to URLs that use these
|
|
||||||
ports.
|
|
||||||
|
|
||||||
Defaults to empty list, which allows all ports.
|
|
||||||
|
|
||||||
#### [`PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS=<bool>`](#PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS) {#PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS}
|
|
||||||
|
|
||||||
: If set to false, webhooks cannot be sent to internal URLs (e.g., localhost).
|
|
||||||
|
|
||||||
Defaults to true, which allows internal requests.
|
|
||||||
|
|
||||||
### Polling {#polling}
|
### Polling {#polling}
|
||||||
|
|
||||||
#### [`PAPERLESS_CONSUMER_POLLING=<num>`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING}
|
#### [`PAPERLESS_CONSUMER_POLLING=<num>`](#PAPERLESS_CONSUMER_POLLING) {#PAPERLESS_CONSUMER_POLLING}
|
||||||
@@ -1338,6 +1331,30 @@ consumers working on the same file. Configure this to prevent that.
|
|||||||
|
|
||||||
Defaults to 0.5 seconds.
|
Defaults to 0.5 seconds.
|
||||||
|
|
||||||
|
## Workflow webhooks
|
||||||
|
|
||||||
|
#### [`PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES=<str>`](#PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES) {#PAPERLESS_WEBHOOKS_ALLOWED_SCHEMES}
|
||||||
|
|
||||||
|
: A comma-separated list of allowed schemes for webhooks. This setting
|
||||||
|
controls which URL schemes are permitted for webhook URLs.
|
||||||
|
|
||||||
|
Defaults to `http,https`.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_WEBHOOKS_ALLOWED_PORTS=<str>`](#PAPERLESS_WEBHOOKS_ALLOWED_PORTS) {#PAPERLESS_WEBHOOKS_ALLOWED_PORTS}
|
||||||
|
|
||||||
|
: A comma-separated list of allowed ports for webhooks. This setting
|
||||||
|
controls which ports are permitted for webhook URLs. For example, if you
|
||||||
|
set this to `80,443`, webhooks will only be sent to URLs that use these
|
||||||
|
ports.
|
||||||
|
|
||||||
|
Defaults to empty list, which allows all ports.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS=<bool>`](#PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS) {#PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS}
|
||||||
|
|
||||||
|
: If set to false, webhooks cannot be sent to internal URLs (e.g., localhost).
|
||||||
|
|
||||||
|
Defaults to true, which allows internal requests.
|
||||||
|
|
||||||
## Incoming Mail {#incoming_mail}
|
## Incoming Mail {#incoming_mail}
|
||||||
|
|
||||||
### Email OAuth {#email_oauth}
|
### Email OAuth {#email_oauth}
|
||||||
|
|||||||
+4
-4
@@ -338,13 +338,13 @@ LANGUAGES = [
|
|||||||
|
|
||||||
## Building the documentation
|
## Building the documentation
|
||||||
|
|
||||||
The documentation is built using material-mkdocs, see their [documentation](https://squidfunk.github.io/mkdocs-material/reference/).
|
The documentation is built using Zensical, see their [documentation](https://zensical.org/docs/).
|
||||||
If you want to build the documentation locally, this is how you do it:
|
If you want to build the documentation locally, this is how you do it:
|
||||||
|
|
||||||
1. Build the documentation
|
1. Build the documentation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ uv run mkdocs build --config-file mkdocs.yml
|
$ uv run zensical build
|
||||||
```
|
```
|
||||||
|
|
||||||
_alternatively..._
|
_alternatively..._
|
||||||
@@ -355,10 +355,10 @@ If you want to build the documentation locally, this is how you do it:
|
|||||||
something.
|
something.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ uv run mkdocs serve
|
$ uv run zensical serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the Docker image
|
## Building the Docker image {#docker_build}
|
||||||
|
|
||||||
The docker image is primarily built by the GitHub actions workflow, but
|
The docker image is primarily built by the GitHub actions workflow, but
|
||||||
it can be faster when developing to build and tag an image locally.
|
it can be faster when developing to build and tag an image locally.
|
||||||
|
|||||||
+8
-2
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: FAQs
|
||||||
|
---
|
||||||
|
|
||||||
# Frequently Asked Questions
|
# Frequently Asked Questions
|
||||||
|
|
||||||
## _What's the general plan for Paperless-ngx?_
|
## _What's the general plan for Paperless-ngx?_
|
||||||
@@ -63,8 +67,10 @@ elsewhere. Here are a couple notes about that.
|
|||||||
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.
|
rather than its file extensions. However, files processed via the
|
||||||
|
consumption directory will be rejected if they have a file extension that
|
||||||
|
not supported by any of the available parsers.
|
||||||
|
|
||||||
## _Will paperless-ngx run on Raspberry Pi?_
|
## _Will paperless-ngx run on Raspberry Pi?_
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Home
|
||||||
|
---
|
||||||
|
|
||||||
<div class="grid-left" markdown>
|
<div class="grid-left" markdown>
|
||||||
{.index-logo}
|
{.index-logo}
|
||||||
{.index-logo}
|
{.index-logo}
|
||||||
|
|||||||
+253
-310
@@ -1,52 +1,77 @@
|
|||||||
## Installation
|
---
|
||||||
|
title: Setup
|
||||||
|
---
|
||||||
|
|
||||||
You can go multiple routes to setup and run Paperless:
|
# Installation
|
||||||
|
|
||||||
- [Use the script to setup a Docker install](#docker_script)
|
!!! tip "Quick Start"
|
||||||
- [Use the Docker compose templates](#docker)
|
|
||||||
- [Build the Docker image yourself](#docker_build)
|
|
||||||
- [Install Paperless-ngx directly on your system manually ("bare metal")](#bare_metal)
|
|
||||||
- A user-maintained list of commercial hosting providers can be found [in the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects)
|
|
||||||
|
|
||||||
The Docker routes are quick & easy. These are the recommended routes.
|
|
||||||
This configures all the stuff from the above automatically so that it
|
|
||||||
just works and uses sensible defaults for all configuration options.
|
|
||||||
Here you find a cheat-sheet for docker beginners: [CLI
|
|
||||||
Basics](https://www.sehn.tech/refs/devops-with-docker/)
|
|
||||||
|
|
||||||
The bare metal route is complicated to setup but makes it easier should
|
|
||||||
you want to contribute some code back. You need to configure and run the
|
|
||||||
above mentioned components yourself.
|
|
||||||
|
|
||||||
### Use the Installation Script {#docker_script}
|
|
||||||
|
|
||||||
Paperless provides an interactive installation script to setup a Docker Compose
|
|
||||||
installation. The script asks for a couple configuration options, and will then create the
|
|
||||||
necessary configuration files, pull the docker image, start Paperless-ngx and create your superuser
|
|
||||||
account. The script essentially automatically performs the steps described in [Docker setup](#docker).
|
|
||||||
|
|
||||||
1. Make sure that Docker and Docker Compose are [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
|
||||||
|
|
||||||
2. Download and run the installation script:
|
|
||||||
|
|
||||||
|
If you just want Paperless-ngx running quickly, use our installation script:
|
||||||
```shell-session
|
```shell-session
|
||||||
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
||||||
```
|
```
|
||||||
|
_If piping into a shell directly from the internet makes you nervous, inspect [the script](https://github.com/paperless-ngx/paperless-ngx/blob/main/install-paperless-ngx.sh) first!_
|
||||||
|
|
||||||
!!! note
|
## Overview
|
||||||
|
|
||||||
macOS users will need to install [gnu-sed](https://formulae.brew.sh/formula/gnu-sed) with support
|
Choose the installation route that best fits your setup:
|
||||||
for running as `sed` as well as [wget](https://formulae.brew.sh/formula/wget).
|
|
||||||
|
|
||||||
### Use Docker Compose {#docker}
|
| Route | Best for | Effort |
|
||||||
|
| ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------ |
|
||||||
|
| [Installation script](#docker_script) | Fastest first-time setup with guided prompts (recommended for most users) | Low |
|
||||||
|
| [Docker Compose templates](#docker) | Manual control over compose files and settings | Medium |
|
||||||
|
| [Bare metal](#bare_metal) | Advanced setups, packaging, and development-adjacent workflows | High |
|
||||||
|
| [Hosted providers (wiki)](https://github.com/paperless-ngx/paperless-ngx/wiki/Related-Projects#hosting-providers) | Managed hosting options maintained by the community — check details carefully | Varies |
|
||||||
|
|
||||||
1. Make sure that Docker and Docker Compose are [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
For most users, Docker is the best option. It is faster to set up,
|
||||||
|
easier to maintain, and ships with sensible defaults.
|
||||||
|
|
||||||
2. Go to the [/docker/compose directory on the project
|
The bare-metal route gives you more control, but it requires manual
|
||||||
|
installation and operation of all components. It is usually best suited
|
||||||
|
for advanced users and contributors.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
Because [superuser](usage.md#superusers) accounts have full access to all objects and documents, you may want to create a separate user account for daily use,
|
||||||
|
or "downgrade" your superuser account to a normal user account after setup.
|
||||||
|
|
||||||
|
## Installation Script {#docker_script}
|
||||||
|
|
||||||
|
Paperless-ngx provides an interactive script for Docker Compose setups.
|
||||||
|
It asks a few configuration questions, then creates the required files,
|
||||||
|
pulls the image, starts the containers, and creates your [superuser](usage.md#superusers)
|
||||||
|
account. In short, it automates the [Docker Compose setup](#docker) described below.
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose must be [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
||||||
|
- macOS users will need [GNU sed](https://formulae.brew.sh/formula/gnu-sed) with support for running as `sed` as well as [wget](https://formulae.brew.sh/formula/wget).
|
||||||
|
|
||||||
|
#### Run the installation script
|
||||||
|
|
||||||
|
```shell-session
|
||||||
|
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After installation
|
||||||
|
|
||||||
|
Paperless-ngx should be available at `http://127.0.0.1:8000` (or similar,
|
||||||
|
depending on your configuration) and you will be able to login with the
|
||||||
|
credentials you provided during the installation script.
|
||||||
|
|
||||||
|
## Docker Compose Install {#docker}
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose must be [installed](https://docs.docker.com/engine/install/){:target="\_blank"}.
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
1. Go to the [/docker/compose directory on the project
|
||||||
page](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose){:target="\_blank"}
|
page](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose){:target="\_blank"}
|
||||||
and download one of the `docker-compose.*.yml` files, depending on which database backend
|
and download one `docker-compose.*.yml` file for your preferred
|
||||||
you want to use. Place the files in a local directory and rename it `docker-compose.yml`. Download the
|
database backend. Save it in a local directory as `docker-compose.yml`.
|
||||||
`docker-compose.env` file and the `.env` file as well in the same directory.
|
Also download `docker-compose.env` and `.env` into that same directory.
|
||||||
|
|
||||||
If you want to enable optional support for Office and other documents, download a
|
If you want to enable optional support for Office and other documents, download a
|
||||||
file with `-tika` in the file name.
|
file with `-tika` in the file name.
|
||||||
@@ -56,15 +81,16 @@ account. The script essentially automatically performs the steps described in [D
|
|||||||
For new installations, it is recommended to use PostgreSQL as the
|
For new installations, it is recommended to use PostgreSQL as the
|
||||||
database backend.
|
database backend.
|
||||||
|
|
||||||
3. Modify `docker-compose.yml` as needed. For example, you may want to change the paths to the
|
2. Modify `docker-compose.yml` as needed. For example, you may want to
|
||||||
consumption, media etc. directories to use 'bind mounts'.
|
change the paths for `consume`, `media`, and other directories to
|
||||||
|
use bind mounts.
|
||||||
Find the line that specifies where to mount the directory, e.g.:
|
Find the line that specifies where to mount the directory, e.g.:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- ./consume:/usr/src/paperless/consume
|
- ./consume:/usr/src/paperless/consume
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace the part _before_ the colon with a local directory of your choice:
|
Replace the part _before_ the colon with your local directory:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
- /home/jonaswinkler/paperless-inbox:/usr/src/paperless/consume
|
||||||
@@ -78,38 +104,15 @@ account. The script essentially automatically performs the steps described in [D
|
|||||||
- 8010:8000
|
- 8010:8000
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rootless**
|
3. Modify `docker-compose.env` with any configuration options you need.
|
||||||
|
|
||||||
!!! warning
|
|
||||||
|
|
||||||
It is currently not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`.
|
|
||||||
|
|
||||||
If you want to run Paperless as a rootless container, you will need
|
|
||||||
to do the following in your `docker-compose.yml`:
|
|
||||||
|
|
||||||
- set the `user` running the container to map to the `paperless`
|
|
||||||
user in the container. This value (`user_id` below), should be
|
|
||||||
the same id that `USERMAP_UID` and `USERMAP_GID` are set to in
|
|
||||||
the next step. See `USERMAP_UID` and `USERMAP_GID`
|
|
||||||
[here](configuration.md#docker).
|
|
||||||
|
|
||||||
Your entry for Paperless should contain something like:
|
|
||||||
|
|
||||||
> ```
|
|
||||||
> webserver:
|
|
||||||
> image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
|
||||||
> user: <user_id>
|
|
||||||
> ```
|
|
||||||
|
|
||||||
4. Modify `docker-compose.env` with any configuration options you'd like.
|
|
||||||
See the [configuration documentation](configuration.md) for all options.
|
See the [configuration documentation](configuration.md) for all options.
|
||||||
|
|
||||||
You may also need to set `USERMAP_UID` and `USERMAP_GID` to
|
You may also need to set `USERMAP_UID` and `USERMAP_GID` to
|
||||||
the uid and gid of your user on the host system. Use `id -u` and
|
the UID and GID of your user on the host system. Use `id -u` and
|
||||||
`id -g` to get these. This ensures that both the container and the host
|
`id -g` to get these values. This ensures both the container and the
|
||||||
user have write access to the consumption directory. If your UID
|
host user can write to the consumption directory. If your UID and
|
||||||
and GID on the host system is 1000 (the default for the first normal
|
GID are `1000` (the default for the first normal user on many
|
||||||
user on most systems), it will work out of the box without any
|
systems), this usually works out of the box without
|
||||||
modifications. Run `id "username"` to check.
|
modifications. Run `id "username"` to check.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -118,79 +121,55 @@ account. The script essentially automatically performs the steps described in [D
|
|||||||
appending `_FILE` to configuration values. For example [`PAPERLESS_DBUSER`](configuration.md#PAPERLESS_DBUSER)
|
appending `_FILE` to configuration values. For example [`PAPERLESS_DBUSER`](configuration.md#PAPERLESS_DBUSER)
|
||||||
can be set using `PAPERLESS_DBUSER_FILE=/var/run/secrets/password.txt`.
|
can be set using `PAPERLESS_DBUSER_FILE=/var/run/secrets/password.txt`.
|
||||||
|
|
||||||
!!! warning
|
4. Run `docker compose pull`. This pulls the image from the GitHub container registry
|
||||||
|
by default, but you can pull from Docker Hub by changing the `image`
|
||||||
Some file systems such as NFS network shares don't support file
|
|
||||||
system notifications with `inotify`. When storing the consumption
|
|
||||||
directory on such a file system, paperless will not pick up new
|
|
||||||
files with the default configuration. You will need to use
|
|
||||||
[`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING), which will disable inotify. See
|
|
||||||
[here](configuration.md#polling).
|
|
||||||
|
|
||||||
5. Run `docker compose pull`. This will pull the image from the GitHub container registry
|
|
||||||
by default but you can change the image to pull from Docker Hub by changing the `image`
|
|
||||||
line to `image: paperlessngx/paperless-ngx:latest`.
|
line to `image: paperlessngx/paperless-ngx:latest`.
|
||||||
|
|
||||||
6. Run `docker compose up -d`. This will create and start the necessary containers.
|
5. Run `docker compose up -d`. This will create and start the necessary containers.
|
||||||
|
|
||||||
7. Congratulations! Your Paperless-ngx instance should now be accessible at `http://127.0.0.1:8000`
|
#### After installation
|
||||||
(or similar, depending on your configuration). When you first access the web interface, you will be
|
|
||||||
prompted to create a superuser account.
|
|
||||||
|
|
||||||
### Build the Docker image yourself {#docker_build}
|
Your Paperless-ngx instance should now be accessible at
|
||||||
|
`http://127.0.0.1:8000` (or similar, depending on your configuration).
|
||||||
|
When you first access the web interface, you will be prompted to create
|
||||||
|
a [superuser](usage.md#superusers) account.
|
||||||
|
|
||||||
1. Clone the entire repository of paperless:
|
#### Optional Advanced Compose Configurations {#advanced_compose data-toc-label="Advanced Compose Configurations"}
|
||||||
|
|
||||||
```shell-session
|
**Rootless**
|
||||||
git clone https://github.com/paperless-ngx/paperless-ngx
|
|
||||||
```
|
|
||||||
|
|
||||||
The main branch always reflects the latest stable version.
|
!!! warning
|
||||||
|
|
||||||
2. Copy one of the `docker/compose/docker-compose.*.yml` to
|
It is not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`.
|
||||||
`docker-compose.yml` in the root folder, depending on which database
|
|
||||||
backend you want to use. Copy `docker-compose.env` into the project
|
|
||||||
root as well.
|
|
||||||
|
|
||||||
3. In the `docker-compose.yml` file, find the line that instructs
|
If you want to run Paperless as a rootless container, set `user:` in `docker-compose.yml` to the UID and GID of your host user (use `id -u` and `id -g` to find these values). The container process starts directly as that user with no internal privilege remapping:
|
||||||
Docker Compose to pull the paperless image from Docker Hub:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
webserver:
|
webserver:
|
||||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
```
|
user: '1000:1000'
|
||||||
|
```
|
||||||
|
|
||||||
and replace it with a line that instructs Docker Compose to build
|
Do not combine this with `USERMAP_UID` or `USERMAP_GID`, which are intended for the non-rootless case described in step 3.
|
||||||
the image from the current working directory instead:
|
|
||||||
|
|
||||||
```yaml
|
**File systems without inotify support (e.g. NFS)**
|
||||||
webserver:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Follow the [Docker setup](#docker) above except when asked to run
|
Some file systems, such as NFS network shares, don't support file system
|
||||||
`docker compose pull` to pull the image, run
|
notifications with `inotify`. When the consumption directory is on such a
|
||||||
|
file system, Paperless-ngx will not pick up new files with the default
|
||||||
|
configuration. Use [`PAPERLESS_CONSUMER_POLLING`](configuration.md#PAPERLESS_CONSUMER_POLLING)
|
||||||
|
to enable polling and disable inotify. See [here](configuration.md#polling).
|
||||||
|
|
||||||
```shell-session
|
## Bare Metal Install {#bare_metal}
|
||||||
docker compose build
|
|
||||||
```
|
|
||||||
|
|
||||||
instead to build the image.
|
#### Prerequisites
|
||||||
|
|
||||||
### Bare Metal Route {#bare_metal}
|
- Paperless runs on Linux only, Windows is not supported.
|
||||||
|
- Python 3 is required with versions 3.10 - 3.12 currently supported. Newer versions may work, but some dependencies may not be fully compatible.
|
||||||
|
|
||||||
Paperless runs on linux only. The following procedure has been tested on
|
#### Installation
|
||||||
a minimal installation of Debian/Buster, which is the current stable
|
|
||||||
release at the time of writing. Windows is not and will never be
|
|
||||||
supported.
|
|
||||||
|
|
||||||
Paperless requires Python 3. At this time, 3.10 - 3.12 are tested versions.
|
1. Install dependencies. Paperless requires the following packages:
|
||||||
Newer versions may work, but some dependencies may not fully support newer versions.
|
|
||||||
Support for older Python versions may be dropped as they reach end of life or as newer versions
|
|
||||||
are released, dependency support is confirmed, etc.
|
|
||||||
|
|
||||||
1. Install dependencies. Paperless requires the following packages.
|
|
||||||
|
|
||||||
- `python3`
|
- `python3`
|
||||||
- `python3-pip`
|
- `python3-pip`
|
||||||
@@ -254,8 +233,8 @@ are released, dependency support is confirmed, etc.
|
|||||||
|
|
||||||
2. Install `redis` >= 6.0 and configure it to start automatically.
|
2. Install `redis` >= 6.0 and configure it to start automatically.
|
||||||
|
|
||||||
3. Optional. Install `postgresql` and configure a database, user and
|
3. Optional: Install `postgresql` and configure a database, user, and
|
||||||
password for paperless. If you do not wish to use PostgreSQL,
|
password for Paperless-ngx. If you do not wish to use PostgreSQL,
|
||||||
MariaDB and SQLite are available as well.
|
MariaDB and SQLite are available as well.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -264,61 +243,60 @@ are released, dependency support is confirmed, etc.
|
|||||||
extension](https://code.djangoproject.com/wiki/JSON1Extension) is
|
extension](https://code.djangoproject.com/wiki/JSON1Extension) is
|
||||||
enabled. This is usually the case, but not always.
|
enabled. This is usually the case, but not always.
|
||||||
|
|
||||||
4. Create a system user with a new home folder under which you wish
|
4. Create a system user with a new home folder in which you want
|
||||||
to run paperless.
|
to run Paperless-ngx.
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
adduser paperless --system --home /opt/paperless --group
|
adduser paperless --system --home /opt/paperless --group
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Get the release archive from
|
5. Download a release archive from
|
||||||
<https://github.com/paperless-ngx/paperless-ngx/releases> for example with
|
<https://github.com/paperless-ngx/paperless-ngx/releases>. For example:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
curl -O -L https://github.com/paperless-ngx/paperless-ngx/releases/download/v1.10.2/paperless-ngx-v1.10.2.tar.xz
|
curl -O -L https://github.com/paperless-ngx/paperless-ngx/releases/download/vX.Y.Z/paperless-ngx-vX.Y.Z.tar.xz
|
||||||
```
|
```
|
||||||
|
|
||||||
Extract the archive with
|
Extract the archive with
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
tar -xf paperless-ngx-v1.10.2.tar.xz
|
tar -xf paperless-ngx-vX.Y.Z.tar.xz
|
||||||
```
|
```
|
||||||
|
|
||||||
and copy the contents to the
|
and copy the contents to the home directory of the user you created
|
||||||
home folder of the user you created before (`/opt/paperless`).
|
earlier (`/opt/paperless`).
|
||||||
|
|
||||||
Optional: If you cloned the git repo, you will have to
|
Optional: If you cloned the Git repository, you will need to
|
||||||
compile the frontend yourself, see [here](development.md#front-end-development)
|
compile the frontend yourself. See [here](development.md#front-end-development)
|
||||||
and use the `build` step, not `serve`.
|
and use the `build` step, not `serve`.
|
||||||
|
|
||||||
6. Configure paperless. See [configuration](configuration.md) for details.
|
6. Configure Paperless-ngx. See [configuration](configuration.md) 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-ngx running are:
|
||||||
paperless running are:
|
|
||||||
|
|
||||||
- [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) should point to your redis server, such as
|
- [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) should point to your Redis server, such as
|
||||||
<redis://localhost:6379>.
|
`redis://localhost:6379`.
|
||||||
- [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) optional, and should be one of `postgres`,
|
- [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) is optional, and should be one of `postgres`,
|
||||||
`mariadb`, or `sqlite`
|
`mariadb`, or `sqlite`
|
||||||
- [`PAPERLESS_DBHOST`](configuration.md#PAPERLESS_DBHOST) should be the hostname on which your
|
- [`PAPERLESS_DBHOST`](configuration.md#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
|
||||||
password as necessary.
|
password as necessary.
|
||||||
- [`PAPERLESS_CONSUMPTION_DIR`](configuration.md#PAPERLESS_CONSUMPTION_DIR) should point to a folder which
|
- [`PAPERLESS_CONSUMPTION_DIR`](configuration.md#PAPERLESS_CONSUMPTION_DIR) should point to the folder
|
||||||
paperless should watch for documents. You might want to have
|
that Paperless-ngx should watch for incoming documents.
|
||||||
this somewhere else. Likewise, [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) and
|
Likewise, [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) and
|
||||||
[`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) define where paperless stores its data.
|
[`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) define where Paperless-ngx stores its data.
|
||||||
If you like, you can point both to the same directory.
|
If needed, these can point to the same directory.
|
||||||
- [`PAPERLESS_SECRET_KEY`](configuration.md#PAPERLESS_SECRET_KEY) should be a random sequence of
|
- [`PAPERLESS_SECRET_KEY`](configuration.md#PAPERLESS_SECRET_KEY) should be a random sequence of
|
||||||
characters. It's used for authentication. Failure to do so
|
characters. It's used for authentication. Failure to do so
|
||||||
allows third parties to forge authentication credentials.
|
allows third parties to forge authentication credentials.
|
||||||
- [`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) if you are behind a reverse proxy. This should
|
- Set [`PAPERLESS_URL`](configuration.md#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.md) for more
|
[configuration](configuration.md) for more
|
||||||
information.
|
information.
|
||||||
|
|
||||||
Many more adjustments can be made to paperless, especially the OCR
|
You can make many more adjustments, especially for OCR.
|
||||||
part. The following options are recommended for everyone:
|
The following options are recommended for most users:
|
||||||
|
|
||||||
- Set [`PAPERLESS_OCR_LANGUAGE`](configuration.md#PAPERLESS_OCR_LANGUAGE) to the language most of your
|
- Set [`PAPERLESS_OCR_LANGUAGE`](configuration.md#PAPERLESS_OCR_LANGUAGE) to the language most of your
|
||||||
documents are written in.
|
documents are written in.
|
||||||
@@ -326,17 +304,16 @@ are released, dependency support is confirmed, etc.
|
|||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
Ensure your Redis instance [is secured](https://redis.io/docs/getting-started/#securing-redis).
|
Ensure your Redis instance [is secured](https://redis.io/docs/latest/operate/oss_and_stack/management/security/).
|
||||||
|
|
||||||
7. Create the following directories if they are missing:
|
7. Create the following directories if they do not already exist:
|
||||||
|
|
||||||
- `/opt/paperless/media`
|
- `/opt/paperless/media`
|
||||||
- `/opt/paperless/data`
|
- `/opt/paperless/data`
|
||||||
- `/opt/paperless/consume`
|
- `/opt/paperless/consume`
|
||||||
|
|
||||||
Adjust as necessary if you configured different folders.
|
Adjust these paths if you configured different folders.
|
||||||
Ensure that the paperless user has write permissions for every one
|
Then verify that the `paperless` user has write permissions:
|
||||||
of these folders with
|
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
ls -l -d /opt/paperless/media
|
ls -l -d /opt/paperless/media
|
||||||
@@ -350,45 +327,44 @@ are released, dependency support is confirmed, etc.
|
|||||||
sudo chown paperless:paperless /opt/paperless/consume
|
sudo chown paperless:paperless /opt/paperless/consume
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Install python requirements from the `requirements.txt` file.
|
8. Install Python dependencies from `requirements.txt`.
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
sudo -Hu paperless pip3 install -r requirements.txt
|
sudo -Hu paperless pip3 install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
This will install all python dependencies in the home directory of
|
This will install all Python dependencies in the home directory of
|
||||||
the new paperless user.
|
the new paperless user.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
It is up to you if you wish to use a virtual environment or not for the Python
|
You can use a virtual environment if you prefer. If you do,
|
||||||
dependencies. This is an alternative to the above and may require adjusting
|
you may need to adjust the example scripts for your virtual
|
||||||
the example scripts to utilize the virtual environment paths
|
environment paths.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
If you use modern Python tooling, such as `uv`, installation will not include
|
If you use modern Python tooling, such as `uv`, installation will not include
|
||||||
dependencies for Postgres or Mariadb. You can select those extras with `--extra <EXTRA>`
|
dependencies for PostgreSQL or MariaDB. You can select those
|
||||||
or all with `--all-extras`
|
extras with `--extra <EXTRA>`, or install all extras with
|
||||||
|
`--all-extras`.
|
||||||
|
|
||||||
9. Go to `/opt/paperless/src`, and execute the following command:
|
9. Go to `/opt/paperless/src` and execute the following command:
|
||||||
|
|
||||||
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
When you first access the web interface you will be prompted to create a superuser account.
|
10. Optional: Test that Paperless-ngx is working by running
|
||||||
|
|
||||||
10. Optional: Test that paperless is working by executing
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Manually starts the webserver
|
# Manually starts the webserver
|
||||||
sudo -Hu paperless python3 manage.py runserver
|
sudo -Hu paperless python3 manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
and pointing your browser to http://localhost:8000 if
|
Then point your browser to `http://localhost:8000` if
|
||||||
accessing from the same devices on which paperless is installed.
|
accessing from the same device on which Paperless-ngx is installed.
|
||||||
If accessing from another machine, set up systemd services. You may need
|
If accessing from another machine, set up systemd services. You may need
|
||||||
to set `PAPERLESS_DEBUG=true` in order for the development server to work
|
to set `PAPERLESS_DEBUG=true` in order for the development server to work
|
||||||
normally in your browser.
|
normally in your browser.
|
||||||
@@ -396,23 +372,24 @@ are released, dependency support is confirmed, etc.
|
|||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
This is a development server which should not be used in production.
|
This is a development server which should not be used in production.
|
||||||
It is not audited for security and performance is inferior to
|
It is not audited for security, and performance is inferior to
|
||||||
production ready web servers.
|
production-ready web servers.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
|
|
||||||
This will not start the consumer. Paperless does this in a separate
|
This will not start the consumer. Paperless does this in a separate
|
||||||
process.
|
process.
|
||||||
|
|
||||||
11. Setup systemd services to run paperless automatically. You may use
|
11. Set up systemd services to run Paperless-ngx automatically. You may use
|
||||||
the service definition files included in the `scripts` folder as a
|
the service definition files included in the `scripts` folder as a
|
||||||
starting point.
|
starting point.
|
||||||
|
|
||||||
Paperless needs the `webserver` script to run the webserver, the
|
Paperless needs:
|
||||||
`consumer` script to watch the input folder, `taskqueue` for the
|
|
||||||
background workers used to handle things like document consumption
|
- The `webserver` script to run the webserver.
|
||||||
and the `scheduler` script to run tasks such as email checking at
|
- The `consumer` script to watch the input folder.
|
||||||
certain times .
|
- The `taskqueue` script for background workers (document consumption, etc.).
|
||||||
|
- The `scheduler` script for periodic tasks such as email checking.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
@@ -421,9 +398,9 @@ are released, dependency support is confirmed, etc.
|
|||||||
`Require=paperless-webserver.socket` in the `webserver` script
|
`Require=paperless-webserver.socket` in the `webserver` script
|
||||||
and configure `granian` to listen on port 80 (set `GRANIAN_PORT`).
|
and configure `granian` to listen on port 80 (set `GRANIAN_PORT`).
|
||||||
|
|
||||||
These services rely on redis and optionally the database server, but
|
These services rely on Redis and optionally the database server, but
|
||||||
don't need to be started in any particular order. The example files
|
don't need to be started in any particular order. The example files
|
||||||
depend on redis being started. If you use a database server, you
|
depend on Redis being started. If you use a database server, you
|
||||||
should add additional dependencies.
|
should add additional dependencies.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
@@ -433,18 +410,15 @@ are released, dependency support is confirmed, etc.
|
|||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
If celery won't start (check with
|
If Celery won't start, check
|
||||||
`sudo systemctl status paperless-task-queue.service` for
|
`sudo systemctl status paperless-task-queue.service` for
|
||||||
paperless-task-queue.service and paperless-scheduler.service
|
`paperless-task-queue.service` and `paperless-scheduler.service`.
|
||||||
) you need to change the path in the files. Example:
|
You may need to change the path in the files. Example:
|
||||||
`ExecStart=/opt/paperless/.local/bin/celery --app paperless worker --loglevel INFO`
|
`ExecStart=/opt/paperless/.local/bin/celery --app paperless worker --loglevel INFO`
|
||||||
|
|
||||||
12. Optional: Install a samba server and make the consumption folder
|
12. Configure ImageMagick to allow processing of PDF documents. Most
|
||||||
available as a network share.
|
|
||||||
|
|
||||||
13. Configure ImageMagick to allow processing of PDF documents. Most
|
|
||||||
distributions have this disabled by default, since PDF documents can
|
distributions have this disabled by default, since PDF documents can
|
||||||
contain malware. If you don't do this, paperless will fall back to
|
contain malware. If you don't do this, Paperless-ngx will fall back to
|
||||||
Ghostscript for certain steps such as thumbnail generation.
|
Ghostscript for certain steps such as thumbnail generation.
|
||||||
|
|
||||||
Edit `/etc/ImageMagick-6/policy.xml` and adjust
|
Edit `/etc/ImageMagick-6/policy.xml` and adjust
|
||||||
@@ -459,32 +433,38 @@ are released, dependency support is confirmed, etc.
|
|||||||
<policy domain="coder" rights="read|write" pattern="PDF" />
|
<policy domain="coder" rights="read|write" pattern="PDF" />
|
||||||
```
|
```
|
||||||
|
|
||||||
14. Optional: Install the
|
**Optional: Install the [jbig2enc](https://ocrmypdf.readthedocs.io/en/latest/jbig2.html) encoder.**
|
||||||
[jbig2enc](https://ocrmypdf.readthedocs.io/en/latest/jbig2.html)
|
This will reduce the size of generated PDF documents. You'll most likely need to compile this yourself, because this
|
||||||
encoder. This will reduce the size of generated PDF documents.
|
software has been patented until around 2017 and binary packages are not available for most distributions.
|
||||||
You'll most likely need to compile this by yourself, because this
|
|
||||||
software has been patented until around 2017 and binary packages are
|
|
||||||
not available for most distributions.
|
|
||||||
|
|
||||||
15. Optional: If using the NLTK machine learning processing (see
|
**Optional: download the NLTK data**
|
||||||
[`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) for details),
|
If using the NLTK machine-learning processing (see [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) for details),
|
||||||
download the NLTK data for the Snowball
|
download the NLTK data for the Snowball Stemmer, Stopwords and Punkt tokenizer to `/usr/share/nltk_data`. Refer to the [NLTK
|
||||||
Stemmer, Stopwords and Punkt tokenizer to `/usr/share/nltk_data`. Refer to the [NLTK
|
instructions](https://www.nltk.org/data.html) for details on how to download the data.
|
||||||
instructions](https://www.nltk.org/data.html) for details on how to
|
|
||||||
download the data.
|
|
||||||
|
|
||||||
# Migrating to Paperless-ngx
|
#### After installation
|
||||||
|
|
||||||
Migration is possible both from Paperless-ng or directly from the
|
Your Paperless-ngx instance should now be accessible at `http://localhost:8000` (or similar, depending on your configuration).
|
||||||
'original' Paperless.
|
When you first access the web interface you will be prompted to create a [superuser](usage.md#superusers) account.
|
||||||
|
|
||||||
## Migrating from Paperless-ng
|
## Build the Docker image yourself {#docker_build data-toc-label="Building the Docker image"}
|
||||||
|
|
||||||
Paperless-ngx is meant to be a drop-in replacement for Paperless-ng and
|
Building the Docker image yourself is typically used for development, but it can also be used for production
|
||||||
thus upgrading should be trivial for most users, especially when using
|
if you want to customize the image. See [Building the Docker image](development.md#docker_build) in the
|
||||||
docker. However, as with any major change, it is recommended to take a
|
development documentation.
|
||||||
|
|
||||||
|
## Migrating to Paperless-ngx
|
||||||
|
|
||||||
|
You can migrate to Paperless-ngx from Paperless-ng or from the original
|
||||||
|
Paperless project.
|
||||||
|
|
||||||
|
<h3 id="migration_ng">Migrating from Paperless-ng</h3>
|
||||||
|
|
||||||
|
Paperless-ngx is meant to be a drop-in replacement for Paperless-ng, and
|
||||||
|
upgrading should be trivial for most users, especially when using
|
||||||
|
Docker. However, as with any major change, it is recommended to take a
|
||||||
full backup first. Once you are ready, simply change the docker image to
|
full backup first. Once you are ready, simply change the docker image to
|
||||||
point to the new source. E.g. if using Docker Compose, edit
|
point to the new source. For example, if using Docker Compose, edit
|
||||||
`docker-compose.yml` and change:
|
`docker-compose.yml` and change:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -497,66 +477,64 @@ to
|
|||||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
and then run `docker compose up -d` which will pull the new image
|
and then run `docker compose up -d`, which will pull the new image and
|
||||||
recreate the container. That's it!
|
recreate the container. That's it.
|
||||||
|
|
||||||
Users who installed with the bare-metal route should also update their
|
Users who installed with the bare-metal route should also update their
|
||||||
Git clone to point to `https://github.com/paperless-ngx/paperless-ngx`,
|
Git clone to point to `https://github.com/paperless-ngx/paperless-ngx`,
|
||||||
e.g. using the command
|
for example using:
|
||||||
`git remote set-url origin https://github.com/paperless-ngx/paperless-ngx`
|
`git remote set-url origin https://github.com/paperless-ngx/paperless-ngx`
|
||||||
and then pull the latest version.
|
and then pull the latest version.
|
||||||
|
|
||||||
## Migrating from Paperless
|
<h3 id="migration_paperless">Migrating from Paperless</h3>
|
||||||
|
|
||||||
At its core, paperless-ngx is still paperless and fully compatible.
|
At its core, Paperless-ngx is still Paperless and fully compatible.
|
||||||
However, some things have changed under the hood, so you need to adapt
|
However, some things have changed under the hood, so you need to adapt
|
||||||
your setup depending on how you installed paperless.
|
your setup depending on how you installed Paperless.
|
||||||
|
|
||||||
This setup describes how to update an existing paperless Docker
|
This section describes how to update an existing Paperless Docker
|
||||||
installation. The important things to keep in mind are as follows:
|
installation. Keep these points in mind:
|
||||||
|
|
||||||
- Read the [changelog](changelog.md) and
|
- Read the [changelog](changelog.md) and
|
||||||
take note of breaking changes.
|
take note of breaking changes.
|
||||||
- You should decide if you want to stick with SQLite or want to
|
- Decide whether to stay on SQLite or migrate to PostgreSQL.
|
||||||
migrate your database to PostgreSQL. See [documentation](#sqlite_to_psql)
|
Both work fine with Paperless-ngx.
|
||||||
for details on
|
However, if you already have a database server running
|
||||||
how to move your data from SQLite to PostgreSQL. Both work fine with
|
for other services, you might as well use it for Paperless as well.
|
||||||
paperless. However, if you already have a database server running
|
- The task scheduler of Paperless, which is used to execute periodic
|
||||||
for other services, you might as well use it for paperless as well.
|
|
||||||
- The task scheduler of paperless, which is used to execute periodic
|
|
||||||
tasks such as email checking and maintenance, requires a
|
tasks such as email checking and maintenance, requires a
|
||||||
[redis](https://redis.io/) message broker instance. The
|
[Redis](https://redis.io/) message broker instance. The
|
||||||
Docker Compose route takes care of that.
|
Docker Compose route takes care of that.
|
||||||
- The layout of the folder structure for your documents and data
|
- The layout of the folder structure for your documents and data
|
||||||
remains the same, so you can just plug your old docker volumes into
|
remains the same, so you can plug your old Docker volumes into
|
||||||
paperless-ngx and expect it to find everything where it should be.
|
paperless-ngx and expect it to find everything where it should be.
|
||||||
|
|
||||||
Migration to paperless-ngx is then performed in a few simple steps:
|
Migration to Paperless-ngx is then performed in a few simple steps:
|
||||||
|
|
||||||
1. Stop paperless.
|
1. Stop Paperless.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/current/paperless
|
cd /path/to/current/paperless
|
||||||
docker compose down
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Do a backup for two purposes: If something goes wrong, you still
|
2. Create a backup for two reasons: if something goes wrong, you still
|
||||||
have your data. Second, if you don't like paperless-ngx, you can
|
have your data; and if you don't like paperless-ngx, you can
|
||||||
switch back to paperless.
|
switch back to Paperless.
|
||||||
|
|
||||||
3. Download the latest release of paperless-ngx. You can either go with
|
3. Download the latest release of Paperless-ngx. You can either use
|
||||||
the Docker Compose files from
|
the Docker Compose files from
|
||||||
[here](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
[here](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
||||||
or clone the repository to build the image yourself (see
|
or clone the repository to build the image yourself (see
|
||||||
[above](#docker_build)). You can
|
[development docs](development.md#docker_build)). You can either replace your current paperless
|
||||||
either replace your current paperless folder or put paperless-ngx in
|
folder or put Paperless-ngx in
|
||||||
a different location.
|
a different location.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
Paperless-ngx includes a `.env` file. This will set the project name
|
Paperless-ngx includes a `.env` file. This will set the project name
|
||||||
for docker compose to `paperless`, which will also define the name
|
for Docker Compose to `paperless`, which will also define the
|
||||||
of the volumes by paperless-ngx. However, if you experience that
|
volume names created by Paperless-ngx. However, if you notice that
|
||||||
paperless-ngx is not using your old paperless volumes, verify the
|
paperless-ngx is not using your old paperless volumes, verify the
|
||||||
names of your volumes with
|
names of your volumes with
|
||||||
|
|
||||||
@@ -572,10 +550,10 @@ 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](#docker) details on
|
See [Docker setup](#docker) for details on
|
||||||
which edits are advised.
|
which edits are recommended.
|
||||||
|
|
||||||
6. [Update paperless.](administration.md#updating)
|
6. Follow the update procedure in [Update paperless](administration.md#updating).
|
||||||
|
|
||||||
7. In order to find your existing documents with the new search
|
7. In order to find your existing documents with the new search
|
||||||
feature, you need to invoke a one-time operation that will create
|
feature, you need to invoke a one-time operation that will create
|
||||||
@@ -586,136 +564,99 @@ Migration to paperless-ngx is then performed in a few simple steps:
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will migrate your database and create the search index. After
|
This will migrate your database and create the search index. After
|
||||||
that, paperless will take care of maintaining the index by itself.
|
that, Paperless-ngx will maintain the index automatically.
|
||||||
|
|
||||||
8. Start paperless-ngx.
|
8. Start Paperless-ngx.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
This will run paperless in the background and automatically start it
|
This will run Paperless-ngx in the background and automatically start it
|
||||||
on system boot.
|
on system boot.
|
||||||
|
|
||||||
9. Paperless installed a permanent redirect to `admin/` in your
|
9. Paperless may have installed a permanent redirect to `admin/` in your
|
||||||
browser. This redirect is still in place and prevents access to the
|
browser. This redirect is still in place and prevents access to the
|
||||||
new UI. Clear your browsing cache in order to fix this.
|
new UI. Clear your browser cache to fix this.
|
||||||
|
|
||||||
10. Optionally, follow the instructions below to migrate your existing
|
10. Optionally, follow the instructions below to migrate your existing
|
||||||
data to PostgreSQL.
|
data to PostgreSQL.
|
||||||
|
|
||||||
## Migrating from LinuxServer.io Docker Image
|
<h3 id="migration_lsio">Migrating from LinuxServer.io Docker Image</h3>
|
||||||
|
|
||||||
As with any upgrades and large changes, it is highly recommended to
|
As with any upgrade or large change, it is highly recommended to
|
||||||
create a backup before starting. This assumes the image was running
|
create a backup before starting. This assumes the image was running
|
||||||
using Docker Compose, but the instructions are translatable to Docker
|
using Docker Compose, but the instructions are translatable to Docker
|
||||||
commands as well.
|
commands as well.
|
||||||
|
|
||||||
1. Stop and remove the paperless container
|
1. Stop and remove the Paperless container.
|
||||||
2. If using an external database, stop the container
|
2. If using an external database, stop that container.
|
||||||
3. Update Redis configuration
|
3. Update Redis configuration.
|
||||||
|
|
||||||
1. If `REDIS_URL` is already set, change it to [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS)
|
1. If `REDIS_URL` is already set, change it to [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS)
|
||||||
and continue to step 4.
|
and continue to step 4.
|
||||||
|
|
||||||
1. Otherwise, in the `docker-compose.yml` add a new service for
|
1. Otherwise, add a new Redis service in `docker-compose.yml`,
|
||||||
Redis, following [the example compose
|
following [the example compose
|
||||||
files](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
files](https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose)
|
||||||
|
|
||||||
1. Set the environment variable [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) so it points to
|
1. Set the environment variable [`PAPERLESS_REDIS`](configuration.md#PAPERLESS_REDIS) so it points to
|
||||||
the new Redis container
|
the new Redis container.
|
||||||
|
|
||||||
4. Update user mapping
|
4. Update user mapping.
|
||||||
|
|
||||||
1. If set, change the environment variable `PUID` to `USERMAP_UID`
|
1. If set, change the environment variable `PUID` to `USERMAP_UID`.
|
||||||
|
|
||||||
1. If set, change the environment variable `PGID` to `USERMAP_GID`
|
1. If set, change the environment variable `PGID` to `USERMAP_GID`.
|
||||||
|
|
||||||
5. Update configuration paths
|
5. Update configuration paths.
|
||||||
|
|
||||||
1. Set the environment variable [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) to `/config`
|
1. Set the environment variable [`PAPERLESS_DATA_DIR`](configuration.md#PAPERLESS_DATA_DIR) to `/config`.
|
||||||
|
|
||||||
6. Update media paths
|
6. Update media paths.
|
||||||
|
|
||||||
1. Set the environment variable [`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) to
|
1. Set the environment variable [`PAPERLESS_MEDIA_ROOT`](configuration.md#PAPERLESS_MEDIA_ROOT) to
|
||||||
`/data/media`
|
`/data/media`.
|
||||||
|
|
||||||
7. Update timezone
|
7. Update timezone.
|
||||||
|
|
||||||
1. Set the environment variable [`PAPERLESS_TIME_ZONE`](configuration.md#PAPERLESS_TIME_ZONE) to the same
|
1. Set the environment variable [`PAPERLESS_TIME_ZONE`](configuration.md#PAPERLESS_TIME_ZONE) to the same
|
||||||
value as `TZ`
|
value as `TZ`.
|
||||||
|
|
||||||
8. Modify the `image:` to point to
|
8. Modify `image:` to point to
|
||||||
`ghcr.io/paperless-ngx/paperless-ngx:latest` or a specific version
|
`ghcr.io/paperless-ngx/paperless-ngx:latest` or a specific version
|
||||||
if preferred.
|
if preferred.
|
||||||
9. Start the containers as before, using `docker compose`.
|
9. Start the containers as before, using `docker compose`.
|
||||||
|
|
||||||
## Moving data from SQLite to PostgreSQL or MySQL/MariaDB {#sqlite_to_psql}
|
## Running Paperless-ngx on less powerful devices {#less-powerful-devices data-toc-label="Less Powerful Devices"}
|
||||||
|
|
||||||
The best way to migrate between database types is to perform an [export](administration.md#exporter) and then
|
Paperless runs on Raspberry Pi. Some tasks can be slow on lower-powered
|
||||||
[import](administration.md#importer) into a clean installation of Paperless-ngx.
|
hardware, but a few settings can improve performance:
|
||||||
|
|
||||||
## Moving back to Paperless
|
|
||||||
|
|
||||||
Lets say you migrated to Paperless-ngx and used it for a while, but
|
|
||||||
decided that you don't like it and want to move back (If you do, send
|
|
||||||
me a mail about what part you didn't like!), you can totally do that
|
|
||||||
with a few simple steps.
|
|
||||||
|
|
||||||
Paperless-ngx modified the database schema slightly, however, these
|
|
||||||
changes can be reverted while keeping your current data, so that your
|
|
||||||
current data will be compatible with original Paperless. Thumbnails
|
|
||||||
were also changed from PNG to WEBP format and will need to be
|
|
||||||
re-generated.
|
|
||||||
|
|
||||||
Execute this:
|
|
||||||
|
|
||||||
```shell-session
|
|
||||||
$ cd /path/to/paperless
|
|
||||||
$ docker compose run --rm webserver migrate documents 0023
|
|
||||||
```
|
|
||||||
|
|
||||||
Or without docker:
|
|
||||||
|
|
||||||
```shell-session
|
|
||||||
$ cd /path/to/paperless/src
|
|
||||||
$ python3 manage.py migrate documents 0023
|
|
||||||
```
|
|
||||||
|
|
||||||
After regenerating thumbnails, you'll need to clear your cookies
|
|
||||||
(Paperless-ngx comes with updated dependencies that do cookie-processing
|
|
||||||
differently) and probably your cache as well.
|
|
||||||
|
|
||||||
# Considerations for less powerful devices {#less-powerful-devices}
|
|
||||||
|
|
||||||
Paperless runs on Raspberry Pi. However, some things are rather slow on
|
|
||||||
the Pi and configuring some options in paperless can help improve
|
|
||||||
performance immensely:
|
|
||||||
|
|
||||||
- Stick with SQLite to save some resources. See [troubleshooting](troubleshooting.md#log-reports-creating-paperlesstask-failed)
|
- Stick with SQLite to save some resources. See [troubleshooting](troubleshooting.md#log-reports-creating-paperlesstask-failed)
|
||||||
if you encounter issues with SQLite locking.
|
if you encounter issues with SQLite locking.
|
||||||
- If you do not need the filesystem-based consumer, consider disabling it
|
- If you do not need the filesystem-based consumer, consider disabling it
|
||||||
entirely by setting [`PAPERLESS_CONSUMER_DISABLE`](configuration.md#PAPERLESS_CONSUMER_DISABLE) to `true`.
|
entirely by setting [`PAPERLESS_CONSUMER_DISABLE`](configuration.md#PAPERLESS_CONSUMER_DISABLE) to `true`.
|
||||||
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that paperless will
|
- Consider setting [`PAPERLESS_OCR_PAGES`](configuration.md#PAPERLESS_OCR_PAGES) to 1, so that Paperless
|
||||||
only OCR the first page of your documents. In most cases, this page
|
OCRs only the first page of your documents. In most cases, this page
|
||||||
contains enough information to be able to find it.
|
contains enough information to be able to find it.
|
||||||
- [`PAPERLESS_TASK_WORKERS`](configuration.md#PAPERLESS_TASK_WORKERS) and [`PAPERLESS_THREADS_PER_WORKER`](configuration.md#PAPERLESS_THREADS_PER_WORKER) are
|
- [`PAPERLESS_TASK_WORKERS`](configuration.md#PAPERLESS_TASK_WORKERS) and [`PAPERLESS_THREADS_PER_WORKER`](configuration.md#PAPERLESS_THREADS_PER_WORKER) are
|
||||||
configured to use all cores. The Raspberry Pi models 3 and up have 4
|
configured to use all cores. The Raspberry Pi models 3 and up have 4
|
||||||
cores, meaning that paperless will use 2 workers and 2 threads per
|
cores, meaning that Paperless will use 2 workers and 2 threads per
|
||||||
worker. This may result in sluggish response times during
|
worker. This may result in sluggish response times during
|
||||||
consumption, so you might want to lower these settings (example: 2
|
consumption, so you might want to lower these settings (example: 2
|
||||||
workers and 1 thread to always have some computing power left for
|
workers and 1 thread to always have some computing power left for
|
||||||
other tasks).
|
other tasks).
|
||||||
- Keep [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) at its default value `skip` and consider
|
- Keep [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) at its default value `skip` and consider
|
||||||
OCR'ing your documents before feeding them into paperless. Some
|
OCRing your documents before feeding them into Paperless. Some
|
||||||
scanners are able to do this!
|
scanners are able to do this!
|
||||||
- Set [`PAPERLESS_OCR_SKIP_ARCHIVE_FILE`](configuration.md#PAPERLESS_OCR_SKIP_ARCHIVE_FILE) to `with_text` to skip archive
|
- Set [`PAPERLESS_OCR_SKIP_ARCHIVE_FILE`](configuration.md#PAPERLESS_OCR_SKIP_ARCHIVE_FILE) to `with_text` to skip archive
|
||||||
file generation for already ocr'ed documents, or `always` to skip it
|
file generation for already OCRed documents, or `always` to skip it
|
||||||
for all documents.
|
for all documents.
|
||||||
- If you want to perform OCR on the device, consider using
|
- If you want to perform OCR on the device, consider using
|
||||||
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
|
`PAPERLESS_OCR_CLEAN=none`. This will speed up OCR times and use
|
||||||
less memory at the expense of slightly worse OCR results.
|
less memory at the expense of slightly worse OCR results.
|
||||||
- If using docker, consider setting [`PAPERLESS_WEBSERVER_WORKERS`](configuration.md#PAPERLESS_WEBSERVER_WORKERS) to 1. This will save some memory.
|
- If using Docker, consider setting [`PAPERLESS_WEBSERVER_WORKERS`](configuration.md#PAPERLESS_WEBSERVER_WORKERS) to 1. This will save some memory.
|
||||||
- Consider setting [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) to false, to disable the
|
- Consider setting [`PAPERLESS_ENABLE_NLTK`](configuration.md#PAPERLESS_ENABLE_NLTK) to false, to disable the
|
||||||
more advanced language processing, which can take more memory and
|
more advanced language processing, which can take more memory and
|
||||||
processing time.
|
processing time.
|
||||||
@@ -727,17 +668,19 @@ For details, refer to [configuration](configuration.md).
|
|||||||
Updating the
|
Updating the
|
||||||
[automatic matching algorithm](advanced_usage.md#automatic-matching) takes quite a bit of time. However, the update mechanism
|
[automatic matching algorithm](advanced_usage.md#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
|
||||||
the task by changing the date and time of the next run to today/now.
|
the task by changing the date and time of the next run to today/now.
|
||||||
|
|
||||||
The actual matching of the algorithm is fast and works on Raspberry Pi
|
The actual matching of the algorithm is fast and works on Raspberry Pi
|
||||||
as well as on any other device.
|
as well as on any other device.
|
||||||
|
|
||||||
# Using nginx as a reverse proxy {#nginx}
|
## Additional considerations
|
||||||
|
|
||||||
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx) for user-maintained documentation of using nginx with Paperless-ngx.
|
**Using a reverse proxy with Paperless-ngx**
|
||||||
|
|
||||||
# Enhancing security {#security}
|
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-a-Reverse-Proxy-with-Paperless-ngx#nginx) for user-maintained documentation on using nginx with Paperless-ngx.
|
||||||
|
|
||||||
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-Security-Tools-with-Paperless-ngx) for user-maintained documentation of how to configure security tools like Fail2ban with Paperless-ngx.
|
**Enhancing security**
|
||||||
|
|
||||||
|
Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Using-Security-Tools-with-Paperless-ngx) for user-maintained documentation on configuring security tools like Fail2ban with Paperless-ngx.
|
||||||
+24
-3
@@ -1,4 +1,8 @@
|
|||||||
# Usage Overview
|
---
|
||||||
|
title: Basic Usage
|
||||||
|
---
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
Paperless-ngx is an application that manages your personal documents. With
|
Paperless-ngx is an application that manages your personal documents. With
|
||||||
the (optional) help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), Paperless-ngx transforms your unwieldy
|
the (optional) help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), Paperless-ngx transforms your unwieldy
|
||||||
@@ -344,6 +348,11 @@ permissions can be granted to limit access to certain parts of the UI (and corre
|
|||||||
|
|
||||||
Superusers can access all parts of the front and backend application as well as any and all objects. Superuser status can only be granted by another superuser.
|
Superusers can access all parts of the front and backend application as well as any and all objects. Superuser status can only be granted by another superuser.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
|
||||||
|
Because superuser accounts can see all objects and documents, you may want to use a regular account for day-to-day use. Additional superuser accounts can
|
||||||
|
be created via [cli](administration.md#create-superuser) or granted superuser status from an existing superuser account.
|
||||||
|
|
||||||
#### Admin Status
|
#### Admin Status
|
||||||
|
|
||||||
Admin status (Django 'staff status') grants access to viewing the paperless logs and the system status dialog
|
Admin status (Django 'staff status') grants access to viewing the paperless logs and the system status dialog
|
||||||
@@ -372,7 +381,7 @@ still have "object-level" permissions.
|
|||||||
| StoragePath | Add, edit, delete or view Storage Paths. |
|
| StoragePath | Add, edit, delete or view Storage Paths. |
|
||||||
| Tag | Add, edit, delete or view Tags. |
|
| Tag | Add, edit, delete or view Tags. |
|
||||||
| UISettings | Add, edit, delete or view the UI settings that are used by the web app.<br/>:warning: **Users that will access the web UI must be granted at least _View_ permissions.** |
|
| UISettings | Add, edit, delete or view the UI settings that are used by the web app.<br/>:warning: **Users that will access the web UI must be granted at least _View_ permissions.** |
|
||||||
| User | Add, edit, delete or view Users. |
|
| User | Add, edit, delete or view other user accounts via Settings > Users & Groups and `/api/users/`. These permissions are not needed for users to edit their own profile via "My Profile" or `/api/profile/`. |
|
||||||
| Workflow | Add, edit, delete or view Workflows.<br/>Note that Workflows are global; all users who can access workflows see the same set. Workflows have other permission implications — see [Workflow permissions](#workflow-permissions). |
|
| Workflow | Add, edit, delete or view Workflows.<br/>Note that Workflows are global; all users who can access workflows see the same set. Workflows have other permission implications — see [Workflow permissions](#workflow-permissions). |
|
||||||
|
|
||||||
#### Detailed Explanation of Object Permissions {#object-permissions}
|
#### Detailed Explanation of Object Permissions {#object-permissions}
|
||||||
@@ -383,6 +392,8 @@ still have "object-level" permissions.
|
|||||||
| View | Confers the ability to view (not edit) a document, tag, etc.<br/>Users without 'view' (or higher) permissions will be shown _'Private'_ in place of the object name for example when viewing a document with a tag for which the user doesn't have permissions. |
|
| View | Confers the ability to view (not edit) a document, tag, etc.<br/>Users without 'view' (or higher) permissions will be shown _'Private'_ in place of the object name for example when viewing a document with a tag for which the user doesn't have permissions. |
|
||||||
| Edit | Confers the ability to edit (and view) a document, tag, etc. |
|
| Edit | Confers the ability to edit (and view) a document, tag, etc. |
|
||||||
|
|
||||||
|
For related metadata such as tags, correspondents, document types, and storage paths, object visibility and document assignment are intentionally distinct. A user may still retain or submit a known object ID when editing a document even if that related object is displayed as _Private_ or omitted from search and selection results. This allows documents to preserve existing assignments that the current user cannot necessarily inspect in detail.
|
||||||
|
|
||||||
### Password reset
|
### Password reset
|
||||||
|
|
||||||
In order to enable the password reset feature you will need to setup an SMTP backend, see
|
In order to enable the password reset feature you will need to setup an SMTP backend, see
|
||||||
@@ -443,6 +454,10 @@ flowchart TD
|
|||||||
'Updated'
|
'Updated'
|
||||||
trigger(s)"}
|
trigger(s)"}
|
||||||
|
|
||||||
|
scheduled{"Documents
|
||||||
|
matching
|
||||||
|
trigger(s)"}
|
||||||
|
|
||||||
A[New Document] --> consumption
|
A[New Document] --> consumption
|
||||||
consumption --> |Yes| C[Workflow Actions Run]
|
consumption --> |Yes| C[Workflow Actions Run]
|
||||||
consumption --> |No| D
|
consumption --> |No| D
|
||||||
@@ -455,6 +470,11 @@ flowchart TD
|
|||||||
updated --> |Yes| J[Workflow Actions Run]
|
updated --> |Yes| J[Workflow Actions Run]
|
||||||
updated --> |No| K
|
updated --> |No| K
|
||||||
J --> K[Document Saved]
|
J --> K[Document Saved]
|
||||||
|
L[Scheduled Task Check<br/>hourly at :05] --> M[Get All Scheduled Triggers]
|
||||||
|
M --> scheduled
|
||||||
|
scheduled --> |Yes| N[Workflow Actions Run]
|
||||||
|
scheduled --> |No| O[Document Saved]
|
||||||
|
N --> O
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Filters {#workflow-trigger-filters}
|
#### Filters {#workflow-trigger-filters}
|
||||||
@@ -552,7 +572,8 @@ applied. You can use the following placeholders in the template with any trigger
|
|||||||
- `{{added_day}}`: added day
|
- `{{added_day}}`: added day
|
||||||
- `{{added_time}}`: added time in HH:MM format
|
- `{{added_time}}`: added time in HH:MM format
|
||||||
- `{{original_filename}}`: original file name without extension
|
- `{{original_filename}}`: original file name without extension
|
||||||
- `{{filename}}`: current file name without extension
|
- `{{filename}}`: current file name without extension (for "added" workflows this may not be final yet, you can use `{{original_filename}}`)
|
||||||
|
- `{{doc_title}}`: current document title
|
||||||
|
|
||||||
The following placeholders are only available for "added" or "updated" triggers
|
The following placeholders are only available for "added" or "updated" triggers
|
||||||
|
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ fi
|
|||||||
# of the provided folder
|
# of the provided folder
|
||||||
if [[ -n $DATABASE_FOLDER ]] ; then
|
if [[ -n $DATABASE_FOLDER ]] ; then
|
||||||
if [[ "$DATABASE_BACKEND" == "postgres" ]] ; then
|
if [[ "$DATABASE_BACKEND" == "postgres" ]] ; then
|
||||||
sed -i "s#- pgdata:/var/lib/postgresql/data#- $DATABASE_FOLDER:/var/lib/postgresql/data#g" docker-compose.yml
|
sed -i "s#- pgdata:/var/lib/postgresql#- $DATABASE_FOLDER:/var/lib/postgresql#g" docker-compose.yml
|
||||||
sed -i "/^\s*pgdata:/d" docker-compose.yml
|
sed -i "/^\s*pgdata:/d" docker-compose.yml
|
||||||
elif [[ "$DATABASE_BACKEND" == "mariadb" ]]; then
|
elif [[ "$DATABASE_BACKEND" == "mariadb" ]]; then
|
||||||
sed -i "s#- dbdata:/var/lib/mysql#- $DATABASE_FOLDER:/var/lib/mysql#g" docker-compose.yml
|
sed -i "s#- dbdata:/var/lib/mysql#- $DATABASE_FOLDER:/var/lib/mysql#g" docker-compose.yml
|
||||||
|
|||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
site_name: Paperless-ngx
|
|
||||||
theme:
|
|
||||||
name: material
|
|
||||||
logo: assets/logo.svg
|
|
||||||
font:
|
|
||||||
text: Roboto
|
|
||||||
code: Roboto Mono
|
|
||||||
palette:
|
|
||||||
# Palette toggle for automatic mode
|
|
||||||
- media: "(prefers-color-scheme)"
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-auto
|
|
||||||
name: Switch to light mode
|
|
||||||
# Palette toggle for light mode
|
|
||||||
- media: "(prefers-color-scheme: light)"
|
|
||||||
scheme: default
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-7
|
|
||||||
name: Switch to dark mode
|
|
||||||
# Palette toggle for dark mode
|
|
||||||
- media: "(prefers-color-scheme: dark)"
|
|
||||||
scheme: slate
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-4
|
|
||||||
name: Switch to system preference
|
|
||||||
features:
|
|
||||||
- navigation.tabs
|
|
||||||
- navigation.top
|
|
||||||
- toc.integrate
|
|
||||||
- content.code.annotate
|
|
||||||
icon:
|
|
||||||
repo: fontawesome/brands/github
|
|
||||||
favicon: assets/favicon.png
|
|
||||||
repo_url: https://github.com/paperless-ngx/paperless-ngx
|
|
||||||
repo_name: paperless-ngx/paperless-ngx
|
|
||||||
edit_uri: blob/main/docs/
|
|
||||||
extra_css:
|
|
||||||
- assets/extra.css
|
|
||||||
markdown_extensions:
|
|
||||||
- attr_list
|
|
||||||
- md_in_html
|
|
||||||
- def_list
|
|
||||||
- admonition
|
|
||||||
- tables
|
|
||||||
- pymdownx.highlight:
|
|
||||||
anchor_linenums: true
|
|
||||||
- pymdownx.superfences
|
|
||||||
- pymdownx.inlinehilite
|
|
||||||
- pymdownx.snippets
|
|
||||||
- pymdownx.tilde
|
|
||||||
- footnotes
|
|
||||||
- pymdownx.superfences:
|
|
||||||
custom_fences:
|
|
||||||
- name: mermaid
|
|
||||||
class: mermaid
|
|
||||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
|
||||||
- pymdownx.emoji:
|
|
||||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
|
||||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
|
||||||
strict: true
|
|
||||||
nav:
|
|
||||||
- index.md
|
|
||||||
- setup.md
|
|
||||||
- 'Basic Usage': usage.md
|
|
||||||
- configuration.md
|
|
||||||
- administration.md
|
|
||||||
- advanced_usage.md
|
|
||||||
- 'REST API': api.md
|
|
||||||
- development.md
|
|
||||||
- 'FAQs': faq.md
|
|
||||||
- troubleshooting.md
|
|
||||||
- changelog.md
|
|
||||||
copyright: Copyright © 2016 - 2023 Daniel Quinn, Jonas Winkler, and the Paperless-ngx team
|
|
||||||
extra:
|
|
||||||
social:
|
|
||||||
- icon: fontawesome/brands/github
|
|
||||||
link: https://github.com/paperless-ngx/paperless-ngx
|
|
||||||
- icon: fontawesome/brands/docker
|
|
||||||
link: https://hub.docker.com/r/paperlessngx/paperless-ngx
|
|
||||||
- icon: material/chat
|
|
||||||
link: https://matrix.to/#/#paperless:matrix.org
|
|
||||||
plugins:
|
|
||||||
- search
|
|
||||||
- glightbox:
|
|
||||||
skip_classes:
|
|
||||||
- no-lightbox
|
|
||||||
+16
-16
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "2.19.2"
|
version = "2.20.14"
|
||||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -17,7 +17,7 @@ classifiers = [
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"babel>=2.17",
|
"babel>=2.17",
|
||||||
"bleach~=6.2.0",
|
"bleach~=6.3.0",
|
||||||
"celery[redis]~=5.5.1",
|
"celery[redis]~=5.5.1",
|
||||||
"channels~=4.2",
|
"channels~=4.2",
|
||||||
"channels-redis~=4.2",
|
"channels-redis~=4.2",
|
||||||
@@ -26,8 +26,8 @@ dependencies = [
|
|||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
"django~=5.2.5",
|
"django~=5.2.5",
|
||||||
"django-allauth[mfa,socialaccount]~=65.4.0",
|
"django-allauth[mfa,socialaccount]~=65.12.1",
|
||||||
"django-auditlog~=3.2.1",
|
"django-auditlog~=3.3.0",
|
||||||
"django-cachalot~=2.8.0",
|
"django-cachalot~=2.8.0",
|
||||||
"django-celery-results~=2.6.0",
|
"django-celery-results~=2.6.0",
|
||||||
"django-compression-middleware~=0.5.0",
|
"django-compression-middleware~=0.5.0",
|
||||||
@@ -41,7 +41,7 @@ dependencies = [
|
|||||||
"djangorestframework~=3.16",
|
"djangorestframework~=3.16",
|
||||||
"djangorestframework-guardian~=0.4.0",
|
"djangorestframework-guardian~=0.4.0",
|
||||||
"drf-spectacular~=0.28",
|
"drf-spectacular~=0.28",
|
||||||
"drf-spectacular-sidecar~=2025.9.1",
|
"drf-spectacular-sidecar~=2025.10.1",
|
||||||
"drf-writable-nested~=0.7.1",
|
"drf-writable-nested~=0.7.1",
|
||||||
"filelock~=3.20.0",
|
"filelock~=3.20.0",
|
||||||
"flower~=2.0.1",
|
"flower~=2.0.1",
|
||||||
@@ -52,17 +52,18 @@ dependencies = [
|
|||||||
"jinja2~=3.1.5",
|
"jinja2~=3.1.5",
|
||||||
"langdetect~=1.0.9",
|
"langdetect~=1.0.9",
|
||||||
"nltk~=3.9.1",
|
"nltk~=3.9.1",
|
||||||
"ocrmypdf~=16.11.0",
|
"ocrmypdf~=16.12.0",
|
||||||
"pathvalidate~=3.3.1",
|
"pathvalidate~=3.3.1",
|
||||||
"pdf2image~=1.17.0",
|
"pdf2image~=1.17.0",
|
||||||
"python-dateutil~=2.9.0",
|
"python-dateutil~=2.9.0",
|
||||||
"python-dotenv~=1.1.0",
|
"python-dotenv~=1.2.1",
|
||||||
"python-gnupg~=0.5.4",
|
"python-gnupg~=0.5.4",
|
||||||
"python-ipware~=3.0.0",
|
"python-ipware~=3.0.0",
|
||||||
"python-magic~=0.4.27",
|
"python-magic~=0.4.27",
|
||||||
"pyzbar~=0.1.9",
|
"pyzbar~=0.1.9",
|
||||||
"rapidfuzz~=3.14.0",
|
"rapidfuzz~=3.14.0",
|
||||||
"redis[hiredis]~=5.2.1",
|
"redis[hiredis]~=5.2.1",
|
||||||
|
"regex>=2025.9.18",
|
||||||
"scikit-learn~=1.7.0",
|
"scikit-learn~=1.7.0",
|
||||||
"setproctitle~=1.3.4",
|
"setproctitle~=1.3.4",
|
||||||
"tika-client~=0.10.0",
|
"tika-client~=0.10.0",
|
||||||
@@ -77,10 +78,10 @@ optional-dependencies.mariadb = [
|
|||||||
"mysqlclient~=2.2.7",
|
"mysqlclient~=2.2.7",
|
||||||
]
|
]
|
||||||
optional-dependencies.postgres = [
|
optional-dependencies.postgres = [
|
||||||
"psycopg[c,pool]==3.2.9",
|
"psycopg[c,pool]==3.2.12",
|
||||||
# Direct dependency for proper resolution of the pre-built wheels
|
# Direct dependency for proper resolution of the pre-built wheels
|
||||||
"psycopg-c==3.2.9",
|
"psycopg-c==3.2.12",
|
||||||
"psycopg-pool==3.2.6",
|
"psycopg-pool==3.2.7",
|
||||||
]
|
]
|
||||||
optional-dependencies.webserver = [
|
optional-dependencies.webserver = [
|
||||||
"granian[uvloop]~=2.5.1",
|
"granian[uvloop]~=2.5.1",
|
||||||
@@ -95,8 +96,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
docs = [
|
docs = [
|
||||||
"mkdocs-glightbox~=0.5.1",
|
"zensical>=0.0.21",
|
||||||
"mkdocs-material~=9.6.4",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
testing = [
|
testing = [
|
||||||
@@ -115,7 +115,7 @@ testing = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
lint = [
|
lint = [
|
||||||
"pre-commit~=4.3.0",
|
"pre-commit~=4.4.0",
|
||||||
"pre-commit-uv~=4.2.0",
|
"pre-commit-uv~=4.2.0",
|
||||||
"ruff~=0.14.0",
|
"ruff~=0.14.0",
|
||||||
]
|
]
|
||||||
@@ -150,8 +150,8 @@ environments = [
|
|||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
# Markers are chosen to select these almost exclusively when building the Docker image
|
# Markers are chosen to select these almost exclusively when building the Docker image
|
||||||
psycopg-c = [
|
psycopg-c = [
|
||||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
|
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
|
||||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
|
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-bookworm-3.2.12/psycopg_c-3.2.12-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
|
||||||
]
|
]
|
||||||
zxing-cpp = [
|
zxing-cpp = [
|
||||||
{ url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
|
{ url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
|
||||||
@@ -237,7 +237,7 @@ lint.isort.force-single-line = true
|
|||||||
|
|
||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
write-changes = true
|
write-changes = true
|
||||||
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober"
|
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish"
|
||||||
skip = "src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/documents/tests/samples/*,*.po,*.json"
|
skip = "src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/documents/tests/samples/*,*.po,*.json"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|||||||
+380
-331
File diff suppressed because it is too large.
Load diff
+30
-30
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "2.19.2",
|
"version": "2.20.14",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
@@ -11,17 +11,17 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^20.2.6",
|
"@angular/cdk": "^20.2.13",
|
||||||
"@angular/common": "~20.3.2",
|
"@angular/common": "~20.3.15",
|
||||||
"@angular/compiler": "~20.3.2",
|
"@angular/compiler": "~20.3.15",
|
||||||
"@angular/core": "~20.3.2",
|
"@angular/core": "~20.3.15",
|
||||||
"@angular/forms": "~20.3.2",
|
"@angular/forms": "~20.3.15",
|
||||||
"@angular/localize": "~20.3.2",
|
"@angular/localize": "~20.3.15",
|
||||||
"@angular/platform-browser": "~20.3.2",
|
"@angular/platform-browser": "~20.3.15",
|
||||||
"@angular/platform-browser-dynamic": "~20.3.2",
|
"@angular/platform-browser-dynamic": "~20.3.15",
|
||||||
"@angular/router": "~20.3.2",
|
"@angular/router": "~20.3.15",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
||||||
"@ng-select/ng-select": "^20.2.2",
|
"@ng-select/ng-select": "^20.7.0",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.8",
|
"bootstrap": "^5.3.8",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"ng2-pdf-viewer": "^10.4.0",
|
"ng2-pdf-viewer": "^10.4.0",
|
||||||
"ngx-bootstrap-icons": "^1.9.3",
|
"ngx-bootstrap-icons": "^1.9.3",
|
||||||
"ngx-color": "^10.1.0",
|
"ngx-color": "^10.1.0",
|
||||||
"ngx-cookie-service": "^20.1.0",
|
"ngx-cookie-service": "^20.1.1",
|
||||||
"ngx-device-detector": "^10.1.0",
|
"ngx-device-detector": "^10.1.0",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^17.0.1",
|
"ngx-ui-tour-ng-bootstrap": "^17.0.1",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
@@ -42,33 +42,33 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^20.0.0",
|
"@angular-builders/custom-webpack": "^20.0.0",
|
||||||
"@angular-builders/jest": "^20.0.0",
|
"@angular-builders/jest": "^20.0.0",
|
||||||
"@angular-devkit/core": "^20.3.3",
|
"@angular-devkit/core": "^20.3.13",
|
||||||
"@angular-devkit/schematics": "^20.3.3",
|
"@angular-devkit/schematics": "^20.3.13",
|
||||||
"@angular-eslint/builder": "20.3.0",
|
"@angular-eslint/builder": "20.6.0",
|
||||||
"@angular-eslint/eslint-plugin": "20.3.0",
|
"@angular-eslint/eslint-plugin": "20.6.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "20.3.0",
|
"@angular-eslint/eslint-plugin-template": "20.6.0",
|
||||||
"@angular-eslint/schematics": "20.3.0",
|
"@angular-eslint/schematics": "20.6.0",
|
||||||
"@angular-eslint/template-parser": "20.3.0",
|
"@angular-eslint/template-parser": "20.6.0",
|
||||||
"@angular/build": "^20.3.3",
|
"@angular/build": "^20.3.13",
|
||||||
"@angular/cli": "~20.3.3",
|
"@angular/cli": "~20.3.13",
|
||||||
"@angular/compiler-cli": "~20.3.2",
|
"@angular/compiler-cli": "~20.3.15",
|
||||||
"@codecov/webpack-plugin": "^1.9.1",
|
"@codecov/webpack-plugin": "^1.9.1",
|
||||||
"@playwright/test": "^1.55.1",
|
"@playwright/test": "^1.57.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.6.1",
|
"@types/node": "^24.10.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||||
"@typescript-eslint/parser": "^8.45.0",
|
"@typescript-eslint/parser": "^8.48.1",
|
||||||
"@typescript-eslint/utils": "^8.45.0",
|
"@typescript-eslint/utils": "^8.48.1",
|
||||||
"eslint": "^9.36.0",
|
"eslint": "^9.39.1",
|
||||||
"jest": "30.2.0",
|
"jest": "30.2.0",
|
||||||
"jest-environment-jsdom": "^30.2.0",
|
"jest-environment-jsdom": "^30.2.0",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
"jest-preset-angular": "^15.0.2",
|
"jest-preset-angular": "^15.0.3",
|
||||||
"jest-websocket-mock": "^2.5.0",
|
"jest-websocket-mock": "^2.5.0",
|
||||||
"prettier-plugin-organize-imports": "^4.3.0",
|
"prettier-plugin-organize-imports": "^4.3.0",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"webpack": "^5.102.0"
|
"webpack": "^5.103.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.17.1",
|
"packageManager": "pnpm@10.17.1",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|||||||
Generated
+1266
-1322
File diff suppressed because it is too large.
Load diff
@@ -145,6 +145,10 @@ HTMLCanvasElement.prototype.getContext = <
|
|||||||
typeof HTMLCanvasElement.prototype.getContext
|
typeof HTMLCanvasElement.prototype.getContext
|
||||||
>jest.fn()
|
>jest.fn()
|
||||||
|
|
||||||
|
if (!HTMLElement.prototype.scrollTo) {
|
||||||
|
HTMLElement.prototype.scrollTo = jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
jest.mock('uuid', () => ({
|
jest.mock('uuid', () => ({
|
||||||
v4: jest.fn(() =>
|
v4: jest.fn(() =>
|
||||||
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char: string) => {
|
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char: string) => {
|
||||||
|
|||||||
@@ -19,13 +19,18 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card bg-light">
|
<div class="card bg-light">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title">
|
<div class="card-title d-flex align-items-center">
|
||||||
<h6>
|
<h6 class="mb-0">
|
||||||
{{option.title}}
|
{{option.title}}
|
||||||
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
|
||||||
<i-bs name="info-circle"></i-bs>
|
|
||||||
</a>
|
|
||||||
</h6>
|
</h6>
|
||||||
|
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
|
||||||
|
<i-bs name="info-circle"></i-bs>
|
||||||
|
</a>
|
||||||
|
@if (isSet(option.key)) {
|
||||||
|
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
|
||||||
|
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-n3">
|
<div class="mb-n3">
|
||||||
@switch (option.type) {
|
@switch (option.type) {
|
||||||
|
|||||||
@@ -144,4 +144,18 @@ describe('ConfigComponent', () => {
|
|||||||
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||||
expect(initSpy).toHaveBeenCalled()
|
expect(initSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should reset option to null', () => {
|
||||||
|
component.configForm.patchValue({ output_type: OutputTypeConfig.PDF_A })
|
||||||
|
expect(component.isSet('output_type')).toBeTruthy()
|
||||||
|
component.resetOption('output_type')
|
||||||
|
expect(component.configForm.get('output_type').value).toBeNull()
|
||||||
|
expect(component.isSet('output_type')).toBeFalsy()
|
||||||
|
component.configForm.patchValue({ app_title: 'Test Title' })
|
||||||
|
component.resetOption('app_title')
|
||||||
|
expect(component.configForm.get('app_title').value).toBeNull()
|
||||||
|
component.configForm.patchValue({ barcodes_enabled: true })
|
||||||
|
component.resetOption('barcodes_enabled')
|
||||||
|
expect(component.configForm.get('barcodes_enabled').value).toBeNull()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -208,4 +208,12 @@ export class ConfigComponent
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isSet(key: string): boolean {
|
||||||
|
return this.configForm.get(key).value != null
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetOption(key: string) {
|
||||||
|
this.configForm.get(key).setValue(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,23 @@
|
|||||||
i18n-title
|
i18n-title
|
||||||
info="Review the log files for the application and for email checking."
|
info="Review the log files for the application and for email checking."
|
||||||
i18n-info>
|
i18n-info>
|
||||||
<div class="form-check form-switch">
|
<div class="input-group input-group-sm align-items-center">
|
||||||
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
<div class="input-group input-group-sm me-3">
|
||||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
<span class="input-group-text text-muted" i18n>Show</span>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
min="100"
|
||||||
|
step="100"
|
||||||
|
[(ngModel)]="limit"
|
||||||
|
(ngModelChange)="onLimitChange($event)"
|
||||||
|
style="width: 100px;">
|
||||||
|
<span class="input-group-text text-muted" i18n>lines</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mt-1">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||||
|
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
@@ -27,16 +41,23 @@
|
|||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
<div #logContainer class="bg-dark text-light font-monospace log-container p-3" (scroll)="onScroll()">
|
||||||
|
@if (loading && !logFiles.length) {
|
||||||
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
|
|
||||||
@if (loading && logFiles.length) {
|
|
||||||
<div>
|
<div>
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
<ng-container i18n>Loading...</ng-container>
|
<ng-container i18n>Loading...</ng-container>
|
||||||
</div>
|
</div>
|
||||||
}
|
} @else {
|
||||||
@for (log of logs; track $index) {
|
@for (log of logs; track log) {
|
||||||
<p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
|
<p class="m-0 p-0" [ngClass]="'log-entry-' + log.level">{{log.message}}</p>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-secondary jump-to-bottom position-fixed bottom-0 end-0 m-5"
|
||||||
|
[class.visible]="showJumpToBottom"
|
||||||
|
(click)="scrollToBottom()"
|
||||||
|
>
|
||||||
|
↓ <span i18n>Jump to bottom</span>
|
||||||
|
</button>
|
||||||
@@ -16,11 +16,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.log-container {
|
.log-container {
|
||||||
overflow-y: scroll;
|
height: calc(100vh - 190px);
|
||||||
height: calc(100vh - 200px);
|
overflow-y: auto;
|
||||||
top: 70px;
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jump-to-bottom {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 120ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-to-bottom.visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
CdkVirtualScrollViewport,
|
||||||
|
ScrollingModule,
|
||||||
|
} from '@angular/cdk/scrolling'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
@@ -38,6 +43,9 @@ describe('LogsComponent', () => {
|
|||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
LogsComponent,
|
LogsComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
|
CommonModule,
|
||||||
|
CdkVirtualScrollViewport,
|
||||||
|
ScrollingModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
@@ -54,13 +62,12 @@ describe('LogsComponent', () => {
|
|||||||
fixture = TestBed.createComponent(LogsComponent)
|
fixture = TestBed.createComponent(LogsComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
reloadSpy = jest.spyOn(component, 'reloadLogs')
|
reloadSpy = jest.spyOn(component, 'reloadLogs')
|
||||||
window.HTMLElement.prototype.scroll = function () {} // mock scroll
|
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display logs with first log initially', () => {
|
it('should display logs with first log initially', () => {
|
||||||
expect(logSpy).toHaveBeenCalledWith('paperless')
|
expect(logSpy).toHaveBeenCalledWith('paperless', 5000)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||||
paperless_logs[0]
|
paperless_logs[0]
|
||||||
@@ -71,7 +78,7 @@ describe('LogsComponent', () => {
|
|||||||
fixture.debugElement
|
fixture.debugElement
|
||||||
.queryAll(By.directive(NgbNavLink))[1]
|
.queryAll(By.directive(NgbNavLink))[1]
|
||||||
.nativeElement.dispatchEvent(new MouseEvent('click'))
|
.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||||
expect(logSpy).toHaveBeenCalledWith('mail')
|
expect(logSpy).toHaveBeenCalledWith('mail', 5000)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle error with no logs', () => {
|
it('should handle error with no logs', () => {
|
||||||
@@ -83,6 +90,10 @@ describe('LogsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should auto refresh, allow toggle', () => {
|
it('should auto refresh, allow toggle', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(CdkVirtualScrollViewport.prototype, 'scrollToIndex')
|
||||||
|
.mockImplementation(() => undefined)
|
||||||
|
|
||||||
jest.advanceTimersByTime(6000)
|
jest.advanceTimersByTime(6000)
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
@@ -90,4 +101,20 @@ describe('LogsComponent', () => {
|
|||||||
jest.advanceTimersByTime(6000)
|
jest.advanceTimersByTime(6000)
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should debounce limit changes before reloading logs', () => {
|
||||||
|
const initialCalls = reloadSpy.mock.calls.length
|
||||||
|
component.onLimitChange(6000)
|
||||||
|
jest.advanceTimersByTime(299)
|
||||||
|
expect(reloadSpy).toHaveBeenCalledTimes(initialCalls)
|
||||||
|
jest.advanceTimersByTime(1)
|
||||||
|
expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update jump to bottom visibility on scroll', () => {
|
||||||
|
component.showJumpToBottom = false
|
||||||
|
jest.spyOn(component as any, 'isNearBottom').mockReturnValue(false)
|
||||||
|
component.onScroll()
|
||||||
|
expect(component.showJumpToBottom).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
@@ -9,7 +10,7 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { filter, takeUntil, timer } from 'rxjs'
|
import { Subject, debounceTime, filter, takeUntil, timer } from 'rxjs'
|
||||||
import { LogService } from 'src/app/services/rest/log.service'
|
import { LogService } from 'src/app/services/rest/log.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
@@ -21,6 +22,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
imports: [
|
imports: [
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
],
|
],
|
||||||
@@ -32,7 +34,7 @@ export class LogsComponent
|
|||||||
private logService = inject(LogService)
|
private logService = inject(LogService)
|
||||||
private changedetectorRef = inject(ChangeDetectorRef)
|
private changedetectorRef = inject(ChangeDetectorRef)
|
||||||
|
|
||||||
public logs: string[] = []
|
public logs: Array<{ message: string; level: number }> = []
|
||||||
|
|
||||||
public logFiles: string[] = []
|
public logFiles: string[] = []
|
||||||
|
|
||||||
@@ -40,9 +42,19 @@ export class LogsComponent
|
|||||||
|
|
||||||
public autoRefreshEnabled: boolean = true
|
public autoRefreshEnabled: boolean = true
|
||||||
|
|
||||||
@ViewChild('logContainer') logContainer: ElementRef
|
public limit: number = 5000
|
||||||
|
|
||||||
|
public showJumpToBottom = false
|
||||||
|
|
||||||
|
private readonly limitChange$ = new Subject<number>()
|
||||||
|
|
||||||
|
@ViewChild('logContainer') logContainer: ElementRef<HTMLElement>
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.limitChange$
|
||||||
|
.pipe(debounceTime(300), takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => this.reloadLogs())
|
||||||
|
|
||||||
this.logService
|
this.logService
|
||||||
.list()
|
.list()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
@@ -68,16 +80,37 @@ export class LogsComponent
|
|||||||
super.ngOnDestroy()
|
super.ngOnDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLimitChange(limit: number): void {
|
||||||
|
this.limitChange$.next(limit)
|
||||||
|
}
|
||||||
|
|
||||||
reloadLogs() {
|
reloadLogs() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
const shouldStickToBottom = this.isNearBottom()
|
||||||
this.logService
|
this.logService
|
||||||
.get(this.activeLog)
|
.get(this.activeLog, this.limit)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
this.logs = result
|
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.scrollToBottom()
|
const parsed = this.parseLogsWithLevel(result)
|
||||||
|
const hasChanges =
|
||||||
|
parsed.length !== this.logs.length ||
|
||||||
|
parsed.some((log, idx) => {
|
||||||
|
const current = this.logs[idx]
|
||||||
|
return (
|
||||||
|
!current ||
|
||||||
|
current.message !== log.message ||
|
||||||
|
current.level !== log.level
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (hasChanges) {
|
||||||
|
this.logs = parsed
|
||||||
|
if (shouldStickToBottom) {
|
||||||
|
this.scrollToBottom()
|
||||||
|
}
|
||||||
|
this.showJumpToBottom = !shouldStickToBottom
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.logs = []
|
this.logs = []
|
||||||
@@ -100,12 +133,35 @@ export class LogsComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseLogsWithLevel(
|
||||||
|
logs: string[]
|
||||||
|
): Array<{ message: string; level: number }> {
|
||||||
|
return logs.map((log) => ({
|
||||||
|
message: log,
|
||||||
|
level: this.getLogLevel(log),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
scrollToBottom(): void {
|
scrollToBottom(): void {
|
||||||
|
const viewport = this.logContainer?.nativeElement
|
||||||
|
if (!viewport) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.changedetectorRef.detectChanges()
|
this.changedetectorRef.detectChanges()
|
||||||
this.logContainer?.nativeElement.scroll({
|
viewport.scrollTop = viewport.scrollHeight
|
||||||
top: this.logContainer.nativeElement.scrollHeight,
|
this.showJumpToBottom = false
|
||||||
left: 0,
|
}
|
||||||
behavior: 'auto',
|
|
||||||
})
|
private isNearBottom(): boolean {
|
||||||
|
if (!this.logContainer?.nativeElement) return true
|
||||||
|
const distanceFromBottom =
|
||||||
|
this.logContainer.nativeElement.scrollHeight -
|
||||||
|
this.logContainer.nativeElement.scrollTop -
|
||||||
|
this.logContainer.nativeElement.clientHeight
|
||||||
|
return distanceFromBottom <= 40
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll(): void {
|
||||||
|
this.showJumpToBottom = !this.isNearBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,6 @@ import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
|||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { GroupService } from 'src/app/services/rest/group.service'
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||||
@@ -129,7 +128,6 @@ describe('SettingsComponent', () => {
|
|||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
CheckComponent,
|
CheckComponent,
|
||||||
ColorComponent,
|
ColorComponent,
|
||||||
SafeHtmlPipe,
|
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
NumberComponent,
|
NumberComponent,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { TrashService } from 'src/app/services/trash.service'
|
import { TrashService } from 'src/app/services/trash.service'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
@@ -53,7 +52,6 @@ describe('TrashComponent', () => {
|
|||||||
TrashComponent,
|
TrashComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
SafeHtmlPipe,
|
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
@for (user of users; track user) {
|
@for (user of users; track user) {
|
||||||
<li class="list-group-item">
|
<li class="list-group-item">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
|
<div class="col d-flex align-items-center" [class.opacity-50]="!user.is_active"><button class="btn btn-link p-0 text-start" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
|
||||||
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
|
<div class="col d-flex align-items-center">{{user.first_name}} {{user.last_name}}</div>
|
||||||
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
|
<div class="col d-flex align-items-center">{{user.groups?.map(getGroupName, this).join(', ')}}</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|||||||
@@ -68,13 +68,15 @@
|
|||||||
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse"
|
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse"
|
||||||
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
|
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
|
||||||
[ngbCollapse]="isMenuCollapsed">
|
[ngbCollapse]="isMenuCollapsed">
|
||||||
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
@if (canSaveSettings) {
|
||||||
@if (slimSidebarEnabled) {
|
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
||||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
@if (slimSidebarEnabled) {
|
||||||
} @else {
|
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
||||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
} @else {
|
||||||
}
|
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
||||||
</button>
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item app-link">
|
<li class="nav-item app-link">
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ main {
|
|||||||
.navbar .dropdown-menu {
|
.navbar .dropdown-menu {
|
||||||
font-size: 0.875rem; // body size
|
font-size: 0.875rem; // body size
|
||||||
|
|
||||||
a i-bs {
|
a i-bs, button i-bs {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,19 @@ export class AppFrameComponent
|
|||||||
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canSaveSettings(): boolean {
|
||||||
|
return (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.Change,
|
||||||
|
PermissionType.UISettings
|
||||||
|
) &&
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.Add,
|
||||||
|
PermissionType.UISettings
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -411,6 +411,9 @@ export class GlobalSearchComponent implements OnInit {
|
|||||||
const ruleType = this.useAdvancedForFullSearch
|
const ruleType = this.useAdvancedForFullSearch
|
||||||
? FILTER_FULLTEXT_QUERY
|
? FILTER_FULLTEXT_QUERY
|
||||||
: FILTER_TITLE_CONTENT
|
: FILTER_TITLE_CONTENT
|
||||||
|
this.documentService.searchQuery = this.useAdvancedForFullSearch
|
||||||
|
? this.query
|
||||||
|
: ''
|
||||||
this.documentListViewService.quickFilter([
|
this.documentListViewService.quickFilter([
|
||||||
{ rule_type: ruleType, value: this.query },
|
{ rule_type: ruleType, value: this.query },
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<p><b>{{messageBold}}</b></p>
|
<p><b>{{messageBold}}</b></p>
|
||||||
}
|
}
|
||||||
@if (message) {
|
@if (message) {
|
||||||
<p class="mb-0" [innerHTML]="message | safeHtml"></p>
|
<p class="mb-0" [innerHTML]="message"></p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { ConfirmDialogComponent } from './confirm-dialog.component'
|
import { ConfirmDialogComponent } from './confirm-dialog.component'
|
||||||
|
|
||||||
describe('ConfirmDialogComponent', () => {
|
describe('ConfirmDialogComponent', () => {
|
||||||
@@ -11,8 +10,8 @@ describe('ConfirmDialogComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [NgbActiveModal, SafeHtmlPipe],
|
providers: [NgbActiveModal],
|
||||||
imports: [ConfirmDialogComponent, SafeHtmlPipe],
|
imports: [ConfirmDialogComponent],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
modal = TestBed.inject(NgbActiveModal)
|
modal = TestBed.inject(NgbActiveModal)
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import { DecimalPipe } from '@angular/common'
|
|||||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-confirm-dialog',
|
selector: 'pngx-confirm-dialog',
|
||||||
templateUrl: './confirm-dialog.component.html',
|
templateUrl: './confirm-dialog.component.html',
|
||||||
styleUrls: ['./confirm-dialog.component.scss'],
|
styleUrls: ['./confirm-dialog.component.scss'],
|
||||||
imports: [DecimalPipe, SafeHtmlPipe],
|
imports: [DecimalPipe],
|
||||||
})
|
})
|
||||||
export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
|
export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
|
||||||
activeModal = inject(NgbActiveModal)
|
activeModal = inject(NgbActiveModal)
|
||||||
|
|||||||
+2
-2
@@ -28,10 +28,10 @@
|
|||||||
<div class="modal-footer flex-nowrap">
|
<div class="modal-footer flex-nowrap">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@if (message) {
|
@if (message) {
|
||||||
<p [innerHTML]="message | safeHtml"></p>
|
<p>{{message}}</p>
|
||||||
}
|
}
|
||||||
@if (messageBold) {
|
@if (messageBold) {
|
||||||
<p class="mb-0 small"><b [innerHTML]="messageBold | safeHtml"></b></p>
|
<p class="mb-0 small"><b>{{messageBold}}</b></p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||||
|
|||||||
-3
@@ -3,7 +3,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
|
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
|
||||||
|
|
||||||
describe('RotateConfirmDialogComponent', () => {
|
describe('RotateConfirmDialogComponent', () => {
|
||||||
@@ -15,11 +14,9 @@ describe('RotateConfirmDialogComponent', () => {
|
|||||||
imports: [
|
imports: [
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
RotateConfirmDialogComponent,
|
RotateConfirmDialogComponent,
|
||||||
SafeHtmlPipe,
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
SafeHtmlPipe,
|
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
],
|
],
|
||||||
|
|||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
import { NgStyle } from '@angular/common'
|
import { NgStyle } from '@angular/common'
|
||||||
import { Component, inject } from '@angular/core'
|
import { Component, inject } from '@angular/core'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
|||||||
selector: 'pngx-rotate-confirm-dialog',
|
selector: 'pngx-rotate-confirm-dialog',
|
||||||
templateUrl: './rotate-confirm-dialog.component.html',
|
templateUrl: './rotate-confirm-dialog.component.html',
|
||||||
styleUrl: './rotate-confirm-dialog.component.scss',
|
styleUrl: './rotate-confirm-dialog.component.scss',
|
||||||
imports: [NgStyle, NgxBootstrapIconsModule, SafeHtmlPipe],
|
imports: [NgStyle, NgxBootstrapIconsModule],
|
||||||
})
|
})
|
||||||
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
||||||
documentService = inject(DocumentService)
|
documentService = inject(DocumentService)
|
||||||
|
|||||||
+1
@@ -63,6 +63,7 @@
|
|||||||
bindValue="id"
|
bindValue="id"
|
||||||
[(ngModel)]="atom.value"
|
[(ngModel)]="atom.value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
|
[virtualScroll]="getSelectOptionsForField(atom.field)?.length > 100"
|
||||||
(mousedown)="$event.stopImmediatePropagation()"
|
(mousedown)="$event.stopImmediatePropagation()"
|
||||||
></ng-select>
|
></ng-select>
|
||||||
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.DocumentLink) {
|
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.DocumentLink) {
|
||||||
|
|||||||
+8
@@ -354,5 +354,13 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
model.removeElement(atom)
|
model.removeElement(atom)
|
||||||
expect(completeSpy).toHaveBeenCalled()
|
expect(completeSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should subscribe to existing elements when queries are assigned', () => {
|
||||||
|
const expression = new CustomFieldQueryExpression()
|
||||||
|
const nextSpy = jest.spyOn(model.changed, 'next')
|
||||||
|
model.queries = [expression]
|
||||||
|
expression.changed.next(expression)
|
||||||
|
expect(nextSpy).toHaveBeenCalledWith(model)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
+33
-9
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { first, Subject, takeUntil } from 'rxjs'
|
import { first, Subject, Subscription, takeUntil } from 'rxjs'
|
||||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
import {
|
import {
|
||||||
CUSTOM_FIELD_QUERY_MAX_ATOMS,
|
CUSTOM_FIELD_QUERY_MAX_ATOMS,
|
||||||
@@ -41,10 +41,27 @@ import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.comp
|
|||||||
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
||||||
|
|
||||||
export class CustomFieldQueriesModel {
|
export class CustomFieldQueriesModel {
|
||||||
public queries: CustomFieldQueryElement[] = []
|
private _queries: CustomFieldQueryElement[] = []
|
||||||
|
private rootSubscriptions: Subscription[] = []
|
||||||
|
|
||||||
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
||||||
|
|
||||||
|
public get queries(): CustomFieldQueryElement[] {
|
||||||
|
return this._queries
|
||||||
|
}
|
||||||
|
|
||||||
|
public set queries(value: CustomFieldQueryElement[]) {
|
||||||
|
this.teardownRootSubscriptions()
|
||||||
|
this._queries = value ?? []
|
||||||
|
for (const element of this._queries) {
|
||||||
|
this.rootSubscriptions.push(
|
||||||
|
element.changed.subscribe(() => {
|
||||||
|
this.changed.next(this)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public clear(fireEvent = true) {
|
public clear(fireEvent = true) {
|
||||||
this.queries = []
|
this.queries = []
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
@@ -107,14 +124,14 @@ export class CustomFieldQueriesModel {
|
|||||||
public addExpression(
|
public addExpression(
|
||||||
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
|
expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
|
||||||
) {
|
) {
|
||||||
if (this.queries.length > 0) {
|
if (this.queries.length === 0) {
|
||||||
;(
|
this.queries = [expression]
|
||||||
(this.queries[0] as CustomFieldQueryExpression)
|
return
|
||||||
.value as CustomFieldQueryElement[]
|
|
||||||
).push(expression)
|
|
||||||
} else {
|
|
||||||
this.queries.push(expression)
|
|
||||||
}
|
}
|
||||||
|
;(
|
||||||
|
(this.queries[0] as CustomFieldQueryExpression)
|
||||||
|
.value as CustomFieldQueryElement[]
|
||||||
|
).push(expression)
|
||||||
expression.changed.subscribe(() => {
|
expression.changed.subscribe(() => {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
})
|
})
|
||||||
@@ -166,6 +183,13 @@ export class CustomFieldQueriesModel {
|
|||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private teardownRootSubscriptions() {
|
||||||
|
for (const subscription of this.rootSubscriptions) {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
}
|
||||||
|
this.rootSubscriptions = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
i18n-placeholder
|
i18n-placeholder
|
||||||
(change)="onSetCreatedRelativeDate($event)">
|
(change)="onSetCreatedRelativeDate($event)">
|
||||||
<ng-template ng-option-tmp let-item="item">
|
<ng-template ng-option-tmp let-item="item">
|
||||||
<div class="d-flex">{{ item.name }}<span class="ms-auto text-muted small">{{ item.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container></span></div>
|
<ng-container [ngTemplateOutlet]="relativeDateOptionTemplate" [ngTemplateOutletContext]="{ $implicit: item }"></ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-select>
|
</ng-select>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
i18n-placeholder
|
i18n-placeholder
|
||||||
(change)="onSetAddedRelativeDate($event)">
|
(change)="onSetAddedRelativeDate($event)">
|
||||||
<ng-template ng-option-tmp let-item="item">
|
<ng-template ng-option-tmp let-item="item">
|
||||||
<div class="d-flex">{{ item.name }}<span class="ms-auto text-muted small">{{ item.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container></span></div>
|
<ng-container [ngTemplateOutlet]="relativeDateOptionTemplate" [ngTemplateOutletContext]="{ $implicit: item }"></ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-select>
|
</ng-select>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,3 +158,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #relativeDateOptionTemplate let-item>
|
||||||
|
<div class="d-flex">
|
||||||
|
{{ item.name }}
|
||||||
|
<span class="ms-auto text-muted small">
|
||||||
|
@if (item.dateEnd) {
|
||||||
|
{{ item.date | customDate:'MMM d' }} – {{ item.dateEnd | customDate:'mediumDate' }}
|
||||||
|
} @else {
|
||||||
|
{{ item.date | customDate:'mediumDate' }} – <ng-container i18n>now</ng-container>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NgClass } from '@angular/common'
|
import { NgClass, NgTemplateOutlet } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@@ -42,6 +42,10 @@ export enum RelativeDate {
|
|||||||
THIS_MONTH = 6,
|
THIS_MONTH = 6,
|
||||||
TODAY = 7,
|
TODAY = 7,
|
||||||
YESTERDAY = 8,
|
YESTERDAY = 8,
|
||||||
|
PREVIOUS_WEEK = 9,
|
||||||
|
PREVIOUS_MONTH = 10,
|
||||||
|
PREVIOUS_QUARTER = 11,
|
||||||
|
PREVIOUS_YEAR = 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -59,6 +63,7 @@ export enum RelativeDate {
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgClass,
|
NgClass,
|
||||||
|
NgTemplateOutlet,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DatesDropdownComponent implements OnInit, OnDestroy {
|
export class DatesDropdownComponent implements OnInit, OnDestroy {
|
||||||
@@ -111,6 +116,46 @@ export class DatesDropdownComponent implements OnInit, OnDestroy {
|
|||||||
name: $localize`Yesterday`,
|
name: $localize`Yesterday`,
|
||||||
date: new Date().setDate(new Date().getDate() - 1),
|
date: new Date().setDate(new Date().getDate() - 1),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: RelativeDate.PREVIOUS_WEEK,
|
||||||
|
name: $localize`Previous week`,
|
||||||
|
date: new Date(
|
||||||
|
new Date().getFullYear(),
|
||||||
|
new Date().getMonth(),
|
||||||
|
new Date().getDate() - new Date().getDay() - 6
|
||||||
|
),
|
||||||
|
dateEnd: new Date(
|
||||||
|
new Date().getFullYear(),
|
||||||
|
new Date().getMonth(),
|
||||||
|
new Date().getDate() - new Date().getDay()
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: RelativeDate.PREVIOUS_MONTH,
|
||||||
|
name: $localize`Previous month`,
|
||||||
|
date: new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1),
|
||||||
|
dateEnd: new Date(new Date().getFullYear(), new Date().getMonth(), 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: RelativeDate.PREVIOUS_QUARTER,
|
||||||
|
name: $localize`Previous quarter`,
|
||||||
|
date: new Date(
|
||||||
|
new Date().getFullYear(),
|
||||||
|
Math.floor(new Date().getMonth() / 3) * 3 - 3,
|
||||||
|
1
|
||||||
|
),
|
||||||
|
dateEnd: new Date(
|
||||||
|
new Date().getFullYear(),
|
||||||
|
Math.floor(new Date().getMonth() / 3) * 3,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: RelativeDate.PREVIOUS_YEAR,
|
||||||
|
name: $localize`Previous year`,
|
||||||
|
date: new Date('1/1/' + (new Date().getFullYear() - 1)),
|
||||||
|
dateEnd: new Date('12/31/' + (new Date().getFullYear() - 1)),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
datePlaceHolder: string
|
datePlaceHolder: string
|
||||||
|
|||||||
-2
@@ -10,7 +10,6 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|||||||
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
||||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { SelectComponent } from '../../input/select/select.component'
|
import { SelectComponent } from '../../input/select/select.component'
|
||||||
import { TextComponent } from '../../input/text/text.component'
|
import { TextComponent } from '../../input/text/text.component'
|
||||||
@@ -35,7 +34,6 @@ describe('CustomFieldEditDialogComponent', () => {
|
|||||||
IfOwnerDirective,
|
IfOwnerDirective,
|
||||||
SelectComponent,
|
SelectComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
SafeHtmlPipe,
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from 'src/app/data/matching-model'
|
} from 'src/app/data/matching-model'
|
||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
@@ -87,6 +88,7 @@ describe('EditDialogComponent', () => {
|
|||||||
let component: TestComponent
|
let component: TestComponent
|
||||||
let fixture: ComponentFixture<TestComponent>
|
let fixture: ComponentFixture<TestComponent>
|
||||||
let tagService: TagService
|
let tagService: TagService
|
||||||
|
let permissionsService: PermissionsService
|
||||||
let settingsService: SettingsService
|
let settingsService: SettingsService
|
||||||
let activeModal: NgbActiveModal
|
let activeModal: NgbActiveModal
|
||||||
let httpTestingController: HttpTestingController
|
let httpTestingController: HttpTestingController
|
||||||
@@ -118,8 +120,10 @@ describe('EditDialogComponent', () => {
|
|||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
tagService = TestBed.inject(TagService)
|
tagService = TestBed.inject(TagService)
|
||||||
|
permissionsService = TestBed.inject(PermissionsService)
|
||||||
settingsService = TestBed.inject(SettingsService)
|
settingsService = TestBed.inject(SettingsService)
|
||||||
settingsService.currentUser = currentUser
|
settingsService.currentUser = currentUser
|
||||||
|
permissionsService.initialize([], currentUser as any)
|
||||||
activeModal = TestBed.inject(NgbActiveModal)
|
activeModal = TestBed.inject(NgbActiveModal)
|
||||||
httpTestingController = TestBed.inject(HttpTestingController)
|
httpTestingController = TestBed.inject(HttpTestingController)
|
||||||
|
|
||||||
@@ -226,6 +230,25 @@ describe('EditDialogComponent', () => {
|
|||||||
expect(updateSpy).toHaveBeenCalled()
|
expect(updateSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not submit owner or permissions for non-owner edits', () => {
|
||||||
|
component.object = tag
|
||||||
|
component.dialogMode = EditDialogMode.EDIT
|
||||||
|
component.ngOnInit()
|
||||||
|
|
||||||
|
component.objectForm.get('name').setValue('Updated tag')
|
||||||
|
component.save()
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}tags/${tag.id}/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('PUT')
|
||||||
|
expect(req.request.body.name).toEqual('Updated tag')
|
||||||
|
expect(req.request.body.owner).toEqual(tag.owner)
|
||||||
|
expect(req.request.body.set_permissions).toBeUndefined()
|
||||||
|
|
||||||
|
req.flush({})
|
||||||
|
})
|
||||||
|
|
||||||
it('should create an object on save in edit mode', () => {
|
it('should create an object on save in edit mode', () => {
|
||||||
const createSpy = jest.spyOn(tagService, 'create')
|
const createSpy = jest.spyOn(tagService, 'create')
|
||||||
component.dialogMode = EditDialogMode.CREATE
|
component.dialogMode = EditDialogMode.CREATE
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { ObjectWithId } from 'src/app/data/object-with-id'
|
|||||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
|
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
@@ -42,6 +43,7 @@ export abstract class EditDialogComponent<
|
|||||||
protected activeModal = inject(NgbActiveModal)
|
protected activeModal = inject(NgbActiveModal)
|
||||||
protected userService = inject(UserService)
|
protected userService = inject(UserService)
|
||||||
protected settingsService = inject(SettingsService)
|
protected settingsService = inject(SettingsService)
|
||||||
|
protected permissionsService = inject(PermissionsService)
|
||||||
|
|
||||||
users: User[]
|
users: User[]
|
||||||
|
|
||||||
@@ -69,10 +71,6 @@ export abstract class EditDialogComponent<
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.object != null && this.dialogMode !== EditDialogMode.CREATE) {
|
if (this.object != null && this.dialogMode !== EditDialogMode.CREATE) {
|
||||||
if ((this.object as ObjectWithPermissions).permissions) {
|
|
||||||
this.object['set_permissions'] = this.object['permissions']
|
|
||||||
}
|
|
||||||
|
|
||||||
this.object['permissions_form'] = {
|
this.object['permissions_form'] = {
|
||||||
owner: (this.object as ObjectWithPermissions).owner,
|
owner: (this.object as ObjectWithPermissions).owner,
|
||||||
set_permissions: (this.object as ObjectWithPermissions).permissions,
|
set_permissions: (this.object as ObjectWithPermissions).permissions,
|
||||||
@@ -151,18 +149,28 @@ export abstract class EditDialogComponent<
|
|||||||
return Object.assign({}, this.objectForm.value)
|
return Object.assign({}, this.objectForm.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected shouldSubmitPermissions(): boolean {
|
||||||
|
return (
|
||||||
|
this.dialogMode === EditDialogMode.CREATE ||
|
||||||
|
this.permissionsService.currentUserOwnsObject(this.object)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
this.error = null
|
this.error = null
|
||||||
const formValues = this.getFormValues()
|
const formValues = this.getFormValues()
|
||||||
const permissionsObject: PermissionsFormObject =
|
const permissionsObject: PermissionsFormObject =
|
||||||
this.objectForm.get('permissions_form')?.value
|
this.objectForm.get('permissions_form')?.value
|
||||||
if (permissionsObject) {
|
if (permissionsObject && this.shouldSubmitPermissions()) {
|
||||||
formValues.owner = permissionsObject.owner
|
formValues.owner = permissionsObject.owner
|
||||||
formValues.set_permissions = permissionsObject.set_permissions
|
formValues.set_permissions = permissionsObject.set_permissions
|
||||||
delete formValues.permissions_form
|
|
||||||
}
|
}
|
||||||
|
delete formValues.permissions_form
|
||||||
|
|
||||||
var newObject = Object.assign(Object.assign({}, this.object), formValues)
|
var newObject = Object.assign(Object.assign({}, this.object), formValues)
|
||||||
|
if (!this.shouldSubmitPermissions()) {
|
||||||
|
delete newObject['set_permissions']
|
||||||
|
}
|
||||||
var serverResponse: Observable<T>
|
var serverResponse: Observable<T>
|
||||||
switch (this.dialogMode) {
|
switch (this.dialogMode) {
|
||||||
case EditDialogMode.CREATE:
|
case EditDialogMode.CREATE:
|
||||||
|
|||||||
-2
@@ -11,7 +11,6 @@ import {
|
|||||||
} from 'src/app/data/mail-rule'
|
} from 'src/app/data/mail-rule'
|
||||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||||
@@ -46,7 +45,6 @@ describe('MailRuleEditDialogComponent', () => {
|
|||||||
PermissionsFormComponent,
|
PermissionsFormComponent,
|
||||||
NumberComponent,
|
NumberComponent,
|
||||||
TagsComponent,
|
TagsComponent,
|
||||||
SafeHtmlPipe,
|
|
||||||
CheckComponent,
|
CheckComponent,
|
||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
],
|
],
|
||||||
|
|||||||
+2
-2
@@ -9,11 +9,11 @@ import { first } from 'rxjs'
|
|||||||
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 { Group } from 'src/app/data/group'
|
import { Group } from 'src/app/data/group'
|
||||||
import { User } from 'src/app/data/user'
|
import { User } from 'src/app/data/user'
|
||||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
|
||||||
import { GroupService } from 'src/app/services/rest/group.service'
|
import { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { ConfirmButtonComponent } from '../../confirm-button/confirm-button.component'
|
||||||
import { PasswordComponent } from '../../input/password/password.component'
|
import { PasswordComponent } from '../../input/password/password.component'
|
||||||
import { SelectComponent } from '../../input/select/select.component'
|
import { SelectComponent } from '../../input/select/select.component'
|
||||||
import { TextComponent } from '../../input/text/text.component'
|
import { TextComponent } from '../../input/text/text.component'
|
||||||
@@ -28,6 +28,7 @@ import { PermissionsSelectComponent } from '../../permissions-select/permissions
|
|||||||
SelectComponent,
|
SelectComponent,
|
||||||
TextComponent,
|
TextComponent,
|
||||||
PasswordComponent,
|
PasswordComponent,
|
||||||
|
ConfirmButtonComponent,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
],
|
],
|
||||||
@@ -37,7 +38,6 @@ export class UserEditDialogComponent
|
|||||||
implements OnInit
|
implements OnInit
|
||||||
{
|
{
|
||||||
private toastService = inject(ToastService)
|
private toastService = inject(ToastService)
|
||||||
private permissionsService = inject(PermissionsService)
|
|
||||||
private groupsService: GroupService
|
private groupsService: GroupService
|
||||||
|
|
||||||
groups: Group[]
|
groups: Group[]
|
||||||
|
|||||||
+6
-4
@@ -77,9 +77,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
|
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
|
||||||
@for (action of object?.actions; track action; let i = $index){
|
@for (action of object?.actions; track action; let i = $index){
|
||||||
<div ngbAccordionItem cdkDrag [formGroup]="actionFields.controls[i]">
|
<div ngbAccordionItem [formGroup]="actionFields.controls[i]">
|
||||||
<div ngbAccordionHeader>
|
<div ngbAccordionHeader cdkDrag>
|
||||||
<button ngbAccordionButton>{{i + 1}}. {{getActionTypeOptionName(actionFields.controls[i].value.type)}}
|
<button ngbAccordionButton>
|
||||||
|
<i-bs name="grip-vertical" class="ms-n3 pe-1"></i-bs>
|
||||||
|
{{i + 1}}. {{getActionTypeOptionName(actionFields.controls[i].value.type)}}
|
||||||
@if(action.id) {
|
@if(action.id) {
|
||||||
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
|
||||||
}
|
}
|
||||||
@@ -159,7 +161,7 @@
|
|||||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" horizontal="true" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" horizontal="true" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||||
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
|
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
|
||||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" horizontal="true" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" horizontal="true" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
||||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" horizontal="true" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a>" [error]="error?.filter_path"></pngx-input-text>
|
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" horizontal="true" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized." [error]="error?.filter_path"></pngx-input-text>
|
||||||
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" horizontal="true" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" horizontal="true" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
||||||
}
|
}
|
||||||
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated || formGroup.get('type').value === WorkflowTriggerType.Scheduled) {
|
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated || formGroup.get('type').value === WorkflowTriggerType.Scheduled) {
|
||||||
|
|||||||
+4
@@ -11,3 +11,7 @@
|
|||||||
:host ::ng-deep .filters .paperless-input-select.mb-3 {
|
:host ::ng-deep .filters .paperless-input-select.mb-3 {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ms-n3 {
|
||||||
|
margin-left: -1rem !important;
|
||||||
|
}
|
||||||
+1
-5
@@ -30,7 +30,6 @@ import {
|
|||||||
} from 'src/app/data/workflow-trigger'
|
} from 'src/app/data/workflow-trigger'
|
||||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
@@ -105,7 +104,6 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
TagsComponent,
|
TagsComponent,
|
||||||
PermissionsUserComponent,
|
PermissionsUserComponent,
|
||||||
PermissionsGroupComponent,
|
PermissionsGroupComponent,
|
||||||
SafeHtmlPipe,
|
|
||||||
ConfirmButtonComponent,
|
ConfirmButtonComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -254,7 +252,7 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
expect(component.object.actions.length).toEqual(2)
|
expect(component.object.actions.length).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update order and remove ids from actions on drag n drop', () => {
|
it('should update order on drag n drop', () => {
|
||||||
const action1 = workflow.actions[0]
|
const action1 = workflow.actions[0]
|
||||||
const action2 = workflow.actions[1]
|
const action2 = workflow.actions[1]
|
||||||
component.object = workflow
|
component.object = workflow
|
||||||
@@ -263,8 +261,6 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
WorkflowAction[]
|
WorkflowAction[]
|
||||||
>)
|
>)
|
||||||
expect(component.object.actions).toEqual([action2, action1])
|
expect(component.object.actions).toEqual([action2, action1])
|
||||||
expect(action1.id).toBeNull()
|
|
||||||
expect(action2.id).toBeNull()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not include auto matching in algorithms', () => {
|
it('should not include auto matching in algorithms', () => {
|
||||||
|
|||||||
-5
@@ -1283,11 +1283,6 @@ export class WorkflowEditDialogComponent
|
|||||||
const actionField = this.actionFields.at(event.previousIndex)
|
const actionField = this.actionFields.at(event.previousIndex)
|
||||||
this.actionFields.removeAt(event.previousIndex)
|
this.actionFields.removeAt(event.previousIndex)
|
||||||
this.actionFields.insert(event.currentIndex, actionField)
|
this.actionFields.insert(event.currentIndex, actionField)
|
||||||
// removing id will effectively re-create the actions in this order
|
|
||||||
this.object.actions.forEach((a) => (a.id = null))
|
|
||||||
this.actionFields.controls.forEach((c) =>
|
|
||||||
c.get('id').setValue(null, { emitEvent: false })
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
|
|||||||
+255
@@ -564,6 +564,261 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps children with their parent when parent has document count', () => {
|
||||||
|
const parent: Tag = {
|
||||||
|
id: 10,
|
||||||
|
name: 'Parent Tag',
|
||||||
|
orderIndex: 0,
|
||||||
|
document_count: 2,
|
||||||
|
}
|
||||||
|
const child: Tag = {
|
||||||
|
id: 11,
|
||||||
|
name: 'Child Tag',
|
||||||
|
parent: parent.id,
|
||||||
|
orderIndex: 1,
|
||||||
|
document_count: 0,
|
||||||
|
}
|
||||||
|
const otherRoot: Tag = {
|
||||||
|
id: 20,
|
||||||
|
name: 'Other Tag',
|
||||||
|
orderIndex: 2,
|
||||||
|
document_count: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
component.selectionModel.items = [parent, child, otherRoot]
|
||||||
|
component.selectionModel = selectionModel
|
||||||
|
component.documentCounts = [
|
||||||
|
{ id: parent.id, document_count: 2 },
|
||||||
|
{ id: otherRoot.id, document_count: 0 },
|
||||||
|
]
|
||||||
|
selectionModel.apply()
|
||||||
|
|
||||||
|
expect(component.selectionModel.items).toEqual([
|
||||||
|
nullItem,
|
||||||
|
parent,
|
||||||
|
child,
|
||||||
|
otherRoot,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps selected branches ahead of document-based ordering', () => {
|
||||||
|
const selectedRoot: Tag = {
|
||||||
|
id: 30,
|
||||||
|
name: 'Selected Root',
|
||||||
|
orderIndex: 0,
|
||||||
|
document_count: 0,
|
||||||
|
}
|
||||||
|
const otherRoot: Tag = {
|
||||||
|
id: 40,
|
||||||
|
name: 'Other Root',
|
||||||
|
orderIndex: 1,
|
||||||
|
document_count: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
component.selectionModel.items = [selectedRoot, otherRoot]
|
||||||
|
component.selectionModel = selectionModel
|
||||||
|
selectionModel.set(selectedRoot.id, ToggleableItemState.Selected)
|
||||||
|
component.documentCounts = [
|
||||||
|
{ id: selectedRoot.id, document_count: 0 },
|
||||||
|
{ id: otherRoot.id, document_count: 2 },
|
||||||
|
]
|
||||||
|
selectionModel.apply()
|
||||||
|
|
||||||
|
expect(component.selectionModel.items).toEqual([
|
||||||
|
nullItem,
|
||||||
|
selectedRoot,
|
||||||
|
otherRoot,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deselecting a parent clears selected descendants', () => {
|
||||||
|
const root: Tag = { id: 100, name: 'Root Tag' }
|
||||||
|
const child: Tag = { id: 101, name: 'Child Tag', parent: root.id }
|
||||||
|
const grandchild: Tag = {
|
||||||
|
id: 102,
|
||||||
|
name: 'Grandchild Tag',
|
||||||
|
parent: child.id,
|
||||||
|
}
|
||||||
|
const other: Tag = { id: 103, name: 'Other Tag' }
|
||||||
|
|
||||||
|
selectionModel.items = [root, child, grandchild, other]
|
||||||
|
selectionModel.set(root.id, ToggleableItemState.Selected, false)
|
||||||
|
selectionModel.set(child.id, ToggleableItemState.Selected, false)
|
||||||
|
selectionModel.set(grandchild.id, ToggleableItemState.Selected, false)
|
||||||
|
selectionModel.set(other.id, ToggleableItemState.Selected, false)
|
||||||
|
|
||||||
|
selectionModel.toggle(root.id, false)
|
||||||
|
|
||||||
|
expect(selectionModel.getSelectedItems()).toEqual([other])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('un-excluding a parent clears excluded descendants', () => {
|
||||||
|
const root: Tag = { id: 110, name: 'Root Tag' }
|
||||||
|
const child: Tag = { id: 111, name: 'Child Tag', parent: root.id }
|
||||||
|
const other: Tag = { id: 112, name: 'Other Tag' }
|
||||||
|
|
||||||
|
selectionModel.items = [root, child, other]
|
||||||
|
selectionModel.set(root.id, ToggleableItemState.Excluded, false)
|
||||||
|
selectionModel.set(child.id, ToggleableItemState.Excluded, false)
|
||||||
|
selectionModel.set(other.id, ToggleableItemState.Excluded, false)
|
||||||
|
|
||||||
|
selectionModel.exclude(root.id, false)
|
||||||
|
|
||||||
|
expect(selectionModel.getExcludedItems()).toEqual([other])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('excluding a selected parent clears selected descendants', () => {
|
||||||
|
const root: Tag = { id: 120, name: 'Root Tag' }
|
||||||
|
const child: Tag = { id: 121, name: 'Child Tag', parent: root.id }
|
||||||
|
const other: Tag = { id: 122, name: 'Other Tag' }
|
||||||
|
|
||||||
|
selectionModel.manyToOne = true
|
||||||
|
selectionModel.items = [root, child, other]
|
||||||
|
selectionModel.set(root.id, ToggleableItemState.Selected, false)
|
||||||
|
selectionModel.set(child.id, ToggleableItemState.Selected, false)
|
||||||
|
selectionModel.set(other.id, ToggleableItemState.Selected, false)
|
||||||
|
|
||||||
|
selectionModel.exclude(root.id, false)
|
||||||
|
|
||||||
|
expect(selectionModel.getExcludedItems()).toEqual([root])
|
||||||
|
expect(selectionModel.getSelectedItems()).toEqual([other])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resorts items immediately when document count sorting enabled', () => {
|
||||||
|
const apple: Tag = { id: 55, name: 'Apple' }
|
||||||
|
const zebra: Tag = { id: 56, name: 'Zebra' }
|
||||||
|
|
||||||
|
selectionModel.documentCountSortingEnabled = true
|
||||||
|
selectionModel.items = [apple, zebra]
|
||||||
|
expect(selectionModel.items.map((item) => item?.id ?? null)).toEqual([
|
||||||
|
null,
|
||||||
|
apple.id,
|
||||||
|
zebra.id,
|
||||||
|
])
|
||||||
|
|
||||||
|
selectionModel.documentCounts = [
|
||||||
|
{ id: zebra.id, document_count: 5 },
|
||||||
|
{ id: apple.id, document_count: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(selectionModel.items.map((item) => item?.id ?? null)).toEqual([
|
||||||
|
null,
|
||||||
|
zebra.id,
|
||||||
|
apple.id,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not resort items by default when document counts are set', () => {
|
||||||
|
const first: Tag = { id: 57, name: 'First' }
|
||||||
|
const second: Tag = { id: 58, name: 'Second' }
|
||||||
|
|
||||||
|
selectionModel.items = [first, second]
|
||||||
|
selectionModel.documentCounts = [
|
||||||
|
{ id: second.id, document_count: 10 },
|
||||||
|
{ id: first.id, document_count: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(selectionModel.items.map((item) => item?.id ?? null)).toEqual([
|
||||||
|
null,
|
||||||
|
first.id,
|
||||||
|
second.id,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses fallback document counts when selection data is missing', () => {
|
||||||
|
const fallbackRoot: Tag = {
|
||||||
|
id: 50,
|
||||||
|
name: 'Fallback Root',
|
||||||
|
orderIndex: 0,
|
||||||
|
document_count: 3,
|
||||||
|
}
|
||||||
|
const fallbackChild: Tag = {
|
||||||
|
id: 51,
|
||||||
|
name: 'Fallback Child',
|
||||||
|
parent: fallbackRoot.id,
|
||||||
|
orderIndex: 1,
|
||||||
|
document_count: 0,
|
||||||
|
}
|
||||||
|
const otherRoot: Tag = {
|
||||||
|
id: 60,
|
||||||
|
name: 'Other Root',
|
||||||
|
orderIndex: 2,
|
||||||
|
document_count: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
component.selectionModel = selectionModel
|
||||||
|
selectionModel.items = [fallbackRoot, fallbackChild, otherRoot]
|
||||||
|
component.documentCounts = [{ id: otherRoot.id, document_count: 0 }]
|
||||||
|
|
||||||
|
selectionModel.apply()
|
||||||
|
|
||||||
|
expect(selectionModel.items).toEqual([
|
||||||
|
nullItem,
|
||||||
|
fallbackRoot,
|
||||||
|
fallbackChild,
|
||||||
|
otherRoot,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles special and non-numeric ids when promoting branches', () => {
|
||||||
|
const rootWithDocs: Tag = {
|
||||||
|
id: 70,
|
||||||
|
name: 'Root With Docs',
|
||||||
|
orderIndex: 0,
|
||||||
|
document_count: 1,
|
||||||
|
}
|
||||||
|
const miscItem: any = { id: 'misc', name: 'Misc Item' }
|
||||||
|
|
||||||
|
component.selectionModel = selectionModel
|
||||||
|
selectionModel.intersection = Intersection.Exclude
|
||||||
|
selectionModel.items = [rootWithDocs, miscItem as any]
|
||||||
|
component.documentCounts = [{ id: rootWithDocs.id, document_count: 1 }]
|
||||||
|
|
||||||
|
selectionModel.apply()
|
||||||
|
|
||||||
|
expect(selectionModel.items.map((item) => item.id)).toEqual([
|
||||||
|
NEGATIVE_NULL_FILTER_VALUE,
|
||||||
|
rootWithDocs.id,
|
||||||
|
'misc',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('memoizes root document counts between lookups', () => {
|
||||||
|
const memoRoot: Tag = { id: 80, name: 'Memo Root' }
|
||||||
|
selectionModel.items = [memoRoot]
|
||||||
|
selectionModel.documentCounts = [{ id: memoRoot.id, document_count: 9 }]
|
||||||
|
|
||||||
|
const getRootDocCount = (selectionModel as any).createRootDocCounter()
|
||||||
|
|
||||||
|
expect(getRootDocCount(memoRoot.id)).toEqual(9)
|
||||||
|
selectionModel.documentCounts = []
|
||||||
|
expect(getRootDocCount(memoRoot.id)).toEqual(9)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to model stored document counts if selection data missing entry', () => {
|
||||||
|
const rootWithoutSelection: Tag = {
|
||||||
|
id: 90,
|
||||||
|
name: 'Fallback Root',
|
||||||
|
document_count: 4,
|
||||||
|
}
|
||||||
|
selectionModel.items = [rootWithoutSelection]
|
||||||
|
selectionModel.documentCounts = []
|
||||||
|
|
||||||
|
const getRootDocCount = (selectionModel as any).createRootDocCounter()
|
||||||
|
|
||||||
|
expect(getRootDocCount(rootWithoutSelection.id)).toEqual(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('defaults to zero document count when neither selection nor model provide it', () => {
|
||||||
|
const rootWithoutCounts: Tag = { id: 91, name: 'Fallback Zero Root' }
|
||||||
|
selectionModel.items = [rootWithoutCounts]
|
||||||
|
selectionModel.documentCounts = []
|
||||||
|
|
||||||
|
const getRootDocCount = (selectionModel as any).createRootDocCounter()
|
||||||
|
|
||||||
|
expect(getRootDocCount(rootWithoutCounts.id)).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
|
it('should set support create, keep open model and call createRef method', fakeAsync(() => {
|
||||||
component.selectionModel.items = items
|
component.selectionModel.items = items
|
||||||
component.icon = 'tag-fill'
|
component.icon = 'tag-fill'
|
||||||
|
|||||||
+249
-9
@@ -32,6 +32,14 @@ export interface ChangedItems {
|
|||||||
itemsToRemove: MatchingModel[]
|
itemsToRemove: MatchingModel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BranchSummary = {
|
||||||
|
items: MatchingModel[]
|
||||||
|
firstIndex: number
|
||||||
|
special: boolean
|
||||||
|
selected: boolean
|
||||||
|
hasDocs: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export enum LogicalOperator {
|
export enum LogicalOperator {
|
||||||
And = 'and',
|
And = 'and',
|
||||||
Or = 'or',
|
Or = 'or',
|
||||||
@@ -53,8 +61,13 @@ export class FilterableDropdownSelectionModel {
|
|||||||
temporaryIntersection: Intersection = this._intersection
|
temporaryIntersection: Intersection = this._intersection
|
||||||
|
|
||||||
private _documentCounts: SelectionDataItem[] = []
|
private _documentCounts: SelectionDataItem[] = []
|
||||||
|
public documentCountSortingEnabled = false
|
||||||
|
|
||||||
public set documentCounts(counts: SelectionDataItem[]) {
|
public set documentCounts(counts: SelectionDataItem[]) {
|
||||||
this._documentCounts = counts
|
this._documentCounts = counts
|
||||||
|
if (this.documentCountSortingEnabled) {
|
||||||
|
this.sortItems()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _items: MatchingModel[] = []
|
private _items: MatchingModel[] = []
|
||||||
@@ -114,6 +127,13 @@ export class FilterableDropdownSelectionModel {
|
|||||||
b.id == NEGATIVE_NULL_FILTER_VALUE)
|
b.id == NEGATIVE_NULL_FILTER_VALUE)
|
||||||
) {
|
) {
|
||||||
return 1
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve hierarchical order when provided (e.g., Tags)
|
||||||
|
const ao = (a as any)['orderIndex']
|
||||||
|
const bo = (b as any)['orderIndex']
|
||||||
|
if (ao !== undefined && bo !== undefined) {
|
||||||
|
return ao - bo
|
||||||
} else if (
|
} else if (
|
||||||
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
|
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
|
||||||
this.getNonTemporary(b.id) != ToggleableItemState.NotSelected
|
this.getNonTemporary(b.id) != ToggleableItemState.NotSelected
|
||||||
@@ -136,17 +156,14 @@ export class FilterableDropdownSelectionModel {
|
|||||||
this.getDocumentCount(a.id) < this.getDocumentCount(b.id)
|
this.getDocumentCount(a.id) < this.getDocumentCount(b.id)
|
||||||
) {
|
) {
|
||||||
return 1
|
return 1
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve hierarchical order when provided (e.g., Tags)
|
|
||||||
const ao = (a as any)['orderIndex']
|
|
||||||
const bo = (b as any)['orderIndex']
|
|
||||||
if (ao !== undefined && bo !== undefined) {
|
|
||||||
return ao - bo
|
|
||||||
} else {
|
} else {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this._documentCounts.length) {
|
||||||
|
this.promoteBranchesWithDocumentCounts()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private selectionStates = new Map<number, ToggleableItemState>()
|
private selectionStates = new Map<number, ToggleableItemState>()
|
||||||
@@ -214,6 +231,7 @@ export class FilterableDropdownSelectionModel {
|
|||||||
state == ToggleableItemState.Excluded
|
state == ToggleableItemState.Excluded
|
||||||
) {
|
) {
|
||||||
this.temporarySelectionStates.delete(id)
|
this.temporarySelectionStates.delete(id)
|
||||||
|
this.clearDescendantSelections(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -240,6 +258,7 @@ export class FilterableDropdownSelectionModel {
|
|||||||
|
|
||||||
if (this.manyToOne || this.singleSelect) {
|
if (this.manyToOne || this.singleSelect) {
|
||||||
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
this.temporarySelectionStates.set(id, ToggleableItemState.Excluded)
|
||||||
|
this.clearDescendantSelections(id)
|
||||||
|
|
||||||
if (this.singleSelect) {
|
if (this.singleSelect) {
|
||||||
for (let key of this.temporarySelectionStates.keys()) {
|
for (let key of this.temporarySelectionStates.keys()) {
|
||||||
@@ -260,9 +279,15 @@ export class FilterableDropdownSelectionModel {
|
|||||||
newState = ToggleableItemState.NotSelected
|
newState = ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
this.temporarySelectionStates.set(id, newState)
|
this.temporarySelectionStates.set(id, newState)
|
||||||
|
if (newState == ToggleableItemState.Excluded) {
|
||||||
|
this.clearDescendantSelections(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (!id || state == ToggleableItemState.Excluded) {
|
} else if (!id || state == ToggleableItemState.Excluded) {
|
||||||
this.temporarySelectionStates.delete(id)
|
this.temporarySelectionStates.delete(id)
|
||||||
|
if (id) {
|
||||||
|
this.clearDescendantSelections(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
@@ -274,6 +299,33 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private clearDescendantSelections(id: number) {
|
||||||
|
for (const descendantID of this.getDescendantIDs(id)) {
|
||||||
|
this.temporarySelectionStates.delete(descendantID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDescendantIDs(id: number): number[] {
|
||||||
|
const descendants: number[] = []
|
||||||
|
const queue: number[] = [id]
|
||||||
|
|
||||||
|
while (queue.length) {
|
||||||
|
const parentID = queue.shift()
|
||||||
|
for (const item of this._items) {
|
||||||
|
if (
|
||||||
|
typeof item?.id === 'number' &&
|
||||||
|
typeof (item as any)['parent'] === 'number' &&
|
||||||
|
(item as any)['parent'] === parentID
|
||||||
|
) {
|
||||||
|
descendants.push(item.id)
|
||||||
|
queue.push(item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return descendants
|
||||||
|
}
|
||||||
|
|
||||||
get logicalOperator(): LogicalOperator {
|
get logicalOperator(): LogicalOperator {
|
||||||
return this.temporaryLogicalOperator
|
return this.temporaryLogicalOperator
|
||||||
}
|
}
|
||||||
@@ -380,6 +432,180 @@ export class FilterableDropdownSelectionModel {
|
|||||||
return this._documentCounts.find((c) => c.id === id)?.document_count
|
return this._documentCounts.find((c) => c.id === id)?.document_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private promoteBranchesWithDocumentCounts() {
|
||||||
|
const parentById = this.buildParentById()
|
||||||
|
const findRootId = this.createRootFinder(parentById)
|
||||||
|
const getRootDocCount = this.createRootDocCounter()
|
||||||
|
const summaries = this.buildBranchSummaries(findRootId, getRootDocCount)
|
||||||
|
const orderedBranches = this.orderBranchesByPriority(summaries)
|
||||||
|
|
||||||
|
this._items = orderedBranches.flatMap((summary) => summary.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildParentById(): Map<number, number | null> {
|
||||||
|
const parentById = new Map<number, number | null>()
|
||||||
|
|
||||||
|
for (const item of this._items) {
|
||||||
|
if (typeof item?.id === 'number') {
|
||||||
|
const parentValue = (item as any)['parent']
|
||||||
|
parentById.set(
|
||||||
|
item.id,
|
||||||
|
typeof parentValue === 'number' ? parentValue : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentById
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRootFinder(
|
||||||
|
parentById: Map<number, number | null>
|
||||||
|
): (id: number) => number {
|
||||||
|
const rootMemo = new Map<number, number>()
|
||||||
|
|
||||||
|
const findRootId = (id: number): number => {
|
||||||
|
const cached = rootMemo.get(id)
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentId = parentById.get(id)
|
||||||
|
if (parentId === undefined || parentId === null) {
|
||||||
|
rootMemo.set(id, id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootId = findRootId(parentId)
|
||||||
|
rootMemo.set(id, rootId)
|
||||||
|
return rootId
|
||||||
|
}
|
||||||
|
|
||||||
|
return findRootId
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRootDocCounter(): (rootId: number) => number {
|
||||||
|
const docCountMemo = new Map<number, number>()
|
||||||
|
|
||||||
|
return (rootId: number): number => {
|
||||||
|
const cached = docCountMemo.get(rootId)
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicit = this.getDocumentCount(rootId)
|
||||||
|
if (typeof explicit === 'number') {
|
||||||
|
docCountMemo.set(rootId, explicit)
|
||||||
|
return explicit
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootItem = this._items.find((i) => i.id === rootId)
|
||||||
|
const fallback =
|
||||||
|
typeof (rootItem as any)?.['document_count'] === 'number'
|
||||||
|
? (rootItem as any)['document_count']
|
||||||
|
: 0
|
||||||
|
|
||||||
|
docCountMemo.set(rootId, fallback)
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildBranchSummaries(
|
||||||
|
findRootId: (id: number) => number,
|
||||||
|
getRootDocCount: (rootId: number) => number
|
||||||
|
): Map<string, BranchSummary> {
|
||||||
|
const summaries = new Map<string, BranchSummary>()
|
||||||
|
|
||||||
|
for (const [index, item] of this._items.entries()) {
|
||||||
|
const { key, special, rootId } = this.describeBranchItem(
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
findRootId
|
||||||
|
)
|
||||||
|
|
||||||
|
let summary = summaries.get(key)
|
||||||
|
if (!summary) {
|
||||||
|
summary = {
|
||||||
|
items: [],
|
||||||
|
firstIndex: index,
|
||||||
|
special,
|
||||||
|
selected: false,
|
||||||
|
hasDocs:
|
||||||
|
special || rootId === null ? false : getRootDocCount(rootId) > 0,
|
||||||
|
}
|
||||||
|
summaries.set(key, summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.items.push(item)
|
||||||
|
|
||||||
|
if (this.shouldMarkSummarySelected(summary, item)) {
|
||||||
|
summary.selected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaries
|
||||||
|
}
|
||||||
|
|
||||||
|
private describeBranchItem(
|
||||||
|
item: MatchingModel,
|
||||||
|
index: number,
|
||||||
|
findRootId: (id: number) => number
|
||||||
|
): { key: string; special: boolean; rootId: number | null } {
|
||||||
|
if (item?.id === null) {
|
||||||
|
return { key: 'null', special: true, rootId: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item?.id === NEGATIVE_NULL_FILTER_VALUE) {
|
||||||
|
return { key: 'neg-null', special: true, rootId: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof item?.id === 'number') {
|
||||||
|
const rootId = findRootId(item.id)
|
||||||
|
return { key: `root-${rootId}`, special: false, rootId }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key: `misc-${index}`, special: false, rootId: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldMarkSummarySelected(
|
||||||
|
summary: BranchSummary,
|
||||||
|
item: MatchingModel
|
||||||
|
): boolean {
|
||||||
|
if (summary.special) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof item?.id !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getNonTemporary(item.id) !== ToggleableItemState.NotSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
private orderBranchesByPriority(
|
||||||
|
summaries: Map<string, BranchSummary>
|
||||||
|
): BranchSummary[] {
|
||||||
|
return Array.from(summaries.values()).sort((a, b) => {
|
||||||
|
const rankDiff = this.branchRank(a) - this.branchRank(b)
|
||||||
|
if (rankDiff !== 0) {
|
||||||
|
return rankDiff
|
||||||
|
}
|
||||||
|
if (a.hasDocs !== b.hasDocs) {
|
||||||
|
return a.hasDocs ? -1 : 1
|
||||||
|
}
|
||||||
|
return a.firstIndex - b.firstIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private branchRank(summary: BranchSummary): number {
|
||||||
|
if (summary.special) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (summary.selected) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
init(map: Map<number, ToggleableItemState>) {
|
init(map: Map<number, ToggleableItemState>) {
|
||||||
this.temporarySelectionStates = map
|
this.temporarySelectionStates = map
|
||||||
this.apply()
|
this.apply()
|
||||||
@@ -465,8 +691,9 @@ export class FilterableDropdownComponent
|
|||||||
this.selectionModel.changed.complete()
|
this.selectionModel.changed.complete()
|
||||||
model.items = this.selectionModel.items
|
model.items = this.selectionModel.items
|
||||||
model.manyToOne = this.selectionModel.manyToOne
|
model.manyToOne = this.selectionModel.manyToOne
|
||||||
model.singleSelect = this.editing && !this.selectionModel.manyToOne
|
model.singleSelect = this._editing && !model.manyToOne
|
||||||
}
|
}
|
||||||
|
model.documentCountSortingEnabled = this._editing
|
||||||
model.changed.subscribe((updatedModel) => {
|
model.changed.subscribe((updatedModel) => {
|
||||||
this.selectionModelChange.next(updatedModel)
|
this.selectionModelChange.next(updatedModel)
|
||||||
})
|
})
|
||||||
@@ -496,8 +723,21 @@ export class FilterableDropdownComponent
|
|||||||
@Input()
|
@Input()
|
||||||
allowSelectNone: boolean = false
|
allowSelectNone: boolean = false
|
||||||
|
|
||||||
|
private _editing = false
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
editing = false
|
set editing(value: boolean) {
|
||||||
|
this._editing = value
|
||||||
|
if (this.selectionModel) {
|
||||||
|
this.selectionModel.singleSelect =
|
||||||
|
this._editing && !this.selectionModel.manyToOne
|
||||||
|
this.selectionModel.documentCountSortingEnabled = this._editing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get editing() {
|
||||||
|
return this._editing
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
applyOnClose = false
|
applyOnClose = false
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (hint) {
|
@if (hint) {
|
||||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
<small class="form-text text-muted" [innerHTML]="hint"></small>
|
||||||
}
|
}
|
||||||
<div class="invalid-feedback position-absolute top-100">
|
<div class="invalid-feedback position-absolute top-100">
|
||||||
{{error}}
|
{{error}}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
}
|
}
|
||||||
<input #inputField type="hidden" class="form-control small" [(ngModel)]="value" [disabled]="true">
|
<input #inputField type="hidden" class="form-control small" [(ngModel)]="value" [disabled]="true">
|
||||||
@if (hint) {
|
@if (hint) {
|
||||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
<small class="form-text text-muted" [innerHTML]="hint"></small>
|
||||||
}
|
}
|
||||||
<div class="invalid-feedback position-absolute top-100">
|
<div class="invalid-feedback position-absolute top-100">
|
||||||
{{error}}
|
{{error}}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
{{error}}
|
{{error}}
|
||||||
</div>
|
</div>
|
||||||
@if (hint) {
|
@if (hint) {
|
||||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
<small class="form-text text-muted" [innerHTML]="hint"></small>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
Loaded 100 of 304 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user