mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
			affd25630e
			...
			feature-tr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 46345376b0 | ||
|   | 495159f0b2 | ||
|   | 33fd8a6579 | ||
|   | e08e34fb90 | ||
|   | 6164bac66e | ||
|   | df86882e8e | ||
|   | 79b30fbade | ||
|   | d609b386fe | ||
|   | 502bbb2420 | ||
|   | 27574009e1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | bd73555ecc | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 613c922dd2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1659aa08e4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 68dfb4a930 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3c439b970f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 962f7994d1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 93eea80f3e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 5bc27eb4b2 | ||
|   | b19701cb96 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9c552bc2d7 | ||
|   | 80fabb0b56 | ||
|   | af1c235af5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 92ee906701 | ||
|   | d6710de486 | ||
|   | f71b13b82a | ||
|   | 3df43d828a | ||
|   | 643e2b4a8e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6fa896df39 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6aeb5a5503 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 86dbeb3a27 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e97217f267 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 05d5d7e796 | ||
|   | ab7875cc76 | ||
|   | 0114993ac6 | 
							
								
								
									
										22
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -25,7 +25,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - name: Check if workflow should run |       - name: Check if workflow should run | ||||||
|         id: check |         id: check | ||||||
|         uses: actions/github-script@v7 |         uses: actions/github-script@v8 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ secrets.GITHUB_TOKEN }} |           github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           script: | |           script: | | ||||||
| @@ -69,7 +69,7 @@ jobs: | |||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|         uses: actions/checkout@v5 |         uses: actions/checkout@v5 | ||||||
|       - name: Install python |       - name: Install python | ||||||
|         uses: actions/setup-python@v5 |         uses: actions/setup-python@v6 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} |           python-version: ${{ env.DEFAULT_PYTHON_VERSION }} | ||||||
|       - name: Check files |       - name: Check files | ||||||
| @@ -84,7 +84,7 @@ jobs: | |||||||
|         uses: actions/checkout@v5 |         uses: actions/checkout@v5 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         id: setup-python |         id: setup-python | ||||||
|         uses: actions/setup-python@v5 |         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 | ||||||
| @@ -138,7 +138,7 @@ jobs: | |||||||
|           docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml up --detach |           docker compose --file ${{ github.workspace }}/docker/compose/docker-compose.ci-test.yml up --detach | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         id: setup-python |         id: setup-python | ||||||
|         uses: actions/setup-python@v5 |         uses: actions/setup-python@v6 | ||||||
|         with: |         with: | ||||||
|           python-version: "${{ matrix.python-version }}" |           python-version: "${{ matrix.python-version }}" | ||||||
|       - name: Install uv |       - name: Install uv | ||||||
| @@ -207,7 +207,7 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           version: 10 |           version: 10 | ||||||
|       - name: Use Node.js 20 |       - name: Use Node.js 20 | ||||||
|         uses: actions/setup-node@v4 |         uses: actions/setup-node@v5 | ||||||
|         with: |         with: | ||||||
|           node-version: 20.x |           node-version: 20.x | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
| @@ -240,7 +240,7 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           version: 10 |           version: 10 | ||||||
|       - name: Use Node.js 20 |       - name: Use Node.js 20 | ||||||
|         uses: actions/setup-node@v4 |         uses: actions/setup-node@v5 | ||||||
|         with: |         with: | ||||||
|           node-version: 20.x |           node-version: 20.x | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
| @@ -288,7 +288,7 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           version: 10 |           version: 10 | ||||||
|       - name: Use Node.js 20 |       - name: Use Node.js 20 | ||||||
|         uses: actions/setup-node@v4 |         uses: actions/setup-node@v5 | ||||||
|         with: |         with: | ||||||
|           node-version: 20.x |           node-version: 20.x | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
| @@ -331,7 +331,7 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           version: 10 |           version: 10 | ||||||
|       - name: Use Node.js 20 |       - name: Use Node.js 20 | ||||||
|         uses: actions/setup-node@v4 |         uses: actions/setup-node@v5 | ||||||
|         with: |         with: | ||||||
|           node-version: 20.x |           node-version: 20.x | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
| @@ -473,7 +473,7 @@ jobs: | |||||||
|         uses: actions/checkout@v5 |         uses: actions/checkout@v5 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         id: setup-python |         id: setup-python | ||||||
|         uses: actions/setup-python@v5 |         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 | ||||||
| @@ -621,7 +621,7 @@ jobs: | |||||||
|           ref: main |           ref: main | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         id: setup-python |         id: setup-python | ||||||
|         uses: actions/setup-python@v5 |         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 | ||||||
| @@ -653,7 +653,7 @@ jobs: | |||||||
|           git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA" |           git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA" | ||||||
|           git push origin ${{ needs.publish-release.outputs.version }}-changelog |           git push origin ${{ needs.publish-release.outputs.version }}-changelog | ||||||
|       - name: Create Pull Request |       - name: Create Pull Request | ||||||
|         uses: actions/github-script@v7 |         uses: actions/github-script@v8 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             const { repo, owner } = context.repo; |             const { repo, owner } = context.repo; | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.github/workflows/cleanup-tags.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/cleanup-tags.yml
									
									
									
									
										vendored
									
									
								
							| @@ -6,10 +6,9 @@ | |||||||
| # This workflow will not trigger runs on forked repos. | # This workflow will not trigger runs on forked repos. | ||||||
| name: Cleanup Image Tags | name: Cleanup Image Tags | ||||||
| on: | on: | ||||||
|   delete: |   workflow_dispatch: | ||||||
|   push: |   schedule: | ||||||
|     paths: |     - cron: '0 0 * * 0' | ||||||
|       - ".github/workflows/cleanup-tags.yml" |  | ||||||
| concurrency: | concurrency: | ||||||
|   group: registry-tags-cleanup |   group: registry-tags-cleanup | ||||||
|   cancel-in-progress: false |   cancel-in-progress: false | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.github/workflows/pr-bot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/pr-bot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,7 +12,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - name: Label PR by file path or branch name |       - name: Label PR by file path or branch name | ||||||
|         # see .github/labeler.yml for the labeler config |         # see .github/labeler.yml for the labeler config | ||||||
|         uses: actions/labeler@v5 |         uses: actions/labeler@v6 | ||||||
|         with: |         with: | ||||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} |           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|       - name: Label by size |       - name: Label by size | ||||||
| @@ -26,7 +26,7 @@ jobs: | |||||||
|           fail_if_xl: 'false' |           fail_if_xl: 'false' | ||||||
|           excluded_files: /\.lock$/ /\.txt$/ ^src-ui/pnpm-lock\.yaml$ ^src-ui/messages\.xlf$ ^src/locale/en_US/LC_MESSAGES/django\.po$ |           excluded_files: /\.lock$/ /\.txt$/ ^src-ui/pnpm-lock\.yaml$ ^src-ui/messages\.xlf$ ^src/locale/en_US/LC_MESSAGES/django\.po$ | ||||||
|       - name: Label by PR title |       - name: Label by PR title | ||||||
|         uses: actions/github-script@v7 |         uses: actions/github-script@v8 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             const pr = context.payload.pull_request; |             const pr = context.payload.pull_request; | ||||||
| @@ -52,7 +52,7 @@ jobs: | |||||||
|             } |             } | ||||||
|       - name: Label bot-generated PRs |       - name: Label bot-generated PRs | ||||||
|         if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }} |         if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }} | ||||||
|         uses: actions/github-script@v7 |         uses: actions/github-script@v8 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             const pr = context.payload.pull_request; |             const pr = context.payload.pull_request; | ||||||
| @@ -77,7 +77,7 @@ jobs: | |||||||
|             } |             } | ||||||
|       - name: Welcome comment |       - name: Welcome comment | ||||||
|         if: ${{ !contains(github.actor, 'bot') }} |         if: ${{ !contains(github.actor, 'bot') }} | ||||||
|         uses: actions/github-script@v7 |         uses: actions/github-script@v8 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             const pr = context.payload.pull_request; |             const pr = context.payload.pull_request; | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.github/workflows/repo-maintenance.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/repo-maintenance.yml
									
									
									
									
										vendored
									
									
								
							| @@ -15,7 +15,7 @@ jobs: | |||||||
|     if: github.repository_owner == 'paperless-ngx' |     if: github.repository_owner == 'paperless-ngx' | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/stale@v9 |       - uses: actions/stale@v10 | ||||||
|         with: |         with: | ||||||
|           days-before-stale: 7 |           days-before-stale: 7 | ||||||
|           days-before-close: 14 |           days-before-close: 14 | ||||||
| @@ -57,7 +57,7 @@ jobs: | |||||||
|     if: github.repository_owner == 'paperless-ngx' |     if: github.repository_owner == 'paperless-ngx' | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/github-script@v7 |       - uses: actions/github-script@v8 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             function sleep(ms) { |             function sleep(ms) { | ||||||
| @@ -114,7 +114,7 @@ jobs: | |||||||
|     if: github.repository_owner == 'paperless-ngx' |     if: github.repository_owner == 'paperless-ngx' | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/github-script@v7 |       - uses: actions/github-script@v8 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             function sleep(ms) { |             function sleep(ms) { | ||||||
| @@ -206,7 +206,7 @@ jobs: | |||||||
|     if: github.repository_owner == 'paperless-ngx' |     if: github.repository_owner == 'paperless-ngx' | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/github-script@v7 |       - uses: actions/github-script@v8 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             function sleep(ms) { |             function sleep(ms) { | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/translate-strings.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/translate-strings.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | |||||||
|           ref: ${{ github.head_ref }} |           ref: ${{ github.head_ref }} | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         id: setup-python |         id: setup-python | ||||||
|         uses: actions/setup-python@v5 |         uses: actions/setup-python@v6 | ||||||
|       - name: Install system dependencies |       - name: Install system dependencies | ||||||
|         run: | |         run: | | ||||||
|           sudo apt-get update -qq |           sudo apt-get update -qq | ||||||
| @@ -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@v4 |         uses: actions/setup-node@v5 | ||||||
|         with: |         with: | ||||||
|           node-version: 20.x |           node-version: 20.x | ||||||
|           cache: 'pnpm' |           cache: 'pnpm' | ||||||
|   | |||||||
| @@ -49,7 +49,7 @@ 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.13.0 |     rev: v0.13.2 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: ruff-check |       - id: ruff-check | ||||||
|       - id: ruff-format |       - id: ruff-format | ||||||
| @@ -59,7 +59,7 @@ repos: | |||||||
|       - id: pyproject-fmt |       - id: pyproject-fmt | ||||||
|   # Dockerfile hooks |   # Dockerfile hooks | ||||||
|   - repo: https://github.com/AleksaC/hadolint-py |   - repo: https://github.com/AleksaC/hadolint-py | ||||||
|     rev: v2.12.1b3 |     rev: v2.14.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: hadolint |       - id: hadolint | ||||||
|   # Shell script hooks |   # Shell script hooks | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -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 | ||||||
|  |  | ||||||
| @@ -170,20 +170,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-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" \ | ||||||
|   | |||||||
							
								
								
									
										319
									
								
								dev.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										319
									
								
								dev.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,319 @@ | |||||||
|  | adduser 3.134 | ||||||
|  | apt 2.6.1 | ||||||
|  | base-files 12.4+deb12u11 | ||||||
|  | base-passwd 3.6.1 | ||||||
|  | bash 5.2.15-2+b8 | ||||||
|  | bsdutils 1:2.38.1-5+deb12u3 | ||||||
|  | ca-certificates 20230311+deb12u1 | ||||||
|  | coreutils 9.1-1 | ||||||
|  | curl 7.88.1-10+deb12u12 | ||||||
|  | dash 0.5.12-2 | ||||||
|  | debconf 1.5.82 | ||||||
|  | debian-archive-keyring 2023.3+deb12u2 | ||||||
|  | debianutils 5.7-0.5~deb12u1 | ||||||
|  | diffutils 1:3.8-4 | ||||||
|  | dirmngr 2.2.40-1.1 | ||||||
|  | dpkg 1.21.22 | ||||||
|  | e2fsprogs 1.47.0-2 | ||||||
|  | file 1:5.44-3 | ||||||
|  | findutils 4.9.0-4 | ||||||
|  | fontconfig 2.14.1-4 | ||||||
|  | fontconfig-config 2.14.1-4 | ||||||
|  | fonts-liberation 1:1.07.4-11 | ||||||
|  | fonts-urw-base35 20200910-7 | ||||||
|  | gcc-12-base 12.2.0-14+deb12u1 | ||||||
|  | gettext 0.21-12 | ||||||
|  | gettext-base 0.21-12 | ||||||
|  | ghostscript 10.03.1~dfsg-1 | ||||||
|  | gnupg 2.2.40-1.1 | ||||||
|  | gnupg-l10n 2.2.40-1.1 | ||||||
|  | gnupg-utils 2.2.40-1.1 | ||||||
|  | gosu 1.14-1+b10 | ||||||
|  | gpg 2.2.40-1.1 | ||||||
|  | gpg-agent 2.2.40-1.1 | ||||||
|  | gpg-wks-client 2.2.40-1.1 | ||||||
|  | gpg-wks-server 2.2.40-1.1 | ||||||
|  | gpgconf 2.2.40-1.1 | ||||||
|  | gpgsm 2.2.40-1.1 | ||||||
|  | gpgv 2.2.40-1.1 | ||||||
|  | grep 3.8-5 | ||||||
|  | gzip 1.12-1 | ||||||
|  | hicolor-icon-theme 0.17-2 | ||||||
|  | hostname 3.23+nmu1 | ||||||
|  | icc-profiles-free 2.0.1+dfsg-1.1 | ||||||
|  | imagemagick 8:6.9.11.60+dfsg-1.6+deb12u3 | ||||||
|  | imagemagick-6-common 8:6.9.11.60+dfsg-1.6+deb12u3 | ||||||
|  | imagemagick-6.q16 8:6.9.11.60+dfsg-1.6+deb12u3 | ||||||
|  | init-system-helpers 1.65.2 | ||||||
|  | jbig2dec 0.19-3 | ||||||
|  | jbig2enc 0.30-1 | ||||||
|  | libacl1 2.3.1-3 | ||||||
|  | libaom3 3.6.0-1+deb12u1 | ||||||
|  | libapt-pkg6.0 2.6.1 | ||||||
|  | libarchive13 3.6.2-1+deb12u2 | ||||||
|  | libassuan0 2.5.5-5 | ||||||
|  | libattr1 1:2.5.1-4 | ||||||
|  | libaudit-common 1:3.0.9-1 | ||||||
|  | libaudit1 1:3.0.9-1 | ||||||
|  | libavahi-client3 0.8-10+deb12u1 | ||||||
|  | libavahi-common-data 0.8-10+deb12u1 | ||||||
|  | libavahi-common3 0.8-10+deb12u1 | ||||||
|  | libavcodec59 7:5.1.6-0+deb12u1 | ||||||
|  | libavformat59 7:5.1.6-0+deb12u1 | ||||||
|  | libavutil57 7:5.1.6-0+deb12u1 | ||||||
|  | libblkid1 2.38.1-5+deb12u3 | ||||||
|  | libbluray2 1:1.3.4-1 | ||||||
|  | libbrotli1 1.0.9-2+b6 | ||||||
|  | libbsd0 0.11.7-2 | ||||||
|  | libbz2-1.0 1.0.8-5+b1 | ||||||
|  | libc-bin 2.36-9+deb12u10 | ||||||
|  | libc6 2.36-9+deb12u10 | ||||||
|  | libcairo-gobject2 1.16.0-7 | ||||||
|  | libcairo2 1.16.0-7 | ||||||
|  | libcap-ng0 0.8.3-1+b3 | ||||||
|  | libcap2 1:2.66-4+deb12u1 | ||||||
|  | libchromaprint1 1.5.1-2+b1 | ||||||
|  | libcjson1 1.7.15-1+deb12u2 | ||||||
|  | libcodec2-1.0 1.0.5-1 | ||||||
|  | libcom-err2 1.47.0-2 | ||||||
|  | libconfig-inifiles-perl 3.000003-2 | ||||||
|  | libcrypt1 1:4.4.33-2 | ||||||
|  | libcups2 2.4.2-3+deb12u8 | ||||||
|  | libcurl4 7.88.1-10+deb12u12 | ||||||
|  | libdatrie1 0.2.13-2+b1 | ||||||
|  | libdav1d6 1.0.0-2+deb12u1 | ||||||
|  | libdb5.3 5.3.28+dfsg2-1 | ||||||
|  | libdbus-1-3 1.14.10-1~deb12u1 | ||||||
|  | libde265-0 1.0.11-1+deb12u2 | ||||||
|  | libdebconfclient0 0.270 | ||||||
|  | libdeflate0 1.14-1 | ||||||
|  | libdrm-common 2.4.114-1 | ||||||
|  | libdrm2 2.4.114-1+b1 | ||||||
|  | libedit2 3.1-20221030-2 | ||||||
|  | libexpat1 2.5.0-1+deb12u1 | ||||||
|  | libext2fs2 1.47.0-2 | ||||||
|  | libffi8 3.4.4-1 | ||||||
|  | libfftw3-double3 3.3.10-1 | ||||||
|  | libfontconfig1 2.14.1-4 | ||||||
|  | libfontenc1 1:1.1.4-1 | ||||||
|  | libfreetype6 2.12.1+dfsg-5+deb12u4 | ||||||
|  | libfribidi0 1.0.8-2.1 | ||||||
|  | libgcc-s1 12.2.0-14+deb12u1 | ||||||
|  | libgcrypt20 1.10.1-3 | ||||||
|  | libgdbm-compat4 1.23-3 | ||||||
|  | libgdbm6 1.23-3 | ||||||
|  | libgdk-pixbuf-2.0-0 2.42.10+dfsg-1+deb12u2 | ||||||
|  | libgdk-pixbuf2.0-common 2.42.10+dfsg-1+deb12u2 | ||||||
|  | libgif7 5.2.1-2.5 | ||||||
|  | libglib2.0-0 2.74.6-2+deb12u6 | ||||||
|  | libgme0 0.6.3-6 | ||||||
|  | libgmp10 2:6.2.1+dfsg1-1.1 | ||||||
|  | libgnutls30 3.7.9-2+deb12u5 | ||||||
|  | libgomp1 12.2.0-14+deb12u1 | ||||||
|  | libgpg-error0 1.46-1 | ||||||
|  | libgraphite2-3 1.3.14-1 | ||||||
|  | libgs-common 10.0.0~dfsg-11+deb12u7 | ||||||
|  | libgs10 10.03.1~dfsg-1 | ||||||
|  | libgs10-common 10.03.1~dfsg-1 | ||||||
|  | libgsm1 1.0.22-1 | ||||||
|  | libgssapi-krb5-2 1.20.1-2+deb12u3 | ||||||
|  | libharfbuzz0b 6.0.0+dfsg-3 | ||||||
|  | libheif1 1.15.1-1+deb12u1 | ||||||
|  | libhogweed6 3.8.1-2 | ||||||
|  | libhwy1 1.0.3-3+deb12u1 | ||||||
|  | libice6 2:1.0.10-1 | ||||||
|  | libicu72 72.1-3+deb12u1 | ||||||
|  | libidn12 1.41-1 | ||||||
|  | libidn2-0 2.3.3-1+b1 | ||||||
|  | libijs-0.35 0.35-15 | ||||||
|  | libimagequant0 2.17.0-1 | ||||||
|  | libjbig0 2.1-6.1 | ||||||
|  | libjbig2dec0 0.19-3 | ||||||
|  | libjpeg62-turbo 1:2.1.5-2 | ||||||
|  | libjxl0.7 0.7.0-10+deb12u1 | ||||||
|  | libk5crypto3 1.20.1-2+deb12u3 | ||||||
|  | libkeyutils1 1.6.3-2 | ||||||
|  | libkrb5-3 1.20.1-2+deb12u3 | ||||||
|  | libkrb5support0 1.20.1-2+deb12u3 | ||||||
|  | libksba8 1.6.3-2 | ||||||
|  | liblcms2-2 2.14-2 | ||||||
|  | libldap-2.5-0 2.5.13+dfsg-5 | ||||||
|  | liblept5 1.82.0-3+b3 | ||||||
|  | liblerc4 4.0.0+ds-2 | ||||||
|  | liblqr-1-0 0.4.2-2.1 | ||||||
|  | libltdl7 2.4.7-7~deb12u1 | ||||||
|  | liblz4-1 1.9.4-1 | ||||||
|  | liblzma5 5.4.1-1 | ||||||
|  | libmagic-mgc 1:5.44-3 | ||||||
|  | libmagic1 1:5.44-3 | ||||||
|  | libmagickcore-6.q16-6 8:6.9.11.60+dfsg-1.6+deb12u3 | ||||||
|  | libmagickwand-6.q16-6 8:6.9.11.60+dfsg-1.6+deb12u3 | ||||||
|  | libmariadb3 1:10.11.11-0+deb12u1 | ||||||
|  | libmbedcrypto7 2.28.3-1 | ||||||
|  | libmd0 1.0.4-2 | ||||||
|  | libmfx1 22.5.4-1 | ||||||
|  | libmount1 2.38.1-5+deb12u3 | ||||||
|  | libmp3lame0 3.100-6 | ||||||
|  | libmpg123-0 1.31.2-1+deb12u1 | ||||||
|  | libncurses6 6.4-4 | ||||||
|  | libncursesw6 6.4-4 | ||||||
|  | libnettle8 3.8.1-2 | ||||||
|  | libnghttp2-14 1.52.0-1+deb12u2 | ||||||
|  | libnorm1 1.5.9+dfsg-2 | ||||||
|  | libnpth0 1.6-3 | ||||||
|  | libnsl2 1.3.0-2 | ||||||
|  | libnspr4 2:4.35-1 | ||||||
|  | libnss3 2:3.87.1-1+deb12u1 | ||||||
|  | libnuma1 2.0.16-1 | ||||||
|  | libogg0 1.3.5-3 | ||||||
|  | libopenjp2-7 2.5.0-2+deb12u1 | ||||||
|  | libopenmpt0 0.6.9-1 | ||||||
|  | libopus0 1.3.1-3 | ||||||
|  | libp11-kit0 0.24.1-2 | ||||||
|  | libpam-modules 1.5.2-6+deb12u1 | ||||||
|  | libpam-modules-bin 1.5.2-6+deb12u1 | ||||||
|  | libpam-runtime 1.5.2-6+deb12u1 | ||||||
|  | libpam0g 1.5.2-6+deb12u1 | ||||||
|  | libpango-1.0-0 1.50.12+ds-1 | ||||||
|  | libpangocairo-1.0-0 1.50.12+ds-1 | ||||||
|  | libpangoft2-1.0-0 1.50.12+ds-1 | ||||||
|  | libpaper1 1.1.29 | ||||||
|  | libpcre2-8-0 10.42-1 | ||||||
|  | libperl5.36 5.36.0-7+deb12u2 | ||||||
|  | libpgm-5.3-0 5.3.128~dfsg-2 | ||||||
|  | libpixman-1-0 0.42.2-1 | ||||||
|  | libpng16-16 1.6.39-2 | ||||||
|  | libpoppler126 22.12.0-2+deb12u1 | ||||||
|  | libpq5 15.13-0+deb12u1 | ||||||
|  | libpsl5 0.21.2-1 | ||||||
|  | libqpdf29 11.9.0-1 | ||||||
|  | librabbitmq4 0.11.0-1+deb12u1 | ||||||
|  | librav1e0 0.5.1-6 | ||||||
|  | libreadline8 8.2-1.3 | ||||||
|  | librist4 0.2.7+dfsg-1 | ||||||
|  | librsvg2-2 2.54.7+dfsg-1~deb12u1 | ||||||
|  | librtmp1 2.4+20151223.gitfa8646d.1-2+b2 | ||||||
|  | libsasl2-2 2.1.28+dfsg-10 | ||||||
|  | libsasl2-modules-db 2.1.28+dfsg-10 | ||||||
|  | libseccomp2 2.5.4-1+deb12u1 | ||||||
|  | libselinux1 3.4-1+b6 | ||||||
|  | libsemanage-common 3.4-1 | ||||||
|  | libsemanage2 3.4-1+b5 | ||||||
|  | libsepol2 3.4-2.1 | ||||||
|  | libshine3 3.1.1-2 | ||||||
|  | libsm6 2:1.2.3-1 | ||||||
|  | libsmartcols1 2.38.1-5+deb12u3 | ||||||
|  | libsnappy1v5 1.1.9-3 | ||||||
|  | libsodium23 1.0.18-1 | ||||||
|  | libsoxr0 0.1.3-4 | ||||||
|  | libspeex1 1.2.1-2 | ||||||
|  | libsqlite3-0 3.40.1-2+deb12u1 | ||||||
|  | libsrt1.5-gnutls 1.5.1-1+deb12u1 | ||||||
|  | libss2 1.47.0-2 | ||||||
|  | libssh-gcrypt-4 0.10.6-0+deb12u1 | ||||||
|  | libssh2-1 1.10.0-3+b1 | ||||||
|  | libssl3 3.0.17-1~deb12u1 | ||||||
|  | libstdc++6 12.2.0-14+deb12u1 | ||||||
|  | libsvtav1enc1 1.4.1+dfsg-1 | ||||||
|  | libswresample4 7:5.1.6-0+deb12u1 | ||||||
|  | libsystemd0 252.38-1~deb12u1 | ||||||
|  | libtasn1-6 4.19.0-2+deb12u1 | ||||||
|  | libtesseract5 5.3.0-2 | ||||||
|  | libthai-data 0.1.29-1 | ||||||
|  | libthai0 0.1.29-1 | ||||||
|  | libtheora0 1.1.1+dfsg.1-16.1+b1 | ||||||
|  | libtiff6 4.5.0-6+deb12u2 | ||||||
|  | libtinfo6 6.4-4 | ||||||
|  | libtirpc-common 1.3.3+ds-1 | ||||||
|  | libtirpc3 1.3.3+ds-1 | ||||||
|  | libtwolame0 0.4.0-2 | ||||||
|  | libudev1 252.38-1~deb12u1 | ||||||
|  | libudfread0 1.1.2-1 | ||||||
|  | libunistring2 1.0-2 | ||||||
|  | libuuid1 2.38.1-5+deb12u3 | ||||||
|  | libv4l-0 1.22.1-5+b2 | ||||||
|  | libv4lconvert0 1.22.1-5+b2 | ||||||
|  | libva-drm2 2.17.0-1 | ||||||
|  | libva-x11-2 2.17.0-1 | ||||||
|  | libva2 2.17.0-1 | ||||||
|  | libvdpau1 1.5-2 | ||||||
|  | libvorbis0a 1.3.7-1 | ||||||
|  | libvorbisenc2 1.3.7-1 | ||||||
|  | libvorbisfile3 1.3.7-1 | ||||||
|  | libvpx7 1.12.0-1+deb12u4 | ||||||
|  | libwebp7 1.2.4-0.2+deb12u1 | ||||||
|  | libwebpdemux2 1.2.4-0.2+deb12u1 | ||||||
|  | libwebpmux3 1.2.4-0.2+deb12u1 | ||||||
|  | libx11-6 2:1.8.4-2+deb12u2 | ||||||
|  | libx11-data 2:1.8.4-2+deb12u2 | ||||||
|  | libx11-xcb1 2:1.8.4-2+deb12u2 | ||||||
|  | libx264-164 2:0.164.3095+gitbaee400-3 | ||||||
|  | libx265-199 3.5-2+b1 | ||||||
|  | libxau6 1:1.0.9-1 | ||||||
|  | libxcb-dri3-0 1.15-1 | ||||||
|  | libxcb-render0 1.15-1 | ||||||
|  | libxcb-shm0 1.15-1 | ||||||
|  | libxcb1 1.15-1 | ||||||
|  | libxdmcp6 1:1.1.2-3 | ||||||
|  | libxext6 2:1.3.4-1+b1 | ||||||
|  | libxfixes3 1:6.0.0-2 | ||||||
|  | libxml2 2.9.14+dfsg-1.3~deb12u2 | ||||||
|  | libxrender1 1:0.9.10-1.1 | ||||||
|  | libxslt1.1 1.1.35-1+deb12u1 | ||||||
|  | libxt6 1:1.2.1-1.1 | ||||||
|  | libxvidcore4 2:1.3.7-1 | ||||||
|  | libxxhash0 0.8.1-1 | ||||||
|  | libzbar0 0.23.92-7+deb12u1 | ||||||
|  | libzmq5 4.3.4-6 | ||||||
|  | libzstd1 1.5.4+dfsg2-5 | ||||||
|  | libzvbi-common 0.2.41-1 | ||||||
|  | libzvbi0 0.2.41-1 | ||||||
|  | login 1:4.13+dfsg1-1+deb12u1 | ||||||
|  | logsave 1.47.0-2 | ||||||
|  | mariadb-client 1:10.11.11-0+deb12u1 | ||||||
|  | mariadb-client-core 1:10.11.11-0+deb12u1 | ||||||
|  | mariadb-common 1:10.11.11-0+deb12u1 | ||||||
|  | mawk 1.3.4.20200120-3.1 | ||||||
|  | media-types 10.0.0 | ||||||
|  | mount 2.38.1-5+deb12u3 | ||||||
|  | mysql-common 5.8+1.1.0 | ||||||
|  | ncurses-base 6.4-4 | ||||||
|  | ncurses-bin 6.4-4 | ||||||
|  | netbase 6.4 | ||||||
|  | ocl-icd-libopencl1 2.3.1-1 | ||||||
|  | openssl 3.0.17-1~deb12u1 | ||||||
|  | passwd 1:4.13+dfsg1-1+deb12u1 | ||||||
|  | perl 5.36.0-7+deb12u2 | ||||||
|  | perl-base 5.36.0-7+deb12u2 | ||||||
|  | perl-modules-5.36 5.36.0-7+deb12u2 | ||||||
|  | pinentry-curses 1.2.1-1 | ||||||
|  | pngquant 2.17.0-1 | ||||||
|  | poppler-data 0.4.12-1 | ||||||
|  | poppler-utils 22.12.0-2+deb12u1 | ||||||
|  | postgresql-client 15+248 | ||||||
|  | postgresql-client-15 15.13-0+deb12u1 | ||||||
|  | postgresql-client-common 248 | ||||||
|  | qpdf 11.9.0-1 | ||||||
|  | readline-common 8.2-1.3 | ||||||
|  | sed 4.9-1 | ||||||
|  | sensible-utils 0.0.17+nmu1 | ||||||
|  | shared-mime-info 2.2-1 | ||||||
|  | sysvinit-utils 3.06-4 | ||||||
|  | tar 1.34+dfsg-1.2+deb12u1 | ||||||
|  | tesseract-ocr 5.3.0-2 | ||||||
|  | tesseract-ocr-deu 1:4.1.0-2 | ||||||
|  | tesseract-ocr-eng 1:4.1.0-2 | ||||||
|  | tesseract-ocr-fra 1:4.1.0-2 | ||||||
|  | tesseract-ocr-ita 1:4.1.0-2 | ||||||
|  | tesseract-ocr-osd 1:4.1.0-2 | ||||||
|  | tesseract-ocr-spa 1:4.1.0-2 | ||||||
|  | tzdata 2025b-0+deb12u1 | ||||||
|  | ucf 3.0043+nmu1+deb12u1 | ||||||
|  | unpaper 7.0.0-0.1 | ||||||
|  | usr-is-merged 37~deb12u1 | ||||||
|  | util-linux 2.38.1-5+deb12u3 | ||||||
|  | util-linux-extra 2.38.1-5+deb12u3 | ||||||
|  | x11-common 1:7.7+23 | ||||||
|  | xfonts-encodings 1:1.0.4-2.2 | ||||||
|  | xfonts-utils 1:7.7+6 | ||||||
|  | zlib1g 1:1.2.13.dfsg-1 | ||||||
| @@ -32,7 +32,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redisdata:/data |       - redisdata:/data | ||||||
|   db: |   db: | ||||||
|     image: docker.io/library/postgres:17 |     image: docker.io/library/postgres:18 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - pgdata:/var/lib/postgresql/data |       - pgdata:/var/lib/postgresql/data | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redisdata:/data |       - redisdata:/data | ||||||
|   db: |   db: | ||||||
|     image: docker.io/library/postgres:17 |     image: docker.io/library/postgres:18 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - pgdata:/var/lib/postgresql/data |       - pgdata:/var/lib/postgresql/data | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ services: | |||||||
|     volumes: |     volumes: | ||||||
|       - redisdata:/data |       - redisdata:/data | ||||||
|   db: |   db: | ||||||
|     image: docker.io/library/postgres:17 |     image: docker.io/library/postgres:18 | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     volumes: |     volumes: | ||||||
|       - pgdata:/var/lib/postgresql/data |       - pgdata:/var/lib/postgresql/data | ||||||
|   | |||||||
| @@ -170,11 +170,11 @@ Available options are `postgresql` and `mariadb`. | |||||||
|  |  | ||||||
|     !!! note |     !!! note | ||||||
|  |  | ||||||
|     A small pool is typically sufficient — for example, a size of 4. |         A small pool is typically sufficient — for example, a size of 4. | ||||||
|     Make sure your PostgreSQL server's max_connections setting is large enough to handle: |         Make sure your PostgreSQL server's max_connections setting is large enough to handle: | ||||||
|     ```(Paperless workers + Celery workers) × pool size + safety margin``` |         ```(Paperless workers + Celery workers) × pool size + safety margin``` | ||||||
|     For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4: |         For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4: | ||||||
|     (4 + 2) × 4 + 10 = 34 connections required. |         (4 + 2) × 4 + 10 = 34 connections required. | ||||||
|  |  | ||||||
| #### [`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} | ||||||
|  |  | ||||||
| @@ -184,9 +184,9 @@ Available options are `postgresql` and `mariadb`. | |||||||
|  |  | ||||||
|     !!! danger |     !!! danger | ||||||
|  |  | ||||||
|     **Do not modify the database outside the application while it is running.** |         **Do not modify the database outside the application while it is running.** | ||||||
|     This includes actions such as restoring a backup, upgrading the database, or performing manual inserts. All external modifications must be done **only when the application is stopped**. |         This includes actions such as restoring a backup, upgrading the database, or performing manual inserts. All external modifications must be done **only when the application is stopped**. | ||||||
|     After making any such changes, you **must invalidate the DB read cache** using the `invalidate_cachalot` management command. |         After making any such changes, you **must invalidate the DB read cache** using the `invalidate_cachalot` management command. | ||||||
|  |  | ||||||
| #### [`PAPERLESS_READ_CACHE_TTL=<int>`](#PAPERLESS_READ_CACHE_TTL) {#PAPERLESS_READ_CACHE_TTL} | #### [`PAPERLESS_READ_CACHE_TTL=<int>`](#PAPERLESS_READ_CACHE_TTL) {#PAPERLESS_READ_CACHE_TTL} | ||||||
|  |  | ||||||
| @@ -196,7 +196,7 @@ Available options are `postgresql` and `mariadb`. | |||||||
|  |  | ||||||
|     !!! warning |     !!! warning | ||||||
|  |  | ||||||
|     A high TTL increases memory usage over time. Memory may be used until end of TTL, even if the cache is invalidated with the `invalidate_cachalot` command. |         A high TTL increases memory usage over time. Memory may be used until end of TTL, even if the cache is invalidated with the `invalidate_cachalot` command. | ||||||
|  |  | ||||||
| In case of an out-of-memory (OOM) situation, Redis may stop accepting new data — including cache entries, scheduled tasks, and documents to consume. | In case of an out-of-memory (OOM) situation, Redis may stop accepting new data — including cache entries, scheduled tasks, and documents to consume. | ||||||
| If your system has limited RAM, consider configuring a dedicated Redis instance for the read cache, with a memory limit and the eviction policy set to `allkeys-lru`. | If your system has limited RAM, consider configuring a dedicated Redis instance for the read cache, with a memory limit and the eviction policy set to `allkeys-lru`. | ||||||
|   | |||||||
| @@ -414,7 +414,7 @@ fields and permissions, which will be merged. | |||||||
|  |  | ||||||
| #### Types {#workflow-trigger-types} | #### Types {#workflow-trigger-types} | ||||||
|  |  | ||||||
| Currently, there are three events that correspond to workflow trigger 'types': | Currently, there are four events that correspond to workflow trigger 'types': | ||||||
|  |  | ||||||
| 1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption | 1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption | ||||||
|    folder or API), file path, file name, mail rule |    folder or API), file path, file name, mail rule | ||||||
| @@ -427,7 +427,7 @@ Currently, there are three events that correspond to workflow trigger 'types': | |||||||
|    added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive |    added, created, updated date or you can specify a (date) custom field. You can also specify a day offset from the date (positive | ||||||
|    offsets will trigger after the date, negative offsets will trigger before). |    offsets will trigger after the date, negative offsets will trigger before). | ||||||
|  |  | ||||||
| The following flow diagram illustrates the three document trigger types: | The following flow diagram illustrates the four document trigger types: | ||||||
|  |  | ||||||
| ```mermaid | ```mermaid | ||||||
| flowchart TD | flowchart TD | ||||||
| @@ -637,7 +637,7 @@ When you first delete a document it is moved to the 'trash' until either it is e | |||||||
| You can set how long documents remain in the trash before being automatically deleted with [`PAPERLESS_EMPTY_TRASH_DELAY`](configuration.md#PAPERLESS_EMPTY_TRASH_DELAY), which defaults | You can set how long documents remain in the trash before being automatically deleted with [`PAPERLESS_EMPTY_TRASH_DELAY`](configuration.md#PAPERLESS_EMPTY_TRASH_DELAY), which defaults | ||||||
| to 30 days. Until the file is actually deleted (e.g. the trash is emptied), all files and database content remains intact and can be restored at any point up until that time. | to 30 days. Until the file is actually deleted (e.g. the trash is emptied), all files and database content remains intact and can be restored at any point up until that time. | ||||||
|  |  | ||||||
| Additionally you may configure a directory where deleted files are moved to when they the trash is emptied with [`PAPERLESS_EMPTY_TRASH_DIR`](configuration.md#PAPERLESS_EMPTY_TRASH_DIR). | Additionally you may configure a directory where deleted files are moved to when the trash is emptied with [`PAPERLESS_EMPTY_TRASH_DIR`](configuration.md#PAPERLESS_EMPTY_TRASH_DIR). | ||||||
| Note that the empty trash directory only stores the original file, the archive file and all database information is permanently removed once a document is fully deleted. | Note that the empty trash directory only stores the original file, the archive file and all database information is permanently removed once a document is fully deleted. | ||||||
|  |  | ||||||
| ## Best practices {#basic-searching} | ## Best practices {#basic-searching} | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ dependencies = [ | |||||||
|   "django-cors-headers~=4.9.0", |   "django-cors-headers~=4.9.0", | ||||||
|   "django-extensions~=4.1", |   "django-extensions~=4.1", | ||||||
|   "django-filter~=25.1", |   "django-filter~=25.1", | ||||||
|   "django-guardian~=3.1.2", |   "django-guardian~=3.2.0", | ||||||
|   "django-multiselectfield~=1.0.1", |   "django-multiselectfield~=1.0.1", | ||||||
|   "django-soft-delete~=1.0.18", |   "django-soft-delete~=1.0.18", | ||||||
|   "django-treenode>=0.23.2", |   "django-treenode>=0.23.2", | ||||||
| @@ -54,7 +54,6 @@ dependencies = [ | |||||||
|   "ocrmypdf~=16.11.0", |   "ocrmypdf~=16.11.0", | ||||||
|   "pathvalidate~=3.3.1", |   "pathvalidate~=3.3.1", | ||||||
|   "pdf2image~=1.17.0", |   "pdf2image~=1.17.0", | ||||||
|   "psycopg-pool", |  | ||||||
|   "python-dateutil~=2.9.0", |   "python-dateutil~=2.9.0", | ||||||
|   "python-dotenv~=1.1.0", |   "python-dotenv~=1.1.0", | ||||||
|   "python-gnupg~=0.5.4", |   "python-gnupg~=0.5.4", | ||||||
|   | |||||||
| @@ -5,14 +5,14 @@ | |||||||
|       <trans-unit id="ngb.alert.close" datatype="html"> |       <trans-unit id="ngb.alert.close" datatype="html"> | ||||||
|         <source>Close</source> |         <source>Close</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/alert/alert.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/alert/alert.ts</context> | ||||||
|           <context context-type="linenumber">50</context> |           <context context-type="linenumber">50</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.carousel.slide-number" datatype="html"> |       <trans-unit id="ngb.carousel.slide-number" datatype="html"> | ||||||
|         <source> Slide <x id="INTERPOLATION" equiv-text="ueryList<NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source> |         <source> Slide <x id="INTERPOLATION" equiv-text="ueryList<NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/carousel/carousel.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/carousel/carousel.ts</context> | ||||||
|           <context context-type="linenumber">131,135</context> |           <context context-type="linenumber">131,135</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <note priority="1" from="description">Currently selected slide number read by screen reader</note> |         <note priority="1" from="description">Currently selected slide number read by screen reader</note> | ||||||
| @@ -20,212 +20,212 @@ | |||||||
|       <trans-unit id="ngb.carousel.previous" datatype="html"> |       <trans-unit id="ngb.carousel.previous" datatype="html"> | ||||||
|         <source>Previous</source> |         <source>Previous</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/carousel/carousel.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/carousel/carousel.ts</context> | ||||||
|           <context context-type="linenumber">157,159</context> |           <context context-type="linenumber">157,159</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.carousel.next" datatype="html"> |       <trans-unit id="ngb.carousel.next" datatype="html"> | ||||||
|         <source>Next</source> |         <source>Next</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/carousel/carousel.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/carousel/carousel.ts</context> | ||||||
|           <context context-type="linenumber">198</context> |           <context context-type="linenumber">198</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.datepicker.previous-month" datatype="html"> |       <trans-unit id="ngb.datepicker.previous-month" datatype="html"> | ||||||
|         <source>Previous month</source> |         <source>Previous month</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/datepicker/datepicker-navigation.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/datepicker/datepicker-navigation.ts</context> | ||||||
|           <context context-type="linenumber">83,85</context> |           <context context-type="linenumber">83,85</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/datepicker/datepicker-navigation.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/datepicker/datepicker-navigation.ts</context> | ||||||
|           <context context-type="linenumber">112</context> |           <context context-type="linenumber">112</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.datepicker.next-month" datatype="html"> |       <trans-unit id="ngb.datepicker.next-month" datatype="html"> | ||||||
|         <source>Next month</source> |         <source>Next month</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/datepicker/datepicker-navigation.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/datepicker/datepicker-navigation.ts</context> | ||||||
|           <context context-type="linenumber">112</context> |           <context context-type="linenumber">112</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/datepicker/datepicker-navigation.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/datepicker/datepicker-navigation.ts</context> | ||||||
|           <context context-type="linenumber">112</context> |           <context context-type="linenumber">112</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.HH" datatype="html"> |       <trans-unit id="ngb.timepicker.HH" datatype="html"> | ||||||
|         <source>HH</source> |         <source>HH</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.toast.close-aria" datatype="html"> |       <trans-unit id="ngb.toast.close-aria" datatype="html"> | ||||||
|         <source>Close</source> |         <source>Close</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.datepicker.select-month" datatype="html"> |       <trans-unit id="ngb.datepicker.select-month" datatype="html"> | ||||||
|         <source>Select month</source> |         <source>Select month</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.pagination.first" datatype="html"> |       <trans-unit id="ngb.pagination.first" datatype="html"> | ||||||
|         <source>««</source> |         <source>««</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.hours" datatype="html"> |       <trans-unit id="ngb.timepicker.hours" datatype="html"> | ||||||
|         <source>Hours</source> |         <source>Hours</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.pagination.previous" datatype="html"> |       <trans-unit id="ngb.pagination.previous" datatype="html"> | ||||||
|         <source>«</source> |         <source>«</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.MM" datatype="html"> |       <trans-unit id="ngb.timepicker.MM" datatype="html"> | ||||||
|         <source>MM</source> |         <source>MM</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.pagination.next" datatype="html"> |       <trans-unit id="ngb.pagination.next" datatype="html"> | ||||||
|         <source>»</source> |         <source>»</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.datepicker.select-year" datatype="html"> |       <trans-unit id="ngb.datepicker.select-year" datatype="html"> | ||||||
|         <source>Select year</source> |         <source>Select year</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.minutes" datatype="html"> |       <trans-unit id="ngb.timepicker.minutes" datatype="html"> | ||||||
|         <source>Minutes</source> |         <source>Minutes</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.pagination.last" datatype="html"> |       <trans-unit id="ngb.pagination.last" datatype="html"> | ||||||
|         <source>»»</source> |         <source>»»</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.pagination.first-aria" datatype="html"> |       <trans-unit id="ngb.pagination.first-aria" datatype="html"> | ||||||
|         <source>First</source> |         <source>First</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.increment-hours" datatype="html"> |       <trans-unit id="ngb.timepicker.increment-hours" datatype="html"> | ||||||
|         <source>Increment hours</source> |         <source>Increment hours</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.pagination.previous-aria" datatype="html"> |       <trans-unit id="ngb.pagination.previous-aria" datatype="html"> | ||||||
|         <source>Previous</source> |         <source>Previous</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.decrement-hours" datatype="html"> |       <trans-unit id="ngb.timepicker.decrement-hours" datatype="html"> | ||||||
|         <source>Decrement hours</source> |         <source>Decrement hours</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.pagination.next-aria" datatype="html"> |       <trans-unit id="ngb.pagination.next-aria" datatype="html"> | ||||||
|         <source>Next</source> |         <source>Next</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.increment-minutes" datatype="html"> |       <trans-unit id="ngb.timepicker.increment-minutes" datatype="html"> | ||||||
|         <source>Increment minutes</source> |         <source>Increment minutes</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.pagination.last-aria" datatype="html"> |       <trans-unit id="ngb.pagination.last-aria" datatype="html"> | ||||||
|         <source>Last</source> |         <source>Last</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.decrement-minutes" datatype="html"> |       <trans-unit id="ngb.timepicker.decrement-minutes" datatype="html"> | ||||||
|         <source>Decrement minutes</source> |         <source>Decrement minutes</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.SS" datatype="html"> |       <trans-unit id="ngb.timepicker.SS" datatype="html"> | ||||||
|         <source>SS</source> |         <source>SS</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.seconds" datatype="html"> |       <trans-unit id="ngb.timepicker.seconds" datatype="html"> | ||||||
|         <source>Seconds</source> |         <source>Seconds</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.increment-seconds" datatype="html"> |       <trans-unit id="ngb.timepicker.increment-seconds" datatype="html"> | ||||||
|         <source>Increment seconds</source> |         <source>Increment seconds</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.decrement-seconds" datatype="html"> |       <trans-unit id="ngb.timepicker.decrement-seconds" datatype="html"> | ||||||
|         <source>Decrement seconds</source> |         <source>Decrement seconds</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="ngb.timepicker.PM" datatype="html"> |       <trans-unit id="ngb.timepicker.PM" datatype="html"> | ||||||
|         <source><x id="INTERPOLATION"/></source> |         <source><x id="INTERPOLATION"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/ngb-config.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/ngb-config.ts</context> | ||||||
|           <context context-type="linenumber">13</context> |           <context context-type="linenumber">13</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
| @@ -233,7 +233,7 @@ | |||||||
|         <source><x id="INTERPOLATION" equiv-text="barConfig); |         <source><x id="INTERPOLATION" equiv-text="barConfig); | ||||||
| 	pu"/></source> | 	pu"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.2.4_@angular+core@20.2.4_@angular+_db9461b4835bfc9061e01150e14e6256/node_modules/src/progressbar/progressbar.ts</context> |           <context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.3.2_@angular+core@20.3.2_@angular+_4a8591e6ee586bf00b666f6438778cc7/node_modules/src/progressbar/progressbar.ts</context> | ||||||
|           <context context-type="linenumber">41,42</context> |           <context context-type="linenumber">41,42</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
| @@ -1636,7 +1636,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">44</context> |           <context context-type="linenumber">45</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> | ||||||
| @@ -1862,7 +1862,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">153</context> |           <context context-type="linenumber">155</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2134950584701094962" datatype="html"> |       <trans-unit id="2134950584701094962" datatype="html"> | ||||||
| @@ -1918,63 +1918,77 @@ | |||||||
|         <source>Result</source> |         <source>Result</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">45</context> |           <context context-type="linenumber">46</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5404910960991552159" datatype="html"> |       <trans-unit id="5404910960991552159" datatype="html"> | ||||||
|         <source>Dismiss selected</source> |         <source>Dismiss selected</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">108</context> |           <context context-type="linenumber">110</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8829078752502782653" datatype="html"> |       <trans-unit id="8829078752502782653" datatype="html"> | ||||||
|         <source>Dismiss all</source> |         <source>Dismiss all</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">109</context> |           <context context-type="linenumber">111</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1323591410517879795" datatype="html"> |       <trans-unit id="1323591410517879795" datatype="html"> | ||||||
|         <source>Confirm Dismiss All</source> |         <source>Confirm Dismiss All</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">150</context> |           <context context-type="linenumber">152</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4157200209636243740" datatype="html"> |       <trans-unit id="4157200209636243740" datatype="html"> | ||||||
|         <source>Dismiss all <x id="PH" equiv-text="tasks.size"/> tasks?</source> |         <source>Dismiss all <x id="PH" equiv-text="tasks.size"/> tasks?</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">151</context> |           <context context-type="linenumber">153</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="3597309129998924778" datatype="html"> | ||||||
|  |         <source>Error dismissing tasks</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|  |           <context context-type="linenumber">161</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="2132179171926568807" datatype="html"> | ||||||
|  |         <source>Error dismissing task</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|  |           <context context-type="linenumber">170</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="9011556615675272238" datatype="html"> |       <trans-unit id="9011556615675272238" datatype="html"> | ||||||
|         <source>queued</source> |         <source>queued</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">236</context> |           <context context-type="linenumber">246</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6415892379431855826" datatype="html"> |       <trans-unit id="6415892379431855826" datatype="html"> | ||||||
|         <source>started</source> |         <source>started</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">238</context> |           <context context-type="linenumber">248</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7510279840486540181" datatype="html"> |       <trans-unit id="7510279840486540181" datatype="html"> | ||||||
|         <source>completed</source> |         <source>completed</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">240</context> |           <context context-type="linenumber">250</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4083337005045748464" datatype="html"> |       <trans-unit id="4083337005045748464" datatype="html"> | ||||||
|         <source>failed</source> |         <source>failed</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">242</context> |           <context context-type="linenumber">252</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3418677553313974490" datatype="html"> |       <trans-unit id="3418677553313974490" datatype="html"> | ||||||
| @@ -2560,11 +2574,11 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1025</context> |           <context context-type="linenumber">1028</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1390</context> |           <context context-type="linenumber">1393</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -3176,7 +3190,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">978</context> |           <context context-type="linenumber">981</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -6654,7 +6668,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1389</context> |           <context context-type="linenumber">1392</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6490688569532630280" datatype="html"> |       <trans-unit id="6490688569532630280" datatype="html"> | ||||||
| @@ -7018,53 +7032,53 @@ | |||||||
|         <source>Error retrieving metadata</source> |         <source>Error retrieving metadata</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">666</context> |           <context context-type="linenumber">669</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3456881259945295697" datatype="html"> |       <trans-unit id="3456881259945295697" datatype="html"> | ||||||
|         <source>Error retrieving suggestions.</source> |         <source>Error retrieving suggestions.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">695</context> |           <context context-type="linenumber">698</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2194092841814123758" datatype="html"> |       <trans-unit id="2194092841814123758" datatype="html"> | ||||||
|         <source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source> |         <source>Document "<x id="PH" equiv-text="newValues.title"/>" saved successfully.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">867</context> |           <context context-type="linenumber">870</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">891</context> |           <context context-type="linenumber">894</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6626387786259219838" datatype="html"> |       <trans-unit id="6626387786259219838" datatype="html"> | ||||||
|         <source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source> |         <source>Error saving document "<x id="PH" equiv-text="this.document.title"/>"</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">897</context> |           <context context-type="linenumber">900</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="448882439049417053" datatype="html"> |       <trans-unit id="448882439049417053" datatype="html"> | ||||||
|         <source>Error saving document</source> |         <source>Error saving document</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">947</context> |           <context context-type="linenumber">950</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8410796510716511826" datatype="html"> |       <trans-unit id="8410796510716511826" datatype="html"> | ||||||
|         <source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source> |         <source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">979</context> |           <context context-type="linenumber">982</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="282586936710748252" datatype="html"> |       <trans-unit id="282586936710748252" datatype="html"> | ||||||
|         <source>Documents can be restored prior to permanent deletion.</source> |         <source>Documents can be restored prior to permanent deletion.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">980</context> |           <context context-type="linenumber">983</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -7075,7 +7089,7 @@ | |||||||
|         <source>Move to trash</source> |         <source>Move to trash</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">982</context> |           <context context-type="linenumber">985</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -7086,14 +7100,14 @@ | |||||||
|         <source>Error deleting document</source> |         <source>Error deleting document</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1001</context> |           <context context-type="linenumber">1004</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="619486176823357521" datatype="html"> |       <trans-unit id="619486176823357521" datatype="html"> | ||||||
|         <source>Reprocess confirm</source> |         <source>Reprocess confirm</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1021</context> |           <context context-type="linenumber">1024</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> | ||||||
| @@ -7104,81 +7118,81 @@ | |||||||
|         <source>This operation will permanently recreate the archive file for this document.</source> |         <source>This operation will permanently recreate the archive file for this document.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1022</context> |           <context context-type="linenumber">1025</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="302054111564709516" datatype="html"> |       <trans-unit id="302054111564709516" datatype="html"> | ||||||
|         <source>The archive file will be re-generated with the current settings.</source> |         <source>The archive file will be re-generated with the current settings.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1023</context> |           <context context-type="linenumber">1026</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8251197608401006898" datatype="html"> |       <trans-unit id="8251197608401006898" datatype="html"> | ||||||
|         <source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source> |         <source>Reprocess operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1033</context> |           <context context-type="linenumber">1036</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4409560272830824468" datatype="html"> |       <trans-unit id="4409560272830824468" datatype="html"> | ||||||
|         <source>Error executing operation</source> |         <source>Error executing operation</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1044</context> |           <context context-type="linenumber">1047</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6030453331794586802" datatype="html"> |       <trans-unit id="6030453331794586802" datatype="html"> | ||||||
|         <source>Error downloading document</source> |         <source>Error downloading document</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1093</context> |           <context context-type="linenumber">1096</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4458954481601077369" datatype="html"> |       <trans-unit id="4458954481601077369" datatype="html"> | ||||||
|         <source>Page Fit</source> |         <source>Page Fit</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1170</context> |           <context context-type="linenumber">1173</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4663705961777238777" datatype="html"> |       <trans-unit id="4663705961777238777" datatype="html"> | ||||||
|         <source>PDF edit operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source> |         <source>PDF edit operation for "<x id="PH" equiv-text="this.document.title"/>" will begin in the background.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1408</context> |           <context context-type="linenumber">1411</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="9043972994040261999" datatype="html"> |       <trans-unit id="9043972994040261999" datatype="html"> | ||||||
|         <source>Error executing PDF edit operation</source> |         <source>Error executing PDF edit operation</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1420</context> |           <context context-type="linenumber">1423</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3740891324955700797" datatype="html"> |       <trans-unit id="3740891324955700797" datatype="html"> | ||||||
|         <source>Print failed.</source> |         <source>Print failed.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1452</context> |           <context context-type="linenumber">1455</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6457245677384603573" datatype="html"> |       <trans-unit id="6457245677384603573" datatype="html"> | ||||||
|         <source>Error loading document for printing.</source> |         <source>Error loading document for printing.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1460</context> |           <context context-type="linenumber">1463</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6085793215710522488" datatype="html"> |       <trans-unit id="6085793215710522488" datatype="html"> | ||||||
|         <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source> |         <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1525</context> |           <context context-type="linenumber">1528</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> | ||||||
|           <context context-type="linenumber">1529</context> |           <context context-type="linenumber">1532</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4958946940233632319" datatype="html"> |       <trans-unit id="4958946940233632319" datatype="html"> | ||||||
|   | |||||||
| @@ -11,17 +11,17 @@ | |||||||
|   }, |   }, | ||||||
|   "private": true, |   "private": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@angular/cdk": "^20.2.2", |     "@angular/cdk": "^20.2.6", | ||||||
|     "@angular/common": "~20.2.4", |     "@angular/common": "~20.3.2", | ||||||
|     "@angular/compiler": "~20.2.4", |     "@angular/compiler": "~20.3.2", | ||||||
|     "@angular/core": "~20.2.4", |     "@angular/core": "~20.3.2", | ||||||
|     "@angular/forms": "~20.2.4", |     "@angular/forms": "~20.3.2", | ||||||
|     "@angular/localize": "~20.2.4", |     "@angular/localize": "~20.3.2", | ||||||
|     "@angular/platform-browser": "~20.2.4", |     "@angular/platform-browser": "~20.3.2", | ||||||
|     "@angular/platform-browser-dynamic": "~20.2.4", |     "@angular/platform-browser-dynamic": "~20.3.2", | ||||||
|     "@angular/router": "~20.2.4", |     "@angular/router": "~20.3.2", | ||||||
|     "@ng-bootstrap/ng-bootstrap": "^19.0.1", |     "@ng-bootstrap/ng-bootstrap": "^19.0.1", | ||||||
|     "@ng-select/ng-select": "^20.1.3", |     "@ng-select/ng-select": "^20.2.2", | ||||||
|     "@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", | ||||||
| @@ -29,47 +29,48 @@ | |||||||
|     "mime-names": "^1.0.0", |     "mime-names": "^1.0.0", | ||||||
|     "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.0.0", |     "ngx-color": "^10.1.0", | ||||||
|     "ngx-cookie-service": "^20.1.0", |     "ngx-cookie-service": "^20.1.0", | ||||||
|     "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", | ||||||
|     "tslib": "^2.8.1", |     "tslib": "^2.8.1", | ||||||
|     "utif": "^3.1.0", |     "utif": "^3.1.0", | ||||||
|     "uuid": "^11.1.0", |     "uuid": "^13.0.0", | ||||||
|     "zone.js": "^0.15.1" |     "zone.js": "^0.15.1" | ||||||
|   }, |   }, | ||||||
|   "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.2.2", |     "@angular-devkit/core": "^20.3.3", | ||||||
|     "@angular-devkit/schematics": "^20.2.2", |     "@angular-devkit/schematics": "^20.3.3", | ||||||
|     "@angular-eslint/builder": "20.2.0", |     "@angular-eslint/builder": "20.3.0", | ||||||
|     "@angular-eslint/eslint-plugin": "20.2.0", |     "@angular-eslint/eslint-plugin": "20.3.0", | ||||||
|     "@angular-eslint/eslint-plugin-template": "20.2.0", |     "@angular-eslint/eslint-plugin-template": "20.3.0", | ||||||
|     "@angular-eslint/schematics": "20.2.0", |     "@angular-eslint/schematics": "20.3.0", | ||||||
|     "@angular-eslint/template-parser": "20.2.0", |     "@angular-eslint/template-parser": "20.3.0", | ||||||
|     "@angular/build": "^20.2.2", |     "@angular/build": "^20.3.3", | ||||||
|     "@angular/cli": "~20.2.2", |     "@angular/cli": "~20.3.3", | ||||||
|     "@angular/compiler-cli": "~20.2.4", |     "@angular/compiler-cli": "~20.3.2", | ||||||
|     "@codecov/webpack-plugin": "^1.9.1", |     "@codecov/webpack-plugin": "^1.9.1", | ||||||
|     "@playwright/test": "^1.55.0", |     "@playwright/test": "^1.55.1", | ||||||
|     "@types/jest": "^30.0.0", |     "@types/jest": "^30.0.0", | ||||||
|     "@types/node": "^24.3.0", |     "@types/node": "^24.6.1", | ||||||
|     "@typescript-eslint/eslint-plugin": "^8.41.0", |     "@typescript-eslint/eslint-plugin": "^8.45.0", | ||||||
|     "@typescript-eslint/parser": "^8.41.0", |     "@typescript-eslint/parser": "^8.45.0", | ||||||
|     "@typescript-eslint/utils": "^8.41.0", |     "@typescript-eslint/utils": "^8.45.0", | ||||||
|     "eslint": "^9.34.0", |     "eslint": "^9.36.0", | ||||||
|     "jest": "30.1.3", |     "jest": "30.2.0", | ||||||
|     "jest-environment-jsdom": "^30.1.2", |     "jest-environment-jsdom": "^30.2.0", | ||||||
|     "jest-junit": "^16.0.0", |     "jest-junit": "^16.0.0", | ||||||
|     "jest-preset-angular": "^15.0.0", |     "jest-preset-angular": "^15.0.2", | ||||||
|     "jest-websocket-mock": "^2.5.0", |     "jest-websocket-mock": "^2.5.0", | ||||||
|     "prettier-plugin-organize-imports": "^4.2.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.101.3" |     "webpack": "^5.102.0" | ||||||
|   }, |   }, | ||||||
|  |   "packageManager": "pnpm@10.17.1", | ||||||
|   "pnpm": { |   "pnpm": { | ||||||
|     "onlyBuiltDependencies": [ |     "onlyBuiltDependencies": [ | ||||||
|       "@parcel/watcher", |       "@parcel/watcher", | ||||||
|   | |||||||
							
								
								
									
										3498
									
								
								src-ui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3498
									
								
								src-ui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -145,4 +145,14 @@ HTMLCanvasElement.prototype.getContext = < | |||||||
|   typeof HTMLCanvasElement.prototype.getContext |   typeof HTMLCanvasElement.prototype.getContext | ||||||
| >jest.fn() | >jest.fn() | ||||||
|  |  | ||||||
|  | jest.mock('uuid', () => ({ | ||||||
|  |   v4: jest.fn(() => | ||||||
|  |     'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char: string) => { | ||||||
|  |       const random = Math.floor(Math.random() * 16) | ||||||
|  |       const value = char === 'x' ? random : (random & 0x3) | 0x8 | ||||||
|  |       return value.toString(16) | ||||||
|  |     }) | ||||||
|  |   ), | ||||||
|  | })) | ||||||
|  |  | ||||||
| jest.mock('pdfjs-dist') | jest.mock('pdfjs-dist') | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import { | |||||||
|   NgbNavItem, |   NgbNavItem, | ||||||
| } from '@ng-bootstrap/ng-bootstrap' | } from '@ng-bootstrap/ng-bootstrap' | ||||||
| import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | ||||||
|  | import { throwError } from 'rxjs' | ||||||
| import { routes } from 'src/app/app-routing.module' | import { routes } from 'src/app/app-routing.module' | ||||||
| import { | import { | ||||||
|   PaperlessTask, |   PaperlessTask, | ||||||
| @@ -28,6 +29,7 @@ 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 { PermissionsService } from 'src/app/services/permissions.service' | import { PermissionsService } from 'src/app/services/permissions.service' | ||||||
| import { TasksService } from 'src/app/services/tasks.service' | import { TasksService } from 'src/app/services/tasks.service' | ||||||
|  | import { ToastService } from 'src/app/services/toast.service' | ||||||
| import { environment } from 'src/environments/environment' | import { environment } from 'src/environments/environment' | ||||||
| import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||||
| import { PageHeaderComponent } from '../../common/page-header/page-header.component' | import { PageHeaderComponent } from '../../common/page-header/page-header.component' | ||||||
| @@ -123,6 +125,7 @@ describe('TasksComponent', () => { | |||||||
|   let router: Router |   let router: Router | ||||||
|   let httpTestingController: HttpTestingController |   let httpTestingController: HttpTestingController | ||||||
|   let reloadSpy |   let reloadSpy | ||||||
|  |   let toastService: ToastService | ||||||
|  |  | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     TestBed.configureTestingModule({ |     TestBed.configureTestingModule({ | ||||||
| @@ -157,6 +160,7 @@ describe('TasksComponent', () => { | |||||||
|     httpTestingController = TestBed.inject(HttpTestingController) |     httpTestingController = TestBed.inject(HttpTestingController) | ||||||
|     modalService = TestBed.inject(NgbModal) |     modalService = TestBed.inject(NgbModal) | ||||||
|     router = TestBed.inject(Router) |     router = TestBed.inject(Router) | ||||||
|  |     toastService = TestBed.inject(ToastService) | ||||||
|     fixture = TestBed.createComponent(TasksComponent) |     fixture = TestBed.createComponent(TasksComponent) | ||||||
|     component = fixture.componentInstance |     component = fixture.componentInstance | ||||||
|     jest.useFakeTimers() |     jest.useFakeTimers() | ||||||
| @@ -249,6 +253,42 @@ describe('TasksComponent', () => { | |||||||
|     expect(dismissSpy).toHaveBeenCalledWith(selected) |     expect(dismissSpy).toHaveBeenCalledWith(selected) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   it('should show an error and re-enable modal buttons when dismissing multiple tasks fails', () => { | ||||||
|  |     component.selectedTasks = new Set([tasks[0].id, tasks[1].id]) | ||||||
|  |     const error = new Error('dismiss failed') | ||||||
|  |     const toastSpy = jest.spyOn(toastService, 'showError') | ||||||
|  |     const dismissSpy = jest | ||||||
|  |       .spyOn(tasksService, 'dismissTasks') | ||||||
|  |       .mockReturnValue(throwError(() => error)) | ||||||
|  |  | ||||||
|  |     let modal: NgbModalRef | ||||||
|  |     modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) | ||||||
|  |  | ||||||
|  |     component.dismissTasks() | ||||||
|  |     expect(modal).not.toBeUndefined() | ||||||
|  |  | ||||||
|  |     modal.componentInstance.confirmClicked.emit() | ||||||
|  |  | ||||||
|  |     expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id, tasks[1].id])) | ||||||
|  |     expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error) | ||||||
|  |     expect(modal.componentInstance.buttonsEnabled).toBe(true) | ||||||
|  |     expect(component.selectedTasks.size).toBe(0) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   it('should show an error when dismissing a single task fails', () => { | ||||||
|  |     const error = new Error('dismiss failed') | ||||||
|  |     const toastSpy = jest.spyOn(toastService, 'showError') | ||||||
|  |     const dismissSpy = jest | ||||||
|  |       .spyOn(tasksService, 'dismissTasks') | ||||||
|  |       .mockReturnValue(throwError(() => error)) | ||||||
|  |  | ||||||
|  |     component.dismissTask(tasks[0]) | ||||||
|  |  | ||||||
|  |     expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id])) | ||||||
|  |     expect(toastSpy).toHaveBeenCalledWith('Error dismissing task', error) | ||||||
|  |     expect(component.selectedTasks.size).toBe(0) | ||||||
|  |   }) | ||||||
|  |  | ||||||
|   it('should support dismiss all tasks', () => { |   it('should support dismiss all tasks', () => { | ||||||
|     let modal: NgbModalRef |     let modal: NgbModalRef | ||||||
|     modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) |     modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1])) | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import { PaperlessTask } from 'src/app/data/paperless-task' | |||||||
| import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' | import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' | ||||||
| import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' | ||||||
| import { TasksService } from 'src/app/services/tasks.service' | import { TasksService } from 'src/app/services/tasks.service' | ||||||
|  | import { ToastService } from 'src/app/services/toast.service' | ||||||
| import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||||
| 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' | ||||||
| @@ -72,6 +73,7 @@ export class TasksComponent | |||||||
|   tasksService = inject(TasksService) |   tasksService = inject(TasksService) | ||||||
|   private modalService = inject(NgbModal) |   private modalService = inject(NgbModal) | ||||||
|   private readonly router = inject(Router) |   private readonly router = inject(Router) | ||||||
|  |   private readonly toastService = inject(ToastService) | ||||||
|  |  | ||||||
|   public activeTab: TaskTab |   public activeTab: TaskTab | ||||||
|   public selectedTasks: Set<number> = new Set() |   public selectedTasks: Set<number> = new Set() | ||||||
| @@ -154,11 +156,19 @@ export class TasksComponent | |||||||
|       modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { |       modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { | ||||||
|         modal.componentInstance.buttonsEnabled = false |         modal.componentInstance.buttonsEnabled = false | ||||||
|         modal.close() |         modal.close() | ||||||
|         this.tasksService.dismissTasks(tasks) |         this.tasksService.dismissTasks(tasks).subscribe({ | ||||||
|  |           error: (e) => { | ||||||
|  |             this.toastService.showError($localize`Error dismissing tasks`, e) | ||||||
|  |             modal.componentInstance.buttonsEnabled = true | ||||||
|  |           }, | ||||||
|  |         }) | ||||||
|         this.clearSelection() |         this.clearSelection() | ||||||
|       }) |       }) | ||||||
|     } else { |     } else { | ||||||
|       this.tasksService.dismissTasks(tasks) |       this.tasksService.dismissTasks(tasks).subscribe({ | ||||||
|  |         error: (e) => | ||||||
|  |           this.toastService.showError($localize`Error dismissing task`, e), | ||||||
|  |       }) | ||||||
|       this.clearSelection() |       this.clearSelection() | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -41,9 +41,3 @@ | |||||||
|     min-width: 140px; |     min-width: 140px; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .btn-group-xs { |  | ||||||
|   > .btn { |  | ||||||
|     border-radius: 0.15rem; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,19 +1,18 @@ | |||||||
| <div class="mb-3"> | <div class="mb-3"> | ||||||
|   @if (title) { |   @if (title) { | ||||||
|     <label [for]="inputId">{{title}}</label> |     <label class="form-label" [for]="inputId">{{title}}</label> | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   <div class="input-group" [class.is-invalid]="error"> |   <div class="input-group" [class.is-invalid]="error"> | ||||||
|     <span class="input-group-text" [style.background-color]="value">   </span> |     <button type="button" class="input-group-text" [style.background-color]="value" (click)="colorPicker.toggle()">   </button> | ||||||
|  |  | ||||||
|     <ng-template #popContent> |     <ng-template #popContent> | ||||||
|       <div style="min-width: 200px;" class="pb-3"> |       <div style="min-width: 200px;" class="pb-3"> | ||||||
|         <color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider> |         <color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|     </ng-template> |     </ng-template> | ||||||
|  |  | ||||||
|     <input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow"> |     <input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" #colorPicker="ngbPopover" placement="bottom" popoverClass="shadow"> | ||||||
|  |  | ||||||
|     <button class="btn btn-outline-secondary" type="button" (click)="randomize()"> |     <button class="btn btn-outline-secondary" type="button" (click)="randomize()"> | ||||||
|       <i-bs name="dice5"></i-bs> |       <i-bs name="dice5"></i-bs> | ||||||
|   | |||||||
| @@ -42,8 +42,8 @@ describe('ColorComponent', () => { | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   it('should set swatch color', () => { |   it('should set swatch color', () => { | ||||||
|     const swatch: HTMLSpanElement = fixture.nativeElement.querySelector( |     const swatch: HTMLButtonElement = fixture.nativeElement.querySelector( | ||||||
|       'span.input-group-text' |       'button.input-group-text' | ||||||
|     ) |     ) | ||||||
|     expect(swatch.style.backgroundColor).toEqual('') |     expect(swatch.style.backgroundColor).toEqual('') | ||||||
|     component.value = '#ff0000' |     component.value = '#ff0000' | ||||||
|   | |||||||
| @@ -1212,7 +1212,7 @@ describe('DocumentDetailComponent', () => { | |||||||
|   it('should support keyboard shortcuts', () => { |   it('should support keyboard shortcuts', () => { | ||||||
|     initNormally() |     initNormally() | ||||||
|  |  | ||||||
|     jest.spyOn(component, 'hasNext').mockReturnValue(true) |     const hasNextSpy = jest.spyOn(component, 'hasNext').mockReturnValue(true) | ||||||
|     const nextSpy = jest.spyOn(component, 'nextDoc') |     const nextSpy = jest.spyOn(component, 'nextDoc') | ||||||
|     document.dispatchEvent( |     document.dispatchEvent( | ||||||
|       new KeyboardEvent('keydown', { key: 'arrowright', ctrlKey: true }) |       new KeyboardEvent('keydown', { key: 'arrowright', ctrlKey: true }) | ||||||
| @@ -1226,21 +1226,32 @@ describe('DocumentDetailComponent', () => { | |||||||
|     ) |     ) | ||||||
|     expect(prevSpy).toHaveBeenCalled() |     expect(prevSpy).toHaveBeenCalled() | ||||||
|  |  | ||||||
|     jest.spyOn(openDocumentsService, 'isDirty').mockReturnValue(true) |     const isDirtySpy = jest | ||||||
|  |       .spyOn(openDocumentsService, 'isDirty') | ||||||
|  |       .mockReturnValue(true) | ||||||
|     const saveSpy = jest.spyOn(component, 'save') |     const saveSpy = jest.spyOn(component, 'save') | ||||||
|     document.dispatchEvent( |     document.dispatchEvent( | ||||||
|       new KeyboardEvent('keydown', { key: 's', ctrlKey: true }) |       new KeyboardEvent('keydown', { key: 's', ctrlKey: true }) | ||||||
|     ) |     ) | ||||||
|     expect(saveSpy).toHaveBeenCalled() |     expect(saveSpy).toHaveBeenCalled() | ||||||
|  |  | ||||||
|     jest.spyOn(openDocumentsService, 'isDirty').mockReturnValue(true) |     hasNextSpy.mockReturnValue(true) | ||||||
|     jest.spyOn(component, 'hasNext').mockReturnValue(true) |  | ||||||
|     const saveNextSpy = jest.spyOn(component, 'saveEditNext') |     const saveNextSpy = jest.spyOn(component, 'saveEditNext') | ||||||
|     document.dispatchEvent( |     document.dispatchEvent( | ||||||
|       new KeyboardEvent('keydown', { key: 's', ctrlKey: true, shiftKey: true }) |       new KeyboardEvent('keydown', { key: 's', ctrlKey: true, shiftKey: true }) | ||||||
|     ) |     ) | ||||||
|     expect(saveNextSpy).toHaveBeenCalled() |     expect(saveNextSpy).toHaveBeenCalled() | ||||||
|  |  | ||||||
|  |     saveSpy.mockClear() | ||||||
|  |     saveNextSpy.mockClear() | ||||||
|  |     isDirtySpy.mockReturnValue(true) | ||||||
|  |     hasNextSpy.mockReturnValue(false) | ||||||
|  |     document.dispatchEvent( | ||||||
|  |       new KeyboardEvent('keydown', { key: 's', ctrlKey: true, shiftKey: true }) | ||||||
|  |     ) | ||||||
|  |     expect(saveNextSpy).not.toHaveBeenCalled() | ||||||
|  |     expect(saveSpy).toHaveBeenCalledWith(true) | ||||||
|  |  | ||||||
|     const closeSpy = jest.spyOn(component, 'close') |     const closeSpy = jest.spyOn(component, 'close') | ||||||
|     document.dispatchEvent(new KeyboardEvent('keydown', { key: 'escape' })) |     document.dispatchEvent(new KeyboardEvent('keydown', { key: 'escape' })) | ||||||
|     expect(closeSpy).toHaveBeenCalled() |     expect(closeSpy).toHaveBeenCalled() | ||||||
|   | |||||||
| @@ -615,7 +615,10 @@ export class DocumentDetailComponent | |||||||
|       }) |       }) | ||||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|       .subscribe(() => { |       .subscribe(() => { | ||||||
|         if (this.openDocumentService.isDirty(this.document)) this.saveEditNext() |         if (this.openDocumentService.isDirty(this.document)) { | ||||||
|  |           if (this.hasNext()) this.saveEditNext() | ||||||
|  |           else this.save(true) | ||||||
|  |         } | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ describe('TasksService', () => { | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => { |   it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => { | ||||||
|     tasksService.dismissTasks(new Set([1, 2, 3])) |     tasksService.dismissTasks(new Set([1, 2, 3])).subscribe() | ||||||
|     const req = httpTestingController.expectOne( |     const req = httpTestingController.expectOne( | ||||||
|       `${environment.apiBaseUrl}tasks/acknowledge/` |       `${environment.apiBaseUrl}tasks/acknowledge/` | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { HttpClient } from '@angular/common/http' | import { HttpClient } from '@angular/common/http' | ||||||
| import { Injectable, inject } from '@angular/core' | import { Injectable, inject } from '@angular/core' | ||||||
| import { Observable, Subject } from 'rxjs' | import { Observable, Subject } from 'rxjs' | ||||||
| import { first, takeUntil } from 'rxjs/operators' | import { first, takeUntil, tap } from 'rxjs/operators' | ||||||
| import { | import { | ||||||
|   PaperlessTask, |   PaperlessTask, | ||||||
|   PaperlessTaskName, |   PaperlessTaskName, | ||||||
| @@ -68,14 +68,17 @@ export class TasksService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public dismissTasks(task_ids: Set<number>) { |   public dismissTasks(task_ids: Set<number>) { | ||||||
|     this.http |     return this.http | ||||||
|       .post(`${this.baseUrl}tasks/acknowledge/`, { |       .post(`${this.baseUrl}tasks/acknowledge/`, { | ||||||
|         tasks: [...task_ids], |         tasks: [...task_ids], | ||||||
|       }) |       }) | ||||||
|       .pipe(first()) |       .pipe( | ||||||
|       .subscribe((r) => { |         first(), | ||||||
|         this.reload() |         takeUntil(this.unsubscribeNotifer), | ||||||
|       }) |         tap(() => { | ||||||
|  |           this.reload() | ||||||
|  |         }) | ||||||
|  |       ) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public cancelPending(): void { |   public cancelPending(): void { | ||||||
|   | |||||||
| @@ -161,3 +161,21 @@ class PaperlessNotePermissions(BasePermission): | |||||||
|         perms = self.perms_map[request.method] |         perms = self.perms_map[request.method] | ||||||
|  |  | ||||||
|         return request.user.has_perms(perms) |         return request.user.has_perms(perms) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AcknowledgeTasksPermissions(BasePermission): | ||||||
|  |     """ | ||||||
|  |     Permissions class that checks for model permissions for acknowledging tasks. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     perms_map = { | ||||||
|  |         "POST": ["documents.change_paperlesstask"], | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def has_permission(self, request, view): | ||||||
|  |         if not request.user or not request.user.is_authenticated:  # pragma: no cover | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |         perms = self.perms_map.get(request.method, []) | ||||||
|  |  | ||||||
|  |         return request.user.has_perms(perms) | ||||||
|   | |||||||
| @@ -76,7 +76,9 @@ def check_sanity(*, progress=False, scheduled=True) -> SanityCheckMessages: | |||||||
|     messages = SanityCheckMessages() |     messages = SanityCheckMessages() | ||||||
|  |  | ||||||
|     present_files = { |     present_files = { | ||||||
|         x.resolve() for x in Path(settings.MEDIA_ROOT).glob("**/*") if not x.is_dir() |         x.resolve() | ||||||
|  |         for x in Path(settings.MEDIA_ROOT).glob("**/*") | ||||||
|  |         if not x.is_dir() and x.name not in settings.IGNORABLE_FILES | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     lockfile = Path(settings.MEDIA_LOCK).resolve() |     lockfile = Path(settings.MEDIA_LOCK).resolve() | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import re | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from decimal import Decimal | from decimal import Decimal | ||||||
| from typing import TYPE_CHECKING | from typing import TYPE_CHECKING | ||||||
|  | from typing import Literal | ||||||
|  |  | ||||||
| import magic | import magic | ||||||
| from celery import states | from celery import states | ||||||
| @@ -252,6 +253,35 @@ class OwnedObjectSerializer( | |||||||
|             except KeyError: |             except KeyError: | ||||||
|                 pass |                 pass | ||||||
|  |  | ||||||
|  |     def _get_perms(self, obj, codename: str, target: Literal["users", "groups"]): | ||||||
|  |         """ | ||||||
|  |         Get the given permissions from context or from django-guardian. | ||||||
|  |  | ||||||
|  |         :param codename: The permission codename, e.g. 'view' or 'change' | ||||||
|  |         :param target: 'users' or 'groups' | ||||||
|  |         """ | ||||||
|  |         key = f"{target}_{codename}_perms" | ||||||
|  |         cached = self.context.get(key, {}).get(obj.pk) | ||||||
|  |         if cached is not None: | ||||||
|  |             return list(cached) | ||||||
|  |  | ||||||
|  |         # Permission not found in the context, get it from guardian | ||||||
|  |         if target == "users": | ||||||
|  |             return list( | ||||||
|  |                 get_users_with_perms( | ||||||
|  |                     obj, | ||||||
|  |                     only_with_perms_in=[f"{codename}_{obj.__class__.__name__.lower()}"], | ||||||
|  |                     with_group_users=False, | ||||||
|  |                 ).values_list("id", flat=True), | ||||||
|  |             ) | ||||||
|  |         else:  # groups | ||||||
|  |             return list( | ||||||
|  |                 get_groups_with_only_permission( | ||||||
|  |                     obj, | ||||||
|  |                     codename=f"{codename}_{obj.__class__.__name__.lower()}", | ||||||
|  |                 ).values_list("id", flat=True), | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     @extend_schema_field( |     @extend_schema_field( | ||||||
|         field={ |         field={ | ||||||
|             "type": "object", |             "type": "object", | ||||||
| @@ -286,31 +316,14 @@ class OwnedObjectSerializer( | |||||||
|         }, |         }, | ||||||
|     ) |     ) | ||||||
|     def get_permissions(self, obj) -> dict: |     def get_permissions(self, obj) -> dict: | ||||||
|         view_codename = f"view_{obj.__class__.__name__.lower()}" |  | ||||||
|         change_codename = f"change_{obj.__class__.__name__.lower()}" |  | ||||||
|  |  | ||||||
|         return { |         return { | ||||||
|             "view": { |             "view": { | ||||||
|                 "users": get_users_with_perms( |                 "users": self._get_perms(obj, "view", "users"), | ||||||
|                     obj, |                 "groups": self._get_perms(obj, "view", "groups"), | ||||||
|                     only_with_perms_in=[view_codename], |  | ||||||
|                     with_group_users=False, |  | ||||||
|                 ).values_list("id", flat=True), |  | ||||||
|                 "groups": get_groups_with_only_permission( |  | ||||||
|                     obj, |  | ||||||
|                     codename=view_codename, |  | ||||||
|                 ).values_list("id", flat=True), |  | ||||||
|             }, |             }, | ||||||
|             "change": { |             "change": { | ||||||
|                 "users": get_users_with_perms( |                 "users": self._get_perms(obj, "change", "users"), | ||||||
|                     obj, |                 "groups": self._get_perms(obj, "change", "groups"), | ||||||
|                     only_with_perms_in=[change_codename], |  | ||||||
|                     with_group_users=False, |  | ||||||
|                 ).values_list("id", flat=True), |  | ||||||
|                 "groups": get_groups_with_only_permission( |  | ||||||
|                     obj, |  | ||||||
|                     codename=change_codename, |  | ||||||
|                 ).values_list("id", flat=True), |  | ||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -135,6 +135,44 @@ class TestTasks(DirectoriesMixin, APITestCase): | |||||||
|         response = self.client.get(self.ENDPOINT + "?acknowledged=false") |         response = self.client.get(self.ENDPOINT + "?acknowledged=false") | ||||||
|         self.assertEqual(len(response.data), 0) |         self.assertEqual(len(response.data), 0) | ||||||
|  |  | ||||||
|  |     def test_acknowledge_tasks_requires_change_permission(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - A regular user initially without change permissions | ||||||
|  |             - A regular user with change permissions | ||||||
|  |         WHEN: | ||||||
|  |             - API call is made to acknowledge tasks | ||||||
|  |         THEN: | ||||||
|  |             - The first user is forbidden from acknowledging tasks | ||||||
|  |             - The second user is allowed to acknowledge tasks | ||||||
|  |         """ | ||||||
|  |         regular_user = User.objects.create_user(username="test") | ||||||
|  |         self.client.force_authenticate(user=regular_user) | ||||||
|  |  | ||||||
|  |         task = PaperlessTask.objects.create( | ||||||
|  |             task_id=str(uuid.uuid4()), | ||||||
|  |             task_file_name="task_one.pdf", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.post( | ||||||
|  |             self.ENDPOINT + "acknowledge/", | ||||||
|  |             {"tasks": [task.id]}, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) | ||||||
|  |  | ||||||
|  |         regular_user2 = User.objects.create_user(username="test2") | ||||||
|  |         regular_user2.user_permissions.add( | ||||||
|  |             Permission.objects.get(codename="change_paperlesstask"), | ||||||
|  |         ) | ||||||
|  |         regular_user2.save() | ||||||
|  |         self.client.force_authenticate(user=regular_user2) | ||||||
|  |  | ||||||
|  |         response = self.client.post( | ||||||
|  |             self.ENDPOINT + "acknowledge/", | ||||||
|  |             {"tasks": [task.id]}, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||||
|  |  | ||||||
|     def test_tasks_owner_aware(self): |     def test_tasks_owner_aware(self): | ||||||
|         """ |         """ | ||||||
|         GIVEN: |         GIVEN: | ||||||
|   | |||||||
| @@ -169,6 +169,13 @@ class TestSanityCheck(DirectoriesMixin, TestCase): | |||||||
|         messages = check_sanity() |         messages = check_sanity() | ||||||
|         self.assertFalse(messages.has_warning) |         self.assertFalse(messages.has_warning) | ||||||
|  |  | ||||||
|  |     def test_ignore_ignorable_files(self): | ||||||
|  |         self.make_test_data() | ||||||
|  |         Path(self.dirs.media_dir, ".DS_Store").touch() | ||||||
|  |         Path(self.dirs.media_dir, "desktop.ini").touch() | ||||||
|  |         messages = check_sanity() | ||||||
|  |         self.assertFalse(messages.has_warning) | ||||||
|  |  | ||||||
|     def test_archive_filename_no_checksum(self): |     def test_archive_filename_no_checksum(self): | ||||||
|         doc = self.make_test_data() |         doc = self.make_test_data() | ||||||
|         doc.archive_checksum = None |         doc.archive_checksum = None | ||||||
|   | |||||||
| @@ -1,17 +1,23 @@ | |||||||
|  | import json | ||||||
| import tempfile | import tempfile | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|  | from django.contrib.auth.models import Group | ||||||
| from django.contrib.auth.models import Permission | from django.contrib.auth.models import Permission | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
|  | from django.db import connection | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.test import override_settings | from django.test import override_settings | ||||||
|  | from django.test.utils import CaptureQueriesContext | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
|  | from guardian.shortcuts import assign_perm | ||||||
| from rest_framework import status | from rest_framework import status | ||||||
|  |  | ||||||
| from documents.models import Document | from documents.models import Document | ||||||
| from documents.models import ShareLink | from documents.models import ShareLink | ||||||
|  | from documents.models import Tag | ||||||
| from documents.tests.utils import DirectoriesMixin | from documents.tests.utils import DirectoriesMixin | ||||||
| from paperless.models import ApplicationConfiguration | from paperless.models import ApplicationConfiguration | ||||||
|  |  | ||||||
| @@ -154,3 +160,113 @@ class TestViews(DirectoriesMixin, TestCase): | |||||||
|         response.render() |         response.render() | ||||||
|         self.assertEqual(response.request["PATH_INFO"], "/accounts/login/") |         self.assertEqual(response.request["PATH_INFO"], "/accounts/login/") | ||||||
|         self.assertContains(response, b"Share link has expired") |         self.assertContains(response, b"Share link has expired") | ||||||
|  |  | ||||||
|  |     def test_list_with_full_permissions(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Tags with different permissions | ||||||
|  |         WHEN: | ||||||
|  |             - Request to get tag list with full permissions is made | ||||||
|  |         THEN: | ||||||
|  |             - Tag list is returned with the right permission information | ||||||
|  |         """ | ||||||
|  |         user2 = User.objects.create(username="user2") | ||||||
|  |         user3 = User.objects.create(username="user3") | ||||||
|  |         group1 = Group.objects.create(name="group1") | ||||||
|  |         group2 = Group.objects.create(name="group2") | ||||||
|  |         group3 = Group.objects.create(name="group3") | ||||||
|  |         t1 = Tag.objects.create(name="invoice", pk=1) | ||||||
|  |         assign_perm("view_tag", self.user, t1) | ||||||
|  |         assign_perm("view_tag", user2, t1) | ||||||
|  |         assign_perm("view_tag", user3, t1) | ||||||
|  |         assign_perm("view_tag", group1, t1) | ||||||
|  |         assign_perm("view_tag", group2, t1) | ||||||
|  |         assign_perm("view_tag", group3, t1) | ||||||
|  |         assign_perm("change_tag", self.user, t1) | ||||||
|  |         assign_perm("change_tag", user2, t1) | ||||||
|  |         assign_perm("change_tag", group1, t1) | ||||||
|  |         assign_perm("change_tag", group2, t1) | ||||||
|  |  | ||||||
|  |         Tag.objects.create(name="bank statement", pk=2) | ||||||
|  |         d1 = Document.objects.create( | ||||||
|  |             title="Invoice 1", | ||||||
|  |             content="This is the invoice of a very expensive item", | ||||||
|  |             checksum="A", | ||||||
|  |         ) | ||||||
|  |         d1.tags.add(t1) | ||||||
|  |         d2 = Document.objects.create( | ||||||
|  |             title="Invoice 2", | ||||||
|  |             content="Internet invoice, I should pay it to continue contributing", | ||||||
|  |             checksum="B", | ||||||
|  |         ) | ||||||
|  |         d2.tags.add(t1) | ||||||
|  |  | ||||||
|  |         view_permissions = Permission.objects.filter( | ||||||
|  |             codename__contains="view_tag", | ||||||
|  |         ) | ||||||
|  |         self.user.user_permissions.add(*view_permissions) | ||||||
|  |         self.user.save() | ||||||
|  |  | ||||||
|  |         self.client.force_login(self.user) | ||||||
|  |         response = self.client.get("/api/tags/?page=1&full_perms=true") | ||||||
|  |         results = json.loads(response.content)["results"] | ||||||
|  |         for tag in results: | ||||||
|  |             if tag["name"] == "invoice": | ||||||
|  |                 assert tag["permissions"] == { | ||||||
|  |                     "view": { | ||||||
|  |                         "users": [self.user.pk, user2.pk, user3.pk], | ||||||
|  |                         "groups": [group1.pk, group2.pk, group3.pk], | ||||||
|  |                     }, | ||||||
|  |                     "change": { | ||||||
|  |                         "users": [self.user.pk, user2.pk], | ||||||
|  |                         "groups": [group1.pk, group2.pk], | ||||||
|  |                     }, | ||||||
|  |                 } | ||||||
|  |             elif tag["name"] == "bank statement": | ||||||
|  |                 assert tag["permissions"] == { | ||||||
|  |                     "view": {"users": [], "groups": []}, | ||||||
|  |                     "change": {"users": [], "groups": []}, | ||||||
|  |                 } | ||||||
|  |             else: | ||||||
|  |                 assert False, f"Unexpected tag found: {tag['name']}" | ||||||
|  |  | ||||||
|  |     def test_list_no_n_plus_1_queries(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Tags with different permissions | ||||||
|  |         WHEN: | ||||||
|  |             - Request to get tag list with full permissions is made | ||||||
|  |         THEN: | ||||||
|  |             - Permissions are not queried in database tag by tag, | ||||||
|  |              i.e. there are no N+1 queries | ||||||
|  |         """ | ||||||
|  |         view_permissions = Permission.objects.filter( | ||||||
|  |             codename__contains="view_tag", | ||||||
|  |         ) | ||||||
|  |         self.user.user_permissions.add(*view_permissions) | ||||||
|  |         self.user.save() | ||||||
|  |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|  |         # Start by a small list, and count the number of SQL queries | ||||||
|  |         for i in range(2): | ||||||
|  |             Tag.objects.create(name=f"tag_{i}") | ||||||
|  |  | ||||||
|  |         with CaptureQueriesContext(connection) as ctx_small: | ||||||
|  |             response_small = self.client.get("/api/tags/?full_perms=true") | ||||||
|  |             assert response_small.status_code == 200 | ||||||
|  |         num_queries_small = len(ctx_small.captured_queries) | ||||||
|  |  | ||||||
|  |         # Complete the list, and count the number of SQL queries again | ||||||
|  |         for i in range(2, 50): | ||||||
|  |             Tag.objects.create(name=f"tag_{i}") | ||||||
|  |  | ||||||
|  |         with CaptureQueriesContext(connection) as ctx_large: | ||||||
|  |             response_large = self.client.get("/api/tags/?full_perms=true") | ||||||
|  |             assert response_large.status_code == 200 | ||||||
|  |         num_queries_large = len(ctx_large.captured_queries) | ||||||
|  |  | ||||||
|  |         # A few additional queries are allowed, but not a linear explosion | ||||||
|  |         assert num_queries_large <= num_queries_small + 5, ( | ||||||
|  |             f"Possible N+1 queries detected: {num_queries_small} queries for 2 tags, " | ||||||
|  |             f"but {num_queries_large} queries for 50 tags" | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -5,9 +5,11 @@ import platform | |||||||
| import re | import re | ||||||
| import tempfile | import tempfile | ||||||
| import zipfile | import zipfile | ||||||
|  | from collections import defaultdict | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from time import mktime | from time import mktime | ||||||
|  | from typing import Literal | ||||||
| from unicodedata import normalize | from unicodedata import normalize | ||||||
| from urllib.parse import quote | from urllib.parse import quote | ||||||
| from urllib.parse import urlparse | from urllib.parse import urlparse | ||||||
| @@ -19,6 +21,7 @@ from celery import states | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.models import Group | from django.contrib.auth.models import Group | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
|  | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.db import connections | from django.db import connections | ||||||
| from django.db.migrations.loader import MigrationLoader | from django.db.migrations.loader import MigrationLoader | ||||||
| from django.db.migrations.recorder import MigrationRecorder | from django.db.migrations.recorder import MigrationRecorder | ||||||
| @@ -56,6 +59,8 @@ from drf_spectacular.utils import OpenApiParameter | |||||||
| from drf_spectacular.utils import extend_schema | from drf_spectacular.utils import extend_schema | ||||||
| from drf_spectacular.utils import extend_schema_view | from drf_spectacular.utils import extend_schema_view | ||||||
| from drf_spectacular.utils import inline_serializer | from drf_spectacular.utils import inline_serializer | ||||||
|  | from guardian.utils import get_group_obj_perms_model | ||||||
|  | from guardian.utils import get_user_obj_perms_model | ||||||
| from langdetect import detect | from langdetect import detect | ||||||
| from packaging import version as packaging_version | from packaging import version as packaging_version | ||||||
| from redis import Redis | from redis import Redis | ||||||
| @@ -131,6 +136,7 @@ from documents.models import WorkflowAction | |||||||
| from documents.models import WorkflowTrigger | from documents.models import WorkflowTrigger | ||||||
| from documents.parsers import get_parser_class_for_mime_type | from documents.parsers import get_parser_class_for_mime_type | ||||||
| from documents.parsers import parse_date_generator | from documents.parsers import parse_date_generator | ||||||
|  | from documents.permissions import AcknowledgeTasksPermissions | ||||||
| from documents.permissions import PaperlessAdminPermissions | from documents.permissions import PaperlessAdminPermissions | ||||||
| from documents.permissions import PaperlessNotePermissions | from documents.permissions import PaperlessNotePermissions | ||||||
| from documents.permissions import PaperlessObjectPermissions | from documents.permissions import PaperlessObjectPermissions | ||||||
| @@ -254,7 +260,104 @@ class PassUserMixin(GenericAPIView): | |||||||
|         return super().get_serializer(*args, **kwargs) |         return super().get_serializer(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
| class PermissionsAwareDocumentCountMixin(PassUserMixin): | class BulkPermissionMixin: | ||||||
|  |     """ | ||||||
|  |     Prefetch Django-Guardian permissions for a list before serialization, to avoid N+1 queries. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def _get_object_perms( | ||||||
|  |         self, | ||||||
|  |         objects: list, | ||||||
|  |         perm_codenames: list[str], | ||||||
|  |         actor: Literal["users", "groups"], | ||||||
|  |     ) -> dict[int, dict[str, list[int]]]: | ||||||
|  |         """ | ||||||
|  |         Collect object-level permissions for either users or groups. | ||||||
|  |         """ | ||||||
|  |         model = self.queryset.model | ||||||
|  |         obj_perm_model = ( | ||||||
|  |             get_user_obj_perms_model(model) | ||||||
|  |             if actor == "users" | ||||||
|  |             else get_group_obj_perms_model(model) | ||||||
|  |         ) | ||||||
|  |         id_field = "user_id" if actor == "users" else "group_id" | ||||||
|  |         ctype = ContentType.objects.get_for_model(model) | ||||||
|  |         object_pks = [obj.pk for obj in objects] | ||||||
|  |  | ||||||
|  |         perms_qs = obj_perm_model.objects.filter( | ||||||
|  |             content_type=ctype, | ||||||
|  |             object_pk__in=object_pks, | ||||||
|  |             permission__codename__in=perm_codenames, | ||||||
|  |         ).values_list("object_pk", id_field, "permission__codename") | ||||||
|  |  | ||||||
|  |         perms: dict[int, dict[str, list[int]]] = defaultdict(lambda: defaultdict(list)) | ||||||
|  |         for object_pk, actor_id, codename in perms_qs: | ||||||
|  |             perms[int(object_pk)][codename].append(actor_id) | ||||||
|  |  | ||||||
|  |         # Ensure that all objects have all codenames, even if empty | ||||||
|  |         for pk in object_pks: | ||||||
|  |             for codename in perm_codenames: | ||||||
|  |                 perms[pk][codename] | ||||||
|  |  | ||||||
|  |         return perms | ||||||
|  |  | ||||||
|  |     def get_serializer_context(self): | ||||||
|  |         """ | ||||||
|  |         Get all permissions of the current list of objects at once and pass them to the serializer. | ||||||
|  |         This avoid fetching permissions object by object in database. | ||||||
|  |         """ | ||||||
|  |         context = super().get_serializer_context() | ||||||
|  |         try: | ||||||
|  |             full_perms = get_boolean( | ||||||
|  |                 str(self.request.query_params.get("full_perms", "false")), | ||||||
|  |             ) | ||||||
|  |         except ValueError: | ||||||
|  |             full_perms = False | ||||||
|  |  | ||||||
|  |         if not full_perms: | ||||||
|  |             return context | ||||||
|  |  | ||||||
|  |         # Check which objects are being paginated | ||||||
|  |         page = getattr(self, "paginator", None) | ||||||
|  |         if page and hasattr(page, "page"): | ||||||
|  |             queryset = page.page.object_list | ||||||
|  |         elif hasattr(self, "page"): | ||||||
|  |             queryset = self.page | ||||||
|  |         else: | ||||||
|  |             queryset = self.filter_queryset(self.get_queryset()) | ||||||
|  |  | ||||||
|  |         model_name = self.queryset.model.__name__.lower() | ||||||
|  |         permission_name_view = f"view_{model_name}" | ||||||
|  |         permission_name_change = f"change_{model_name}" | ||||||
|  |  | ||||||
|  |         user_perms = self._get_object_perms( | ||||||
|  |             objects=queryset, | ||||||
|  |             perm_codenames=[permission_name_view, permission_name_change], | ||||||
|  |             actor="users", | ||||||
|  |         ) | ||||||
|  |         group_perms = self._get_object_perms( | ||||||
|  |             objects=queryset, | ||||||
|  |             perm_codenames=[permission_name_view, permission_name_change], | ||||||
|  |             actor="groups", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         context["users_view_perms"] = { | ||||||
|  |             pk: user_perms[pk][permission_name_view] for pk in user_perms | ||||||
|  |         } | ||||||
|  |         context["users_change_perms"] = { | ||||||
|  |             pk: user_perms[pk][permission_name_change] for pk in user_perms | ||||||
|  |         } | ||||||
|  |         context["groups_view_perms"] = { | ||||||
|  |             pk: group_perms[pk][permission_name_view] for pk in group_perms | ||||||
|  |         } | ||||||
|  |         context["groups_change_perms"] = { | ||||||
|  |             pk: group_perms[pk][permission_name_change] for pk in group_perms | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PermissionsAwareDocumentCountMixin(BulkPermissionMixin, PassUserMixin): | ||||||
|     """ |     """ | ||||||
|     Mixin to add document count to queryset, permissions-aware if needed |     Mixin to add document count to queryset, permissions-aware if needed | ||||||
|     """ |     """ | ||||||
| @@ -2385,7 +2488,11 @@ class TasksViewSet(ReadOnlyModelViewSet): | |||||||
|             queryset = PaperlessTask.objects.filter(task_id=task_id) |             queryset = PaperlessTask.objects.filter(task_id=task_id) | ||||||
|         return queryset |         return queryset | ||||||
|  |  | ||||||
|     @action(methods=["post"], detail=False) |     @action( | ||||||
|  |         methods=["post"], | ||||||
|  |         detail=False, | ||||||
|  |         permission_classes=[IsAuthenticated, AcknowledgeTasksPermissions], | ||||||
|  |     ) | ||||||
|     def acknowledge(self, request): |     def acknowledge(self, request): | ||||||
|         serializer = AcknowledgeTasksViewSerializer(data=request.data) |         serializer = AcknowledgeTasksViewSerializer(data=request.data) | ||||||
|         serializer.is_valid(raise_exception=True) |         serializer.is_valid(raise_exception=True) | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: paperless-ngx\n" | "Project-Id-Version: paperless-ngx\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-09-22 18:20+0000\n" | "POT-Creation-Date: 2025-09-30 16:50+0000\n" | ||||||
| "PO-Revision-Date: 2022-02-17 04:17\n" | "PO-Revision-Date: 2022-02-17 04:17\n" | ||||||
| "Last-Translator: \n" | "Last-Translator: \n" | ||||||
| "Language-Team: English\n" | "Language-Team: English\n" | ||||||
| @@ -1191,44 +1191,44 @@ msgstr "" | |||||||
| msgid "workflow runs" | msgid "workflow runs" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:140 | #: documents/serialisers.py:141 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "Invalid regular expression: %(error)s" | msgid "Invalid regular expression: %(error)s" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:594 | #: documents/serialisers.py:607 | ||||||
| msgid "Invalid color." | msgid "Invalid color." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:623 | #: documents/serialisers.py:636 | ||||||
| msgid "Invalid parent tag." | msgid "Invalid parent tag." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:1780 | #: documents/serialisers.py:1793 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "File type %(type)s not supported" | msgid "File type %(type)s not supported" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:1824 | #: documents/serialisers.py:1837 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "Custom field id must be an integer: %(id)s" | msgid "Custom field id must be an integer: %(id)s" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:1831 | #: documents/serialisers.py:1844 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "Custom field with id %(id)s does not exist" | msgid "Custom field with id %(id)s does not exist" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:1848 documents/serialisers.py:1858 | #: documents/serialisers.py:1861 documents/serialisers.py:1871 | ||||||
| msgid "" | msgid "" | ||||||
| "Custom fields must be a list of integers or an object mapping ids to values." | "Custom fields must be a list of integers or an object mapping ids to values." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:1853 | #: documents/serialisers.py:1866 | ||||||
| msgid "Some custom fields don't exist or were specified twice." | msgid "Some custom fields don't exist or were specified twice." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: documents/serialisers.py:1923 | #: documents/serialisers.py:1936 | ||||||
| msgid "Invalid variable detected." | msgid "Invalid variable detected." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1003,6 +1003,18 @@ THREADS_PER_WORKER = os.getenv( | |||||||
| # Paperless Specific Settings                                                 # | # Paperless Specific Settings                                                 # | ||||||
| ############################################################################### | ############################################################################### | ||||||
|  |  | ||||||
|  | IGNORABLE_FILES: Final[list[str]] = [ | ||||||
|  |     ".DS_Store", | ||||||
|  |     ".DS_STORE", | ||||||
|  |     "._*", | ||||||
|  |     ".stfolder/*", | ||||||
|  |     ".stversions/*", | ||||||
|  |     ".localized/*", | ||||||
|  |     "desktop.ini", | ||||||
|  |     "@eaDir/*", | ||||||
|  |     "Thumbs.db", | ||||||
|  | ] | ||||||
|  |  | ||||||
| CONSUMER_POLLING = int(os.getenv("PAPERLESS_CONSUMER_POLLING", 0)) | CONSUMER_POLLING = int(os.getenv("PAPERLESS_CONSUMER_POLLING", 0)) | ||||||
|  |  | ||||||
| CONSUMER_POLLING_DELAY = int(os.getenv("PAPERLESS_CONSUMER_POLLING_DELAY", 5)) | CONSUMER_POLLING_DELAY = int(os.getenv("PAPERLESS_CONSUMER_POLLING_DELAY", 5)) | ||||||
| @@ -1025,7 +1037,7 @@ CONSUMER_IGNORE_PATTERNS = list( | |||||||
|     json.loads( |     json.loads( | ||||||
|         os.getenv( |         os.getenv( | ||||||
|             "PAPERLESS_CONSUMER_IGNORE_PATTERNS", |             "PAPERLESS_CONSUMER_IGNORE_PATTERNS", | ||||||
|             '[".DS_Store", ".DS_STORE", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini", "@eaDir/*", "Thumbs.db"]', |             json.dumps(IGNORABLE_FILES), | ||||||
|         ), |         ), | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user