Compare commits

..

89 Commits

Author SHA1 Message Date
shamoon
d9eb6a9224 Docs 2026-02-12 11:51:56 -08:00
shamoon
3ba48953aa DRY 2026-02-12 11:42:43 -08:00
shamoon
56e52f8701 Guard against stale ws events for version uploads 2026-02-12 11:39:34 -08:00
shamoon
825b241362 Ah, index should handle delete, update root when version changed 2026-02-12 11:35:04 -08:00
shamoon
e5d7abc8f9 Only send version when needed for metadata too 2026-02-12 11:34:59 -08:00
shamoon
472021b803 Fix discard goes to wrong version content 2026-02-12 11:15:31 -08:00
shamoon
c7c9845806 pre-fetch versions 2026-02-12 11:10:41 -08:00
shamoon
3b4112f930 Only append when needed 2026-02-12 11:07:11 -08:00
shamoon
6813542f29 Fix UI content should change on version select and thumbnail 2026-02-12 11:05:10 -08:00
shamoon
31e57db7ab Frontend apply version id to retrieval when needed 2026-02-12 11:03:50 -08:00
shamoon
aceeb26d32 Allow retrieve to pull specific version 2026-02-12 10:56:04 -08:00
shamoon
755915c357 typing stuff 2026-02-12 10:33:00 -08:00
shamoon
b7d3be6f75 Make content edits target a specific version 2026-02-12 10:27:49 -08:00
shamoon
6a0fae67e9 Make content follow the version
- store content per version
- root doc retrieval returns latest content
- updating content affects the latest version
- load metadata per version
2026-02-12 10:20:47 -08:00
shamoon
60e400fb68 Fix version suffix 2026-02-11 23:24:55 -08:00
shamoon
595603f695 mypy too 2026-02-11 23:06:50 -08:00
shamoon
c414857ac4 pyrefly happy? 2026-02-11 23:02:46 -08:00
shamoon
f12d5cb610 Unify this a bit 2026-02-11 23:00:08 -08:00
shamoon
74ce218b78 more backend coverage 2026-02-11 22:41:59 -08:00
shamoon
f5195cdb96 Last part of frontend coverage 2026-02-11 22:16:35 -08:00
shamoon
46b4763706 Merge branch 'dev' into feature-document-versions-1218 2026-02-11 22:08:38 -08:00
shamoon
158aa46f9a Frontend coverage, at least 2026-02-11 22:08:31 -08:00
shamoon
addb369d32 Simplfy this too 2026-02-11 21:56:40 -08:00
shamoon
fea289c29c Some markup stuff 2026-02-11 21:45:21 -08:00
shamoon
de09a62550 Simplify this 2026-02-11 21:39:02 -08:00
shamoon
fe6b3a1a41 some more backend coverage 2026-02-11 00:08:53 -08:00
shamoon
65bf55f610 Move for sonar 2026-02-11 00:02:10 -08:00
shamoon
8391469b1c Coverage for doc details 2026-02-10 23:58:55 -08:00
shamoon
a4f448e930 Oops leftover 2026-02-10 23:47:42 -08:00
shamoon
c2b4787c45 Sonar 2026-02-10 23:45:07 -08:00
shamoon
865c79a9cc Set sp 2026-02-10 23:35:00 -08:00
shamoon
19171b1641 Tweak markup 2026-02-10 23:34:55 -08:00
shamoon
64e95d9903 Consolidate migration 2026-02-10 23:17:51 -08:00
shamoon
6092ea8ee8 Update .mypy-baseline.txt 2026-02-10 22:41:15 -08:00
shamoon
9cd71de89d No of course it wasnt 2026-02-10 22:20:33 -08:00
shamoon
06b5c22858 Please be the last one 2026-02-10 22:03:50 -08:00
shamoon
b1f2606022 et tu, mypy? 2026-02-10 21:52:13 -08:00
shamoon
5a0a8a58b3 Try this way 2026-02-10 21:15:33 -08:00
shamoon
1a47f3801f Ok one more 2026-02-10 21:11:04 -08:00
shamoon
23390d0890 More typing stuff 2026-02-10 21:08:36 -08:00
shamoon
8b663393c2 type checking 2026-02-10 20:52:00 -08:00
shamoon
640025f2a9 Ugh, help with typing stuff? 2026-02-10 20:52:00 -08:00
shamoon
e0a1688be8 Consistently version_label not label 2026-02-10 20:51:59 -08:00
shamoon
ddbf9982a5 frontend tests 2026-02-10 20:08:17 -08:00
shamoon
d36a64d3fe some backend tests 2026-02-10 19:52:37 -08:00
shamoon
4e70f304fe fix frontend tests 2026-02-10 19:45:08 -08:00
shamoon
8eb931f6f6 fix backend tests, schema 2026-02-10 19:45:07 -08:00
shamoon
1d0e80c784 Update views.py 2026-02-10 18:34:35 -08:00
shamoon
8b722a3db5 Fix deleted audit log
[ci skip]
2026-02-10 17:54:17 -08:00
shamoon
9d3e62ff16 audit log entries for version 2026-02-10 17:27:20 -08:00
shamoon
d81748b39d Merge branch 'dev' into feature-document-versions-1218 2026-02-10 17:02:06 -08:00
shamoon
daa4586eeb Bulk edit and actions should update version 2026-02-10 16:42:38 -08:00
shamoon
8014932419 head --> root to avoid confusion, prevent root deletion
[ci skip]
2026-02-10 16:26:13 -08:00
shamoon
7fa400f486 Markup stuff 2026-02-10 16:13:34 -08:00
shamoon
43480bb611 Merge migrations
[ci skip]
2026-02-10 16:05:07 -08:00
shamoon
99199efb5f Merge branch 'dev' into feature-document-versions-1218 2026-02-10 16:04:07 -08:00
shamoon
bfb65a1eb8 Fix switching between docs 2026-02-10 15:10:45 -08:00
shamoon
b676397b80 Fix these ones 2026-02-10 13:52:20 -08:00
shamoon
5dd2e1040d Love an icon 2026-02-10 13:30:45 -08:00
shamoon
f7413506f3 Versions, move dropdown 2026-02-10 13:27:30 -08:00
shamoon
40d5f8f756 Exclude versions from duplicates 2026-02-10 13:25:17 -08:00
shamoon
a5c211cc0f Handle opening a version should rediect to head 2026-02-10 13:20:45 -08:00
shamoon
667e4b81eb Sweet, live updating 2026-02-10 13:13:21 -08:00
shamoon
3a5a32771e Update versions after upload finishes 2026-02-10 11:11:23 -08:00
shamoon
79001c280d Refresh versions after delete 2026-02-10 11:07:39 -08:00
shamoon
6ecd66da86 Delete version ui 2026-02-10 10:42:21 -08:00
shamoon
41d8854f56 Add delete-version endpoint 2026-02-10 09:57:01 -08:00
shamoon
57395ff99c Trash versions when deleting head docs 2026-02-10 09:53:28 -08:00
shamoon
90e3ed142f Set added timestamp for new versions 2026-02-10 09:47:16 -08:00
shamoon
9ca80af42f Frontend version info updates, checksum 2026-02-10 09:43:15 -08:00
shamoon
224a873de2 Version label 2026-02-10 09:16:20 -08:00
shamoon
719582938e Fix consume task args 2026-02-10 00:15:30 -08:00
shamoon
9b0af67033 Move migration 2026-02-09 23:42:25 -08:00
shamoon
7f2789e323 Merge branch 'dev' into feature-document-versions-1218 2026-02-09 23:41:44 -08:00
shamoon
b436530e4f Fix tests 2025-09-20 16:08:36 -07:00
shamoon
0ab94ab130 Make head_version and versions read-only via API 2025-09-20 15:38:21 -07:00
shamoon
ce5f5140f9 Random cleanup 2025-09-20 10:47:56 -07:00
shamoon
d8cb07b4a6 Llint 2025-09-20 10:17:54 -07:00
shamoon
1e48f9f9a9 Fix migration 2025-09-20 10:11:03 -07:00
shamoon
dc20db39e7 Fix caching
[ci skip]
2025-09-20 10:10:09 -07:00
shamoon
065f501272 Fix frontend versions switching
[ci skip]
2025-09-20 10:10:09 -07:00
shamoon
339a4db893 Update views.py 2025-09-20 10:10:08 -07:00
shamoon
0cc5f12cbf version aware doc endpoints 2025-09-20 10:10:08 -07:00
shamoon
e099998b2f Fix archive filename clash 2025-09-20 10:10:07 -07:00
shamoon
521628c1c3 Super basic UI stuff
[ci skip]
2025-09-20 10:10:07 -07:00
shamoon
80ed84f538 Bulk editing to update version instead of replace 2025-09-20 10:10:06 -07:00
shamoon
2557c03463 Fix migration 2025-09-20 10:09:35 -07:00
shamoon
9ed75561e7 Basic start of update endpoint 2025-09-20 10:09:34 -07:00
shamoon
02a7500696 Add head_version 2025-09-20 10:09:31 -07:00
329 changed files with 17945 additions and 29543 deletions

View File

@@ -14,6 +14,10 @@ component_management:
# https://docs.codecov.com/docs/carryforward-flags
flags:
# Backend Python versions
backend-python-3.10:
paths:
- src/**
carryforward: true
backend-python-3.11:
paths:
- src/**
@@ -22,14 +26,6 @@ flags:
paths:
- src/**
carryforward: true
backend-python-3.13:
paths:
- src/**
carryforward: true
backend-python-3.14:
paths:
- src/**
carryforward: true
# Frontend (shards merge into single flag)
frontend-node-24.x:
paths:
@@ -45,10 +41,9 @@ coverage:
project:
backend:
flags:
- backend-python-3.10
- backend-python-3.11
- backend-python-3.12
- backend-python-3.13
- backend-python-3.14
paths:
- src/**
# https://docs.codecov.com/docs/commit-status#threshold
@@ -64,10 +59,9 @@ coverage:
patch:
backend:
flags:
- backend-python-3.10
- backend-python-3.11
- backend-python-3.12
- backend-python-3.13
- backend-python-3.14
paths:
- src/**
target: 100%

View File

@@ -64,6 +64,8 @@ ARG RUNTIME_PACKAGES="\
libmagic1 \
media-types \
zlib1g \
# Barcode splitter
libzbar0 \
poppler-utils \
htop \
sudo"

View File

@@ -39,6 +39,3 @@ max_line_length = off
[Dockerfile*]
indent_style = space
[*.toml]
indent_style = space

View File

@@ -12,8 +12,6 @@ updates:
open-pull-requests-limit: 10
schedule:
interval: "monthly"
cooldown:
default-days: 7
labels:
- "frontend"
- "dependencies"
@@ -38,9 +36,7 @@ updates:
directory: "/"
# Check for updates once a week
schedule:
interval: "monthly"
cooldown:
default-days: 7
interval: "weekly"
labels:
- "backend"
- "dependencies"
@@ -73,6 +69,7 @@ updates:
patterns:
- "ocrmypdf"
- "pdf2image"
- "pyzbar"
- "zxing-cpp"
- "tika-client"
- "gotenberg-client"
@@ -101,8 +98,6 @@ updates:
schedule:
# Check for updates to GitHub Actions every month
interval: "monthly"
cooldown:
default-days: 7
labels:
- "ci-cd"
- "dependencies"
@@ -118,9 +113,7 @@ updates:
- "/"
- "/.devcontainer/"
schedule:
interval: "monthly"
cooldown:
default-days: 7
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
@@ -131,9 +124,7 @@ updates:
- package-ecosystem: "docker-compose"
directory: "/docker/compose/"
schedule:
interval: "monthly"
cooldown:
default-days: 7
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
@@ -157,11 +148,3 @@ updates:
postgres:
patterns:
- "docker.io/library/postgres*"
- package-ecosystem: "pre-commit" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "monthly"
groups:
pre-commit-dependencies:
patterns:
- "*"

View File

@@ -3,9 +3,21 @@ on:
push:
branches-ignore:
- 'translations**'
paths:
- 'src/**'
- 'pyproject.toml'
- 'uv.lock'
- 'docker/compose/docker-compose.ci-test.yml'
- '.github/workflows/ci-backend.yml'
pull_request:
branches-ignore:
- 'translations**'
paths:
- 'src/**'
- 'pyproject.toml'
- 'uv.lock'
- 'docker/compose/docker-compose.ci-test.yml'
- '.github/workflows/ci-backend.yml'
workflow_dispatch:
concurrency:
group: backend-${{ github.event.pull_request.number || github.ref }}
@@ -14,75 +26,27 @@ env:
DEFAULT_UV_VERSION: "0.10.x"
NLTK_DATA: "/usr/share/nltk_data"
jobs:
changes:
name: Detect Backend Changes
runs-on: ubuntu-slim
outputs:
backend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.backend == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Decide run mode
id: force
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "run_all=true" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event_name }}" == "push" && ( "${{ github.ref_name }}" == "main" || "${{ github.ref_name }}" == "dev" ) ]]; then
echo "run_all=true" >> "$GITHUB_OUTPUT"
else
echo "run_all=false" >> "$GITHUB_OUTPUT"
fi
- name: Set diff range
id: range
if: steps.force.outputs.run_all != 'true'
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "base=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event.created }}" == "true" ]]; then
echo "base=${{ github.event.repository.default_branch }}" >> "$GITHUB_OUTPUT"
else
echo "base=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
fi
echo "ref=${{ github.sha }}" >> "$GITHUB_OUTPUT"
- name: Detect changes
id: filter
if: steps.force.outputs.run_all != 'true'
uses: dorny/paths-filter@v3.0.2
with:
base: ${{ steps.range.outputs.base }}
ref: ${{ steps.range.outputs.ref }}
filters: |
backend:
- 'src/**'
- 'pyproject.toml'
- 'uv.lock'
- 'docker/compose/docker-compose.ci-test.yml'
- '.github/workflows/ci-backend.yml'
test:
needs: changes
if: needs.changes.outputs.backend_changed == 'true'
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13', '3.14']
python-version: ['3.10', '3.11', '3.12']
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
- name: Start containers
run: |
docker compose --file docker/compose/docker-compose.ci-test.yml pull --quiet
docker compose --file docker/compose/docker-compose.ci-test.yml up --detach
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6.2.0
uses: actions/setup-python@v6
with:
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@v7.3.1
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -91,7 +55,7 @@ jobs:
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends \
unpaper tesseract-ocr imagemagick ghostscript poppler-utils
unpaper tesseract-ocr imagemagick ghostscript libzbar0 poppler-utils
- name: Configure ImageMagick
run: |
sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml
@@ -119,13 +83,13 @@ jobs:
pytest
- name: Upload test results to Codecov
if: always()
uses: codecov/codecov-action@v5.5.2
uses: codecov/codecov-action@v5
with:
flags: backend-python-${{ matrix.python-version }}
files: junit.xml
report_type: test_results
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.5.2
uses: codecov/codecov-action@v5
with:
flags: backend-python-${{ matrix.python-version }}
files: coverage.xml
@@ -136,22 +100,20 @@ jobs:
docker compose --file docker/compose/docker-compose.ci-test.yml logs
docker compose --file docker/compose/docker-compose.ci-test.yml down
typing:
needs: changes
if: needs.changes.outputs.backend_changed == 'true'
name: Check project typing
runs-on: ubuntu-24.04
env:
DEFAULT_PYTHON: "3.12"
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6.0.1
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6.2.0
with:
python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv
uses: astral-sh/setup-uv@v7.3.1
uses: astral-sh/setup-uv@v7.2.1
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -167,7 +129,6 @@ jobs:
run: |
uv pip list
- name: Check typing (pyrefly)
continue-on-error: true
run: |
uv run pyrefly \
check \
@@ -182,33 +143,8 @@ jobs:
${{ runner.os }}-mypy-py${{ env.DEFAULT_PYTHON }}-
${{ runner.os }}-mypy-
- name: Check typing (mypy)
continue-on-error: true
run: |
uv run mypy \
--show-error-codes \
--warn-unused-configs \
src/ | uv run mypy-baseline filter
gate:
name: Backend CI Gate
needs: [changes, test, typing]
if: always()
runs-on: ubuntu-slim
steps:
- name: Check gate
run: |
if [[ "${{ needs.changes.outputs.backend_changed }}" != "true" ]]; then
echo "No backend-relevant changes detected."
exit 0
fi
if [[ "${{ needs.test.result }}" != "success" ]]; then
echo "::error::Backend test job result: ${{ needs.test.result }}"
exit 1
fi
if [[ "${{ needs.typing.result }}" != "success" ]]; then
echo "::error::Backend typing job result: ${{ needs.typing.result }}"
exit 1
fi
echo "Backend checks passed."

View File

@@ -41,7 +41,7 @@ jobs:
ref-name: ${{ steps.ref.outputs.name }}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6.0.1
- name: Determine ref name
id: ref
run: |
@@ -104,9 +104,9 @@ jobs:
echo "repository=${repo_name}"
echo "name=${repo_name}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4.0.0
uses: docker/setup-buildx-action@v3.12.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.0.0
uses: docker/login-action@v3.7.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -130,7 +130,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6.19.2
uses: docker/build-push-action@v6.18.0
with:
context: .
file: ./Dockerfile
@@ -149,16 +149,15 @@ jobs:
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
echo "digest=${digest}"
echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
if: steps.check-push.outputs.should-push == 'true'
uses: actions/upload-artifact@v7.0.0
uses: actions/upload-artifact@v6.0.0
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/digest-${{ matrix.arch }}.txt
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
archive: false
merge-and-push:
name: Merge and Push Manifest
runs-on: ubuntu-24.04
@@ -169,32 +168,32 @@ jobs:
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@v8.0.0
uses: actions/download-artifact@v7.0.0
with:
path: /tmp/digests
pattern: digest-*.txt
pattern: digests-*
merge-multiple: true
- name: List digests
run: |
echo "Downloaded digests:"
ls -la /tmp/digests/
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4.0.0
uses: docker/setup-buildx-action@v3.12.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.0.0
uses: docker/login-action@v3.7.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: needs.build-arch.outputs.push-external == 'true'
uses: docker/login-action@v4.0.0
uses: docker/login-action@v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Quay.io
if: needs.build-arch.outputs.push-external == 'true'
uses: docker/login-action@v4.0.0
uses: docker/login-action@v3.7.0
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
@@ -218,9 +217,8 @@ jobs:
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "${DOCKER_METADATA_OUTPUT_JSON}")
digests=""
for digest_file in digest-*.txt; do
digest=$(cat "${digest_file}")
digests+="${{ env.REGISTRY }}/${REPOSITORY}@${digest} "
for digest in *; do
digests+="${{ env.REGISTRY }}/${REPOSITORY}@sha256:${digest} "
done
echo "Creating manifest with tags: ${tags}"

View File

@@ -1,9 +1,22 @@
name: Documentation
on:
push:
branches-ignore:
- 'translations**'
branches:
- main
- dev
paths:
- 'docs/**'
- 'zensical.toml'
- 'pyproject.toml'
- 'uv.lock'
- '.github/workflows/ci-docs.yml'
pull_request:
paths:
- 'docs/**'
- 'zensical.toml'
- 'pyproject.toml'
- 'uv.lock'
- '.github/workflows/ci-docs.yml'
workflow_dispatch:
concurrency:
group: docs-${{ github.event.pull_request.number || github.ref }}
@@ -13,71 +26,23 @@ permissions:
pages: write
id-token: write
env:
DEFAULT_UV_VERSION: "0.10.x"
DEFAULT_PYTHON_VERSION: "3.12"
DEFAULT_UV_VERSION: "0.9.x"
DEFAULT_PYTHON_VERSION: "3.11"
jobs:
changes:
name: Detect Docs Changes
runs-on: ubuntu-slim
outputs:
docs_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.docs == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Decide run mode
id: force
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "run_all=true" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event_name }}" == "push" && ( "${{ github.ref_name }}" == "main" || "${{ github.ref_name }}" == "dev" ) ]]; then
echo "run_all=true" >> "$GITHUB_OUTPUT"
else
echo "run_all=false" >> "$GITHUB_OUTPUT"
fi
- name: Set diff range
id: range
if: steps.force.outputs.run_all != 'true'
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "base=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event.created }}" == "true" ]]; then
echo "base=${{ github.event.repository.default_branch }}" >> "$GITHUB_OUTPUT"
else
echo "base=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
fi
echo "ref=${{ github.sha }}" >> "$GITHUB_OUTPUT"
- name: Detect changes
id: filter
if: steps.force.outputs.run_all != 'true'
uses: dorny/paths-filter@v3.0.2
with:
base: ${{ steps.range.outputs.base }}
ref: ${{ steps.range.outputs.ref }}
filters: |
docs:
- 'docs/**'
- 'zensical.toml'
- 'pyproject.toml'
- 'uv.lock'
- '.github/workflows/ci-docs.yml'
build:
needs: changes
if: needs.changes.outputs.docs_changed == 'true'
name: Build Documentation
runs-on: ubuntu-24.04
steps:
- uses: actions/configure-pages@v5.0.0
- uses: actions/configure-pages@v5
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6.2.0
uses: actions/setup-python@v6
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v7.3.1
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -93,40 +58,21 @@ jobs:
--frozen \
zensical build --clean
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v4.0.0
uses: actions/upload-pages-artifact@v4
with:
path: site
name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
deploy:
name: Deploy Documentation
needs: [changes, build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.docs_changed == 'true'
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy GitHub Pages
uses: actions/deploy-pages@v4.0.5
uses: actions/deploy-pages@v4
id: deployment
with:
artifact_name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
gate:
name: Docs CI Gate
needs: [changes, build]
if: always()
runs-on: ubuntu-slim
steps:
- name: Check gate
run: |
if [[ "${{ needs.changes.outputs.docs_changed }}" != "true" ]]; then
echo "No docs-relevant changes detected."
exit 0
fi
if [[ "${{ needs.build.result }}" != "success" ]]; then
echo "::error::Docs build job result: ${{ needs.build.result }}"
exit 1
fi
echo "Docs checks passed."

View File

@@ -3,78 +3,39 @@ on:
push:
branches-ignore:
- 'translations**'
paths:
- 'src-ui/**'
- '.github/workflows/ci-frontend.yml'
pull_request:
branches-ignore:
- 'translations**'
paths:
- 'src-ui/**'
- '.github/workflows/ci-frontend.yml'
workflow_dispatch:
concurrency:
group: frontend-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Detect Frontend Changes
runs-on: ubuntu-slim
outputs:
frontend_changed: ${{ steps.force.outputs.run_all == 'true' || steps.filter.outputs.frontend == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Decide run mode
id: force
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "run_all=true" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event_name }}" == "push" && ( "${{ github.ref_name }}" == "main" || "${{ github.ref_name }}" == "dev" ) ]]; then
echo "run_all=true" >> "$GITHUB_OUTPUT"
else
echo "run_all=false" >> "$GITHUB_OUTPUT"
fi
- name: Set diff range
id: range
if: steps.force.outputs.run_all != 'true'
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "base=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event.created }}" == "true" ]]; then
echo "base=${{ github.event.repository.default_branch }}" >> "$GITHUB_OUTPUT"
else
echo "base=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
fi
echo "ref=${{ github.sha }}" >> "$GITHUB_OUTPUT"
- name: Detect changes
id: filter
if: steps.force.outputs.run_all != 'true'
uses: dorny/paths-filter@v3.0.2
with:
base: ${{ steps.range.outputs.base }}
ref: ${{ steps.range.outputs.ref }}
filters: |
frontend:
- 'src-ui/**'
- '.github/workflows/ci-frontend.yml'
install-dependencies:
needs: changes
if: needs.changes.outputs.frontend_changed == 'true'
name: Install Dependencies
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v5.0.3
uses: actions/cache@v5
with:
path: |
~/.pnpm-store
@@ -84,24 +45,23 @@ jobs:
run: cd src-ui && pnpm install
lint:
name: Lint
needs: [changes, install-dependencies]
if: needs.changes.outputs.frontend_changed == 'true'
needs: install-dependencies
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@v5.0.3
uses: actions/cache@v5
with:
path: |
~/.pnpm-store
@@ -113,8 +73,7 @@ jobs:
run: cd src-ui && pnpm run lint
unit-tests:
name: "Unit Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
needs: [changes, install-dependencies]
if: needs.changes.outputs.frontend_changed == 'true'
needs: install-dependencies
runs-on: ubuntu-24.04
strategy:
fail-fast: false
@@ -124,19 +83,19 @@ jobs:
shard-count: [4]
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@v5.0.3
uses: actions/cache@v5
with:
path: |
~/.pnpm-store
@@ -148,20 +107,19 @@ jobs:
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
- name: Upload test results to Codecov
if: always()
uses: codecov/codecov-action@v5.5.2
uses: codecov/codecov-action@v5
with:
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/
report_type: test_results
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.5.2
uses: codecov/codecov-action@v5
with:
flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/
e2e-tests:
name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
needs: [changes, install-dependencies]
if: needs.changes.outputs.frontend_changed == 'true'
needs: install-dependencies
runs-on: ubuntu-24.04
container: mcr.microsoft.com/playwright:v1.58.2-noble
env:
@@ -175,19 +133,19 @@ jobs:
shard-count: [2]
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@v5.0.3
uses: actions/cache@v5
with:
path: |
~/.pnpm-store
@@ -201,26 +159,23 @@ jobs:
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
bundle-analysis:
name: Bundle Analysis
needs: [changes, unit-tests, e2e-tests]
if: needs.changes.outputs.frontend_changed == 'true'
needs: [unit-tests, e2e-tests]
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 2
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@v5.0.3
uses: actions/cache@v5
with:
path: |
~/.pnpm-store
@@ -232,42 +187,3 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: cd src-ui && pnpm run build --configuration=production
gate:
name: Frontend CI Gate
needs: [changes, install-dependencies, lint, unit-tests, e2e-tests, bundle-analysis]
if: always()
runs-on: ubuntu-slim
steps:
- name: Check gate
run: |
if [[ "${{ needs.changes.outputs.frontend_changed }}" != "true" ]]; then
echo "No frontend-relevant changes detected."
exit 0
fi
if [[ "${{ needs['install-dependencies'].result }}" != "success" ]]; then
echo "::error::Frontend install job result: ${{ needs['install-dependencies'].result }}"
exit 1
fi
if [[ "${{ needs.lint.result }}" != "success" ]]; then
echo "::error::Frontend lint job result: ${{ needs.lint.result }}"
exit 1
fi
if [[ "${{ needs['unit-tests'].result }}" != "success" ]]; then
echo "::error::Frontend unit-tests job result: ${{ needs['unit-tests'].result }}"
exit 1
fi
if [[ "${{ needs['e2e-tests'].result }}" != "success" ]]; then
echo "::error::Frontend e2e-tests job result: ${{ needs['e2e-tests'].result }}"
exit 1
fi
if [[ "${{ needs['bundle-analysis'].result }}" != "success" ]]; then
echo "::error::Frontend bundle-analysis job result: ${{ needs['bundle-analysis'].result }}"
exit 1
fi
echo "Frontend checks passed."

View File

@@ -8,8 +8,8 @@ concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
DEFAULT_UV_VERSION: "0.10.x"
DEFAULT_PYTHON_VERSION: "3.12"
DEFAULT_UV_VERSION: "0.9.x"
DEFAULT_PYTHON_VERSION: "3.11"
jobs:
wait-for-docker:
name: Wait for Docker Build
@@ -28,14 +28,14 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
# ---- Frontend Build ----
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6
with:
node-version: 24.x
cache: 'pnpm'
@@ -47,11 +47,11 @@ jobs:
# ---- Backend Setup ----
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6.2.0
uses: actions/setup-python@v6
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v7.3.1
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -118,7 +118,7 @@ jobs:
sudo chown -R 1000:1000 paperless-ngx/
tar -cJf paperless-ngx.tar.xz paperless-ngx/
- name: Upload release artifact
uses: actions/upload-artifact@v7.0.0
uses: actions/upload-artifact@v6
with:
name: release
path: dist/paperless-ngx.tar.xz
@@ -133,7 +133,7 @@ jobs:
version: ${{ steps.get-version.outputs.version }}
steps:
- name: Download release artifact
uses: actions/download-artifact@v8.0.0
uses: actions/download-artifact@v7
with:
name: release
path: ./
@@ -148,7 +148,7 @@ jobs:
fi
- name: Create release and changelog
id: create-release
uses: release-drafter/release-drafter@v6.2.0
uses: release-drafter/release-drafter@v6
with:
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.version }}
@@ -159,7 +159,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release archive
uses: shogo82148/actions-upload-release-asset@v1.9.2
uses: shogo82148/actions-upload-release-asset@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
upload_url: ${{ steps.create-release.outputs.upload_url }}
@@ -176,16 +176,16 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
with:
ref: main
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6.2.0
uses: actions/setup-python@v6
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v7.3.1
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -218,7 +218,7 @@ jobs:
git commit -am "Changelog ${{ needs.publish-release.outputs.version }} - GHA"
git push origin ${{ needs.publish-release.outputs.version }}-changelog
- name: Create pull request
uses: actions/github-script@v8.0.0
uses: actions/github-script@v8
with:
script: |
const { repo, owner } = context.repo;

View File

@@ -34,10 +34,10 @@ jobs:
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.32.5
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -45,4 +45,4 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.32.5
uses: github/codeql-action/analyze@v4

View File

@@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
with:
token: ${{ secrets.PNGX_BOT_PAT }}
- name: crowdin action
uses: crowdin/github-action@v2.15.0
uses: crowdin/github-action@v2
with:
upload_translations: false
download_translations: true

View File

@@ -2,28 +2,17 @@ name: PR Bot
on:
pull_request_target:
types: [opened]
permissions:
contents: read
pull-requests: write
jobs:
anti-slop:
runs-on: ubuntu-latest
permissions:
contents: read
issues: read
pull-requests: write
steps:
- uses: peakoss/anti-slop@v0.2.1
with:
max-failures: 4
failure-add-pr-labels: 'ai'
pr-bot:
name: Automated PR Bot
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Label PR by file path or branch name
# see .github/labeler.yml for the labeler config
uses: actions/labeler@v6.0.1
uses: actions/labeler@v6
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Label by size
@@ -37,7 +26,7 @@ jobs:
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$
- name: Label by PR title
uses: actions/github-script@v8.0.0
uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;
@@ -63,7 +52,7 @@ jobs:
}
- name: Label bot-generated PRs
if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }}
uses: actions/github-script@v8.0.0
uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;
@@ -88,7 +77,7 @@ jobs:
}
- name: Welcome comment
if: ${{ !contains(github.actor, 'bot') }}
uses: actions/github-script@v8.0.0
uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;

View File

@@ -19,6 +19,6 @@ jobs:
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
steps:
- name: Label PR with release-drafter
uses: release-drafter/release-drafter@v6.2.0
uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -15,7 +15,7 @@ jobs:
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/stale@v10.2.0
- uses: actions/stale@v10
with:
days-before-stale: 7
days-before-close: 14
@@ -37,7 +37,7 @@ jobs:
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: dessant/lock-threads@v6.0.0
- uses: dessant/lock-threads@v6
with:
issue-inactive-days: '30'
pr-inactive-days: '30'
@@ -57,7 +57,7 @@ jobs:
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v8.0.0
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {
@@ -114,7 +114,7 @@ jobs:
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v8.0.0
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {
@@ -206,7 +206,7 @@ jobs:
if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v8.0.0
- uses: actions/github-script@v8
with:
script: |
function sleep(ms) {

View File

@@ -11,7 +11,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6.0.2
uses: actions/checkout@v6
env:
GH_REF: ${{ github.ref }} # sonar rule:githubactions:S7630 - avoid injection
with:
@@ -19,13 +19,13 @@ jobs:
ref: ${{ env.GH_REF }}
- name: Set up Python
id: setup-python
uses: actions/setup-python@v6.2.0
uses: actions/setup-python@v6
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext
- name: Install uv
uses: astral-sh/setup-uv@v7.3.1
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install backend python dependencies
@@ -36,18 +36,18 @@ jobs:
- name: Generate backend translation strings
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
- name: Install pnpm
uses: pnpm/action-setup@v4.2.0
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@v5.0.3
uses: actions/cache@v5
with:
path: |
~/.pnpm-store
@@ -63,7 +63,7 @@ jobs:
cd src-ui
pnpm run ng extract-i18n
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v7.1.0
uses: stefanzweifel/git-auto-commit-action@v7
with:
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
commit_message: "Auto translate strings"

View File

@@ -20,6 +20,7 @@ src/documents/admin.py:0: error: Skipping analyzing "auditlog.models": module is
src/documents/admin.py:0: error: Skipping analyzing "treenode.admin": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/barcodes.py:0: error: "Image" has no attribute "filename" [attr-defined]
src/documents/barcodes.py:0: error: Cannot find implementation or library stub for module named "zxingcpp" [import-not-found]
src/documents/barcodes.py:0: error: Skipping analyzing "pyzbar": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/bulk_download.py:0: error: Return type "None" of "add_document" incompatible with return type "Never" in supertype "BulkArchiveStrategy" [override]
src/documents/bulk_download.py:0: error: Return type "None" of "add_document" incompatible with return type "Never" in supertype "BulkArchiveStrategy" [override]
src/documents/bulk_download.py:0: error: Return type "None" of "add_document" incompatible with return type "Never" in supertype "BulkArchiveStrategy" [override]
@@ -96,6 +97,7 @@ src/documents/conditionals.py:0: error: Function is missing a type annotation fo
src/documents/conditionals.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/conditionals.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/conditionals.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/conditionals.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/consumer.py:0: error: "ConsumerPluginMixin" has no attribute "input_doc" [attr-defined]
src/documents/consumer.py:0: error: "ConsumerPluginMixin" has no attribute "metadata" [attr-defined]
src/documents/consumer.py:0: error: "ConsumerPluginMixin" has no attribute "metadata" [attr-defined]
@@ -341,9 +343,6 @@ src/documents/migrations/0001_initial.py:0: error: Skipping analyzing "multisele
src/documents/migrations/0001_initial.py:0: error: Skipping analyzing "multiselectfield.db.fields": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/migrations/0008_sharelinkbundle.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/migrations/0008_sharelinkbundle.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/migrations/0012_savedview_visibility_to_ui_settings.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/migrations/0012_savedview_visibility_to_ui_settings.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/migrations/0012_savedview_visibility_to_ui_settings.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/models.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type]
src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.correspondent'. [django-manager-missing]
src/documents/models.py:0: error: Couldn't resolve related manager 'documents' for relation 'documents.models.Document.document_type'. [django-manager-missing]
@@ -438,11 +437,15 @@ src/documents/permissions.py:0: error: Function is missing a type annotation [n
src/documents/permissions.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/permissions.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/permissions.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/permissions.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/permissions.py:0: error: Item "list[str]" of "Any | list[str] | QuerySet[User, User]" has no attribute "exclude" [union-attr]
src/documents/permissions.py:0: error: Item "list[str]" of "Any | list[str] | QuerySet[User, User]" has no attribute "exists" [union-attr]
src/documents/permissions.py:0: error: Item "list[str]" of "Any | list[str] | QuerySet[User, User]" has no attribute "exists" [union-attr]
src/documents/permissions.py:0: error: Missing type parameters for generic type "QuerySet" [type-arg]
src/documents/permissions.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/plugins/helpers.py:0: error: "Collection[str]" has no attribute "update" [attr-defined]
src/documents/plugins/helpers.py:0: error: Argument 1 to "send" of "BaseStatusManager" has incompatible type "dict[str, Collection[str]]"; expected "dict[str, str | int | None]" [arg-type]
src/documents/plugins/helpers.py:0: error: Argument 1 to "send" of "BaseStatusManager" has incompatible type "dict[str, Collection[str]]"; expected "dict[str, str | int | None]" [arg-type]
src/documents/plugins/helpers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/plugins/helpers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/plugins/helpers.py:0: error: Skipping analyzing "channels_redis.pubsub": module is installed, but missing library stubs or py.typed marker [import-untyped]
@@ -550,7 +553,7 @@ src/documents/serialisers.py:0: error: Function is missing a type annotation [n
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -641,7 +644,6 @@ src/documents/serialisers.py:0: error: Missing type parameters for generic type
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/serialisers.py:0: error: Need type annotation for "document" [var-annotated]
@@ -669,6 +671,7 @@ src/documents/signals/handlers.py:0: error: Argument 3 to "validate_move" has in
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -692,11 +695,15 @@ src/documents/signals/handlers.py:0: error: Function is missing a type annotatio
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Incompatible return value type (got "tuple[DocumentMetadataOverrides | None, str]", expected "tuple[DocumentMetadataOverrides, str] | None") [return-value]
src/documents/signals/handlers.py:0: error: Incompatible types in assignment (expression has type "list[Tag]", variable has type "set[Tag]") [assignment]
src/documents/signals/handlers.py:0: error: Incompatible types in assignment (expression has type "tuple[Any, Any, Any]", variable has type "tuple[Any, Any]") [assignment]
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "refresh_from_db" [union-attr]
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "save" [union-attr]
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "source_path" [union-attr]
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "tags" [union-attr]
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "tags" [union-attr]
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "title" [union-attr]
src/documents/signals/handlers.py:0: error: Item "None" of "Any | None" has no attribute "get" [union-attr]
src/documents/signals/handlers.py:0: error: Item "None" of "Any | None" has no attribute "get" [union-attr]
@@ -923,6 +930,7 @@ src/documents/tests/test_barcodes.py:0: error: Argument 1 to "get_reader" of "Ge
src/documents/tests/test_barcodes.py:0: error: Argument 3 to "BarcodePlugin" has incompatible type "DummyProgressManager"; expected "ProgressManager" [arg-type]
src/documents/tests/test_barcodes.py:0: error: Argument 3 to "BarcodePlugin" has incompatible type "DummyProgressManager"; expected "ProgressManager" [arg-type]
src/documents/tests/test_barcodes.py:0: error: Argument 3 to "BarcodePlugin" has incompatible type "DummyProgressManager"; expected "ProgressManager" [arg-type]
src/documents/tests/test_barcodes.py:0: error: Cannot find implementation or library stub for module named "zxingcpp" [import-not-found]
src/documents/tests/test_barcodes.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/test_barcodes.py:0: error: Incompatible types in assignment (expression has type "Path", variable has type "str") [assignment]
src/documents/tests/test_barcodes.py:0: error: Item "None" of "ApplicationConfiguration | None" has no attribute "barcode_string" [union-attr]
@@ -1176,14 +1184,6 @@ src/documents/tests/test_management_exporter.py:0: error: Skipping analyzing "al
src/documents/tests/test_management_fuzzy.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/test_management_retagger.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/test_management_superuser.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Cannot determine type of "apps" [has-type]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Cannot determine type of "apps" [has-type]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
src/documents/tests/test_migration_saved_view_visibility.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
src/documents/tests/test_migration_share_link_bundle.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_share_link_bundle.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_share_link_bundle.py:0: error: Incompatible types in assignment (expression has type "str", base class "TestMigrations" defined the type as "None") [assignment]
@@ -1543,6 +1543,7 @@ src/documents/views.py:0: error: "get_serializer_context" undefined in superclas
src/documents/views.py:0: error: "object" not callable [operator]
src/documents/views.py:0: error: "type[Model]" has no attribute "objects" [attr-defined]
src/documents/views.py:0: error: Argument "path" to "EmailAttachment" has incompatible type "Path | None"; expected "Path" [arg-type]
src/documents/views.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_correspondents" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_document_types" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_storage_paths" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
@@ -1560,6 +1561,8 @@ src/documents/views.py:0: error: Function is missing a return type annotation [
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
@@ -1616,8 +1619,7 @@ src/documents/views.py:0: error: Function is missing a type annotation [no-unty
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Incompatible type for lookup 'owner': (got "User | AnonymousUser", expected "User | int | None") [misc]
src/documents/views.py:0: error: Incompatible types in assignment (expression has type "Any | None", variable has type "dict[Any, Any]") [assignment]
src/documents/views.py:0: error: Incompatible types in assignment (expression has type "QuerySet[Any, Any]", variable has type "list[Any]") [assignment]
src/documents/views.py:0: error: Incompatible types in assignment (expression has type "QuerySet[Any, Any]", variable has type "list[Document]") [assignment]
@@ -1683,11 +1685,11 @@ src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable
src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Any]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[CustomField]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Group]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[MailAccount]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[MailRule]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[SavedView]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[User]" is not indexable [index]
src/documents/views.py:0: error: Value of type "Iterable[Workflow]" is not indexable [index]
src/documents/views.py:0: error: Value of type "dict[str, _PingReply] | None" is not indexable [index]
@@ -1814,6 +1816,7 @@ src/paperless/checks.py:0: error: Argument 2 to "path_check" has incompatible ty
src/paperless/checks.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/checks.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/checks.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/checks.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/checks.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/checks.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/checks.py:0: error: Function is missing a type annotation [no-untyped-def]
@@ -1936,7 +1939,6 @@ src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLaye
src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr]
src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr]
src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr]
src/paperless/tests/test_websockets.py:0: error: Item "None" of "BaseChannelLayer | None" has no attribute "group_send" [union-attr]
src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item]
src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item]
src/paperless/tests/test_websockets.py:0: error: TypedDict "_WebsocketTestScope" has no key "user" [typeddict-item]
@@ -2114,6 +2116,7 @@ src/paperless_mail/mail.py:0: error: Function is missing a return type annotatio
src/paperless_mail/mail.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless_mail/mail.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless_mail/mail.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless_mail/mail.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless_mail/mail.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/paperless_mail/mail.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/paperless_mail/mail.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -2183,34 +2186,34 @@ src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "flagged" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "seen" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "MailMessage" has no attribute "seen" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[att@481]" has no attribute "filename" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@427]" has no attribute "from_" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@427]" has no attribute "from_" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@427]" has no attribute "from_values" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message@420]" has no attribute "from_" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message@420]" has no attribute "from_values" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message@479]" has no attribute "subject" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message@532]" has no attribute "attachments" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[att@480]" has no attribute "filename" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@426]" has no attribute "from_" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@426]" has no attribute "from_" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message2@426]" has no attribute "from_values" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message@419]" has no attribute "from_" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message@419]" has no attribute "from_values" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message@478]" has no attribute "subject" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: "type[message@531]" has no attribute "attachments" [attr-defined]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxFolderSelectError" has incompatible type "None"; expected "tuple[Any, ...]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxFolderSelectError" has incompatible type "None"; expected "tuple[Any, ...]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "MailboxLoginError" has incompatible type "str"; expected "tuple[Any, ...]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message2@427]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message2@427]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@420]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@420]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@420]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@420]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@479]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@479]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@479]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message2@426]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message2@426]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@419]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@419]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@419]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_correspondent" of "MailAccountHandler" has incompatible type "type[message@419]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@478]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@478]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "_get_title" of "MailAccountHandler" has incompatible type "type[message@478]"; expected "MailMessage" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[MailMessage], TypeGuard[Never]]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 1 to "filter" has incompatible type "Callable[[Any], bool]"; expected "Callable[[MailMessage], TypeGuard[Never]]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@481]"; expected "MailAttachment" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@481]"; expected "MailAttachment" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@481]"; expected "MailAttachment" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@480]"; expected "MailAttachment" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@480]"; expected "MailAttachment" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "_get_title" of "MailAccountHandler" has incompatible type "type[att@480]"; expected "MailAttachment" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Argument 2 to "assertIn" of "TestCase" has incompatible type "str | None"; expected "Iterable[Any] | Container[Any]" [arg-type]
src/paperless_mail/tests/test_mail.py:0: error: Dict entry 0 has incompatible type "str": "None"; expected "str": "str" [dict-item]
src/paperless_mail/tests/test_mail.py:0: error: Dict entry 0 has incompatible type "str": "int"; expected "str": "str" [dict-item]

View File

@@ -29,7 +29,7 @@ repos:
- id: check-case-conflict
- id: detect-private-key
- repo: https://github.com/codespell-project/codespell
rev: v2.4.2
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies: [tomli]
@@ -46,11 +46,11 @@ repos:
- ts
- markdown
additional_dependencies:
- prettier@3.8.1
- 'prettier-plugin-organize-imports@4.3.0'
- prettier@3.3.3
- 'prettier-plugin-organize-imports@4.1.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.5
rev: v0.15.0
hooks:
- id: ruff-check
- id: ruff-format
@@ -65,7 +65,7 @@ repos:
- id: hadolint
# Shell script hooks
- repo: https://github.com/lovesegfault/beautysh
rev: v6.4.3
rev: v6.4.2
hooks:
- id: beautysh
types: [file]

View File

@@ -5,6 +5,14 @@ const config = {
singleQuote: true,
// https://prettier.io/docs/en/options.html#trailing-commas
trailingComma: 'es5',
overrides: [
{
files: ['docs/*.md'],
options: {
tabWidth: 4,
},
},
],
plugins: [require('prettier-plugin-organize-imports')],
}

View File

@@ -13,9 +13,7 @@ If you want to implement something big:
## Python
Paperless-ngx currently supports Python 3.11, 3.12, 3.13, and 3.14. As a policy, we aim to support at least the three most recent Python versions, and drop support for versions as they reach end-of-life. Older versions may be supported if dependencies permit, but this is not guaranteed.
We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
Paperless supports python 3.10 - 3.12 at this time. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/).
## Branches

View File

@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.10.9-python3.12-trixie-slim AS s6-overlay-base
FROM ghcr.io/astral-sh/uv:0.10.0-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6
@@ -45,7 +45,7 @@ ENV \
ARG TARGETARCH
ARG TARGETVARIANT
# Lock this version
ARG S6_OVERLAY_VERSION=3.2.2.0
ARG S6_OVERLAY_VERSION=3.2.1.0
ARG S6_BUILD_TIME_PKGS="curl \
xz-utils"
@@ -154,6 +154,8 @@ ARG RUNTIME_PACKAGES="\
libmagic1 \
media-types \
zlib1g \
# Barcode splitter
libzbar0 \
poppler-utils"
# Install basic runtime packages.

View File

@@ -4,7 +4,7 @@
# correct networking for the tests
services:
gotenberg:
image: docker.io/gotenberg/gotenberg:8.27
image: docker.io/gotenberg/gotenberg:8.26
hostname: gotenberg
container_name: gotenberg
network_mode: host

View File

@@ -72,7 +72,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.27
image: docker.io/gotenberg/gotenberg:8.26
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.

View File

@@ -56,7 +56,6 @@ services:
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
PAPERLESS_DBENGINE: postgres
env_file:
- stack.env
volumes:

View File

@@ -62,12 +62,11 @@ services:
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
PAPERLESS_DBENGINE: postgresql
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.27
image: docker.io/gotenberg/gotenberg:8.26
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.

View File

@@ -56,7 +56,6 @@ services:
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBHOST: db
PAPERLESS_DBENGINE: postgresql
volumes:
data:
media:

View File

@@ -51,12 +51,11 @@ services:
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBENGINE: sqlite
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.27
image: docker.io/gotenberg/gotenberg:8.26
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.

View File

@@ -42,7 +42,6 @@ services:
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_DBENGINE: sqlite
volumes:
data:
media:

View File

@@ -10,10 +10,8 @@ cd "${PAPERLESS_SRC_DIR}"
# The whole migrate, with flock, needs to run as the right user
if [[ -n "${USER_IS_NON_ROOT}" ]]; then
python3 manage.py check --tag compatibility paperless || exit 1
exec s6-setlock -n "${data_dir}/migration_lock" python3 manage.py migrate --skip-checks --no-input
else
s6-setuidgid paperless python3 manage.py check --tag compatibility paperless || exit 1
exec s6-setuidgid paperless \
s6-setlock -n "${data_dir}/migration_lock" \
python3 manage.py migrate --skip-checks --no-input

View File

@@ -10,16 +10,16 @@ consuming documents at that time.
Options available to any installation of paperless:
- Use the [document exporter](#exporter). The document exporter exports all your documents,
thumbnails, metadata, and database contents to a specific folder. You may import your
documents and settings into a fresh instance of paperless again or store your
documents in another DMS with this export.
- Use the [document exporter](#exporter). The document exporter exports all your documents,
thumbnails, metadata, and database contents to a specific folder. You may import your
documents and settings into a fresh instance of paperless again or store your
documents in another DMS with this export.
The document exporter is also able to update an already existing
export. Therefore, incremental backups with `rsync` are entirely
possible.
The document exporter is also able to update an already existing
export. Therefore, incremental backups with `rsync` are entirely
possible.
The exporter does not include API tokens and they will need to be re-generated after importing.
The exporter does not include API tokens and they will need to be re-generated after importing.
!!! caution
@@ -29,27 +29,28 @@ Options available to any installation of paperless:
Options available to docker installations:
- Backup the docker volumes. These usually reside within
`/var/lib/docker/volumes` on the host and you need to be root in
order to access them.
- Backup the docker volumes. These usually reside within
`/var/lib/docker/volumes` on the host and you need to be root in
order to access them.
Paperless uses 4 volumes:
- `paperless_media`: This is where your documents are stored.
- `paperless_data`: This is where auxiliary data is stored. This
folder also contains the SQLite database, if you use it.
- `paperless_pgdata`: Exists only if you use PostgreSQL and
contains the database.
- `paperless_dbdata`: Exists only if you use MariaDB and contains
the database.
Paperless uses 4 volumes:
- `paperless_media`: This is where your documents are stored.
- `paperless_data`: This is where auxiliary data is stored. This
folder also contains the SQLite database, if you use it.
- `paperless_pgdata`: Exists only if you use PostgreSQL and
contains the database.
- `paperless_dbdata`: Exists only if you use MariaDB and contains
the database.
Options available to bare-metal and non-docker installations:
- Backup the entire paperless folder. This ensures that if your
paperless instance crashes at some point or your disk fails, you can
simply copy the folder back into place and it works.
- Backup the entire paperless folder. This ensures that if your
paperless instance crashes at some point or your disk fails, you can
simply copy the folder back into place and it works.
When using PostgreSQL or MariaDB, you'll also have to backup the
database.
When using PostgreSQL or MariaDB, you'll also have to backup the
database.
### Restoring {#migrating-restoring}
@@ -61,10 +62,6 @@ copies you created in the steps above.
## Updating Paperless {#updating}
!!! warning
Please review the [migration instructions](migration-v3.md) before upgrading Paperless-ngx to v3.0, it includes some breaking changes that require manual intervention before upgrading.
### Docker Route {#docker-updating}
If a new release of paperless-ngx is available, upgrading depends on how
@@ -508,19 +505,19 @@ collection for issues.
The issues detected by the sanity checker are as follows:
- Missing original files.
- Missing archive files.
- Inaccessible original files due to improper permissions.
- Inaccessible archive files due to improper permissions.
- Corrupted original documents by comparing their checksum against
what is stored in the database.
- Corrupted archive documents by comparing their checksum against what
is stored in the database.
- Missing thumbnails.
- Inaccessible thumbnails due to improper permissions.
- Documents without any content (warning).
- Orphaned files in the media directory (warning). These are files
that are not referenced by any document in paperless.
- Missing original files.
- Missing archive files.
- Inaccessible original files due to improper permissions.
- Inaccessible archive files due to improper permissions.
- Corrupted original documents by comparing their checksum against
what is stored in the database.
- Corrupted archive documents by comparing their checksum against what
is stored in the database.
- Missing thumbnails.
- Inaccessible thumbnails due to improper permissions.
- Documents without any content (warning).
- Orphaned files in the media directory (warning). These are files
that are not referenced by any document in paperless.
```
document_sanity_checker

View File

@@ -25,20 +25,20 @@ documents.
The following algorithms are available:
- **None:** No matching will be performed.
- **Any:** Looks for any occurrence of any word provided in match in
the PDF. If you define the match as `Bank1 Bank2`, it will match
documents containing either of these terms.
- **All:** Requires that every word provided appears in the PDF,
albeit not in the order provided.
- **Exact:** Matches only if the match appears exactly as provided
(i.e. preserve ordering) in the PDF.
- **Regular expression:** Parses the match as a regular expression and
tries to find a match within the document.
- **Fuzzy match:** Uses a partial matching based on locating the tag text
inside the document, using a [partial ratio](https://rapidfuzz.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
- **Auto:** Tries to automatically match new documents. This does not
require you to set a match. See the [notes below](#automatic-matching).
- **None:** No matching will be performed.
- **Any:** Looks for any occurrence of any word provided in match in
the PDF. If you define the match as `Bank1 Bank2`, it will match
documents containing either of these terms.
- **All:** Requires that every word provided appears in the PDF,
albeit not in the order provided.
- **Exact:** Matches only if the match appears exactly as provided
(i.e. preserve ordering) in the PDF.
- **Regular expression:** Parses the match as a regular expression and
tries to find a match within the document.
- **Fuzzy match:** Uses a partial matching based on locating the tag text
inside the document, using a [partial ratio](https://rapidfuzz.github.io/RapidFuzz/Usage/fuzz.html#partial-ratio)
- **Auto:** Tries to automatically match new documents. This does not
require you to set a match. See the [notes below](#automatic-matching).
When using the _any_ or _all_ matching algorithms, you can search for
terms that consist of multiple words by enclosing them in double quotes.
@@ -69,33 +69,33 @@ Paperless tries to hide much of the involved complexity with this
approach. However, there are a couple caveats you need to keep in mind
when using this feature:
- Changes to your documents are not immediately reflected by the
matching algorithm. The neural network needs to be _trained_ on your
documents after changes. Paperless periodically (default: once each
hour) checks for changes and does this automatically for you.
- The Auto matching algorithm only takes documents into account which
are NOT placed in your inbox (i.e. have any inbox tags assigned to
them). This ensures that the neural network only learns from
documents which you have correctly tagged before.
- The matching algorithm can only work if there is a correlation
between the tag, correspondent, document type, or storage path and
the document itself. Your bank statements usually contain your bank
account number and the name of the bank, so this works reasonably
well, However, tags such as "TODO" cannot be automatically
assigned.
- The matching algorithm needs a reasonable number of documents to
identify when to assign tags, correspondents, storage paths, and
types. If one out of a thousand documents has the correspondent
"Very obscure web shop I bought something five years ago", it will
probably not assign this correspondent automatically if you buy
something from them again. The more documents, the better.
- Paperless also needs a reasonable amount of negative examples to
decide when not to assign a certain tag, correspondent, document
type, or storage path. This will usually be the case as you start
filling up paperless with documents. Example: If all your documents
are either from "Webshop" or "Bank", paperless will assign one
of these correspondents to ANY new document, if both are set to
automatic matching.
- Changes to your documents are not immediately reflected by the
matching algorithm. The neural network needs to be _trained_ on your
documents after changes. Paperless periodically (default: once each
hour) checks for changes and does this automatically for you.
- The Auto matching algorithm only takes documents into account which
are NOT placed in your inbox (i.e. have any inbox tags assigned to
them). This ensures that the neural network only learns from
documents which you have correctly tagged before.
- The matching algorithm can only work if there is a correlation
between the tag, correspondent, document type, or storage path and
the document itself. Your bank statements usually contain your bank
account number and the name of the bank, so this works reasonably
well, However, tags such as "TODO" cannot be automatically
assigned.
- The matching algorithm needs a reasonable number of documents to
identify when to assign tags, correspondents, storage paths, and
types. If one out of a thousand documents has the correspondent
"Very obscure web shop I bought something five years ago", it will
probably not assign this correspondent automatically if you buy
something from them again. The more documents, the better.
- Paperless also needs a reasonable amount of negative examples to
decide when not to assign a certain tag, correspondent, document
type, or storage path. This will usually be the case as you start
filling up paperless with documents. Example: If all your documents
are either from "Webshop" or "Bank", paperless will assign one
of these correspondents to ANY new document, if both are set to
automatic matching.
## Hooking into the consumption process {#consume-hooks}
@@ -243,12 +243,12 @@ webserver:
Troubleshooting:
- Monitor the Docker Compose log
`cd ~/paperless-ngx; docker compose logs -f`
- Check your script's permission e.g. in case of permission error
`sudo chmod 755 post-consumption-example.sh`
- Pipe your scripts's output to a log file e.g.
`echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`
- Monitor the Docker Compose log
`cd ~/paperless-ngx; docker compose logs -f`
- Check your script's permission e.g. in case of permission error
`sudo chmod 755 post-consumption-example.sh`
- Pipe your scripts's output to a log file e.g.
`echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log`
## File name handling {#file-name-handling}
@@ -262,10 +262,6 @@ your files differently, you can do that by adjusting the
or using [storage paths (see below)](#storage-paths). Paperless adds the
correct file extension e.g. `.pdf`, `.jpg` automatically.
When a document has file versions, each version uses the same naming rules and
storage path resolution as any other document file, with an added version suffix
such as `_v1`, `_v2`, etc.
This variable allows you to configure the filename (folders are allowed)
using placeholders. For example, configuring this to
@@ -307,35 +303,35 @@ will create a directory structure as follows:
Paperless provides the following variables for use within filenames:
- `{{ asn }}`: The archive serial number of the document, or "none".
- `{{ correspondent }}`: The name of the correspondent, or "none".
- `{{ document_type }}`: The name of the document type, or "none".
- `{{ tag_list }}`: A comma separated list of all tags assigned to the
document.
- `{{ title }}`: The title of the document.
- `{{ created }}`: The full date (ISO 8601 format, e.g. `2024-03-14`) the document was created.
- `{{ created_year }}`: Year created only, formatted as the year with
century.
- `{{ created_year_short }}`: Year created only, formatted as the year
without century, zero padded.
- `{{ created_month }}`: Month created only (number 01-12).
- `{{ created_month_name }}`: Month created name, as per locale
- `{{ created_month_name_short }}`: Month created abbreviated name, as per
locale
- `{{ created_day }}`: Day created only (number 01-31).
- `{{ added }}`: The full date (ISO format) the document was added to
paperless.
- `{{ added_year }}`: Year added only.
- `{{ added_year_short }}`: Year added only, formatted as the year without
century, zero padded.
- `{{ added_month }}`: Month added only (number 01-12).
- `{{ added_month_name }}`: Month added name, as per locale
- `{{ added_month_name_short }}`: Month added abbreviated name, as per
locale
- `{{ added_day }}`: Day added only (number 01-31).
- `{{ owner_username }}`: Username of document owner, if any, or "none"
- `{{ original_name }}`: Document original filename, minus the extension, if any, or "none"
- `{{ doc_pk }}`: The paperless identifier (primary key) for the document.
- `{{ asn }}`: The archive serial number of the document, or "none".
- `{{ correspondent }}`: The name of the correspondent, or "none".
- `{{ document_type }}`: The name of the document type, or "none".
- `{{ tag_list }}`: A comma separated list of all tags assigned to the
document.
- `{{ title }}`: The title of the document.
- `{{ created }}`: The full date (ISO 8601 format, e.g. `2024-03-14`) the document was created.
- `{{ created_year }}`: Year created only, formatted as the year with
century.
- `{{ created_year_short }}`: Year created only, formatted as the year
without century, zero padded.
- `{{ created_month }}`: Month created only (number 01-12).
- `{{ created_month_name }}`: Month created name, as per locale
- `{{ created_month_name_short }}`: Month created abbreviated name, as per
locale
- `{{ created_day }}`: Day created only (number 01-31).
- `{{ added }}`: The full date (ISO format) the document was added to
paperless.
- `{{ added_year }}`: Year added only.
- `{{ added_year_short }}`: Year added only, formatted as the year without
century, zero padded.
- `{{ added_month }}`: Month added only (number 01-12).
- `{{ added_month_name }}`: Month added name, as per locale
- `{{ added_month_name_short }}`: Month added abbreviated name, as per
locale
- `{{ added_day }}`: Day added only (number 01-31).
- `{{ owner_username }}`: Username of document owner, if any, or "none"
- `{{ original_name }}`: Document original filename, minus the extension, if any, or "none"
- `{{ doc_pk }}`: The paperless identifier (primary key) for the document.
!!! warning
@@ -357,8 +353,6 @@ If paperless detects that two documents share the same filename,
paperless will automatically append `_01`, `_02`, etc to the filename.
This happens if all the placeholders in a filename evaluate to the same
value.
For versioned files, this counter is appended after the version suffix
(for example `statement_v2_01.pdf`).
If there are any errors in the placeholders included in `PAPERLESS_FILENAME_FORMAT`,
paperless will fall back to using the default naming scheme instead.
@@ -388,10 +382,10 @@ before empty placeholders are removed as well, empty directories are omitted.
When a single storage layout is not sufficient for your use case, storage paths allow for more complex
structure to set precisely where each document is stored in the file system.
- Each storage path is a [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) and
follows the rules described above
- Each document is assigned a storage path using the matching algorithms described above, but can be
overwritten at any time
- Each storage path is a [`PAPERLESS_FILENAME_FORMAT`](configuration.md#PAPERLESS_FILENAME_FORMAT) and
follows the rules described above
- Each document is assigned a storage path using the matching algorithms described above, but can be
overwritten at any time
For example, you could define the following two storage paths:
@@ -437,10 +431,8 @@ This allows for complex logic to be included in the format, including [logical s
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11) to manipulate the [variables](#filename-format-variables)
provided. The template is provided as a string, potentially multiline, and rendered into a single line.
In addition, a limited `document` object is available for advanced templates.
This object includes common metadata fields such as `id`, `pk`, `title`, `content`, `page_count`, `created`, `added`, `modified`, `mime_type`,
`checksum`, `archive_checksum`, `archive_serial_number`, `filename`, `archive_filename`, and `original_filename`.
Related values are available as nested objects with limited fields, for example document.correspondent.name, etc.
In addition, the entire Document instance is available to be utilized in a more advanced way, as well as some variables which only make sense to be accessed
with more complex logic.
#### Custom Jinja2 Filters
@@ -457,13 +449,13 @@ The `get_cf_value` filter retrieves a value from custom field data with optional
###### Parameters
- `custom_fields`: This _must_ be the provided custom field data
- `name` (str): Name of the custom field to retrieve
- `default` (str, optional): Default value to return if field is not found or has no value
- `custom_fields`: This _must_ be the provided custom field data
- `name` (str): Name of the custom field to retrieve
- `default` (str, optional): Default value to return if field is not found or has no value
###### Returns
- `str | None`: The field value, default value, or `None` if neither exists
- `str | None`: The field value, default value, or `None` if neither exists
###### Examples
@@ -487,12 +479,12 @@ The `datetime` filter formats a datetime string or datetime object using Python'
###### Parameters
- `value` (str | datetime): Date/time value to format (strings will be parsed automatically)
- `format` (str): Python strftime format string
- `value` (str | datetime): Date/time value to format (strings will be parsed automatically)
- `format` (str): Python strftime format string
###### Returns
- `str`: Formatted datetime string
- `str`: Formatted datetime string
###### Examples
@@ -525,13 +517,13 @@ An ISO string can also be provided to control the output format.
###### Parameters
- `value` (date | datetime | str): Date, datetime object or ISO string to format (datetime should be timezone-aware)
- `format` (str): Format type - either a Babel preset ('short', 'medium', 'long', 'full') or custom pattern
- `locale` (str): Locale code for localization (e.g., 'en_US', 'fr_FR', 'de_DE')
- `value` (date | datetime | str): Date, datetime object or ISO string to format (datetime should be timezone-aware)
- `format` (str): Format type - either a Babel preset ('short', 'medium', 'long', 'full') or custom pattern
- `locale` (str): Locale code for localization (e.g., 'en_US', 'fr_FR', 'de_DE')
###### Returns
- `str`: Localized, formatted date string
- `str`: Localized, formatted date string
###### Examples
@@ -565,15 +557,15 @@ See the [supported format codes](https://unicode.org/reports/tr35/tr35-dates.htm
### Format Presets
- **short**: Abbreviated format (e.g., "1/15/24")
- **medium**: Medium-length format (e.g., "Jan 15, 2024")
- **long**: Long format with full month name (e.g., "January 15, 2024")
- **full**: Full format including day of week (e.g., "Monday, January 15, 2024")
- **short**: Abbreviated format (e.g., "1/15/24")
- **medium**: Medium-length format (e.g., "Jan 15, 2024")
- **long**: Long format with full month name (e.g., "January 15, 2024")
- **full**: Full format including day of week (e.g., "Monday, January 15, 2024")
#### Additional Variables
- `{{ tag_name_list }}`: A list of tag names applied to the document, ordered by the tag name. Note this is a list, not a single string
- `{{ custom_fields }}`: A mapping of custom field names to their type and value. A user can access the mapping by field name or check if a field is applied by checking its existence in the variable.
- `{{ tag_name_list }}`: A list of tag names applied to the document, ordered by the tag name. Note this is a list, not a single string
- `{{ custom_fields }}`: A mapping of custom field names to their type and value. A user can access the mapping by field name or check if a field is applied by checking its existence in the variable.
!!! tip
@@ -675,15 +667,15 @@ installation, you can use volumes to accomplish this:
```yaml
services:
# ...
webserver:
environment:
- PAPERLESS_ENABLE_FLOWER
ports:
- 5555:5555 # (2)!
# ...
volumes:
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro # (1)!
webserver:
environment:
- PAPERLESS_ENABLE_FLOWER
ports:
- 5555:5555 # (2)!
# ...
volumes:
- /path/to/my/flowerconfig.py:/usr/src/paperless/src/paperless/flowerconfig.py:ro # (1)!
```
1. Note the `:ro` tag means the file will be mounted as read only.
@@ -714,11 +706,11 @@ For example, using Docker Compose:
```yaml
services:
# ...
webserver:
# ...
volumes:
- /path/to/my/scripts:/custom-cont-init.d:ro # (1)!
webserver:
# ...
volumes:
- /path/to/my/scripts:/custom-cont-init.d:ro # (1)!
```
1. Note the `:ro` tag means the folder will be mounted as read only. This is for extra security against changes
@@ -771,17 +763,18 @@ Paperless is able to utilize barcodes for automatically performing some tasks.
At this time, the library utilized for detection of barcodes supports the following types:
- AN-13/UPC-A
- UPC-E
- EAN-8
- Code 128
- Code 93
- Code 39
- Codabar
- Interleaved 2 of 5
- QR Code
- SQ Code
- AN-13/UPC-A
- UPC-E
- EAN-8
- Code 128
- Code 93
- Code 39
- Codabar
- Interleaved 2 of 5
- QR Code
- SQ Code
You may check for updates on the [zbar library homepage](https://github.com/mchehab/zbar).
For usage in Paperless, the type of barcode does not matter, only the contents of it.
For how to enable barcode usage, see [the configuration](configuration.md#barcodes).
@@ -790,17 +783,9 @@ below.
### Document Splitting {#document-splitting}
If document splitting is enabled, Paperless splits _after_ a separator barcode by default.
This means:
- any page containing the configured separator barcode starts a new document, starting with the **next** page
- pages containing the separator barcode are discarded
This is intended for dedicated separator sheets such as PATCH-T pages.
If [`PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES`](configuration.md#PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES)
is enabled, the page containing the separator barcode is retained instead. In this mode,
each page containing the separator barcode becomes the **first** page of a new document.
When enabled, Paperless will look for a barcode with the configured value and create a new document
starting from the next page. The page with the barcode on it will _not_ be retained. It
is expected to be a page existing only for triggering the split.
### Archive Serial Number Assignment
@@ -809,9 +794,8 @@ archive serial number, allowing quick reference back to the original, paper docu
If document splitting via barcode is also enabled, documents will be split when an ASN
barcode is located. However, differing from the splitting, the page with the
barcode _will_ be retained. Each detected ASN barcode starts a new document _starting with
that page_. This allows placing ASN barcodes on content pages that should remain part of
the document.
barcode _will_ be retained. This allows application of a barcode to any page, including
one which holds data to keep in the document.
### Tag Assignment
@@ -831,10 +815,10 @@ to `true`.
When enabled, documents will be split at pages containing tag barcodes, similar to how
ASN barcodes work. Key features:
- The page with the tag barcode is **retained** in the resulting document
- **Each split document extracts its own tags** - only tags on pages within that document are assigned
- Multiple tag barcodes can trigger multiple splits in the same document
- Works seamlessly with ASN barcodes - each split document gets its own ASN and tags
- The page with the tag barcode is **retained** in the resulting document
- **Each split document extracts its own tags** - only tags on pages within that document are assigned
- Multiple tag barcodes can trigger multiple splits in the same document
- Works seamlessly with ASN barcodes - each split document gets its own ASN and tags
This is useful for batch scanning where you place tag barcode pages between different
documents to both separate and categorize them in a single operation.
@@ -996,9 +980,9 @@ If using docker, you'll need to add the following volume mounts to your `docker-
```yaml
webserver:
volumes:
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
- <path to gpg-agent socket>:/usr/src/paperless/.gnupg/S.gpg-agent
volumes:
- /home/user/.gnupg/pubring.gpg:/usr/src/paperless/.gnupg/pubring.gpg
- <path to gpg-agent socket>:/usr/src/paperless/.gnupg/S.gpg-agent
```
For a 'bare-metal' installation no further configuration is necessary. If you
@@ -1006,9 +990,9 @@ want to use a separate `GNUPG_HOME`, you can do so by configuring the [PAPERLESS
### Troubleshooting
- Make sure, that `gpg-agent` is running on your host machine
- Make sure, that encryption and decryption works from inside the container using the `gpg` commands from above.
- Check that all files in `/usr/src/paperless/.gnupg` have correct permissions
- Make sure, that `gpg-agent` is running on your host machine
- Make sure, that encryption and decryption works from inside the container using the `gpg` commands from above.
- Check that all files in `/usr/src/paperless/.gnupg` have correct permissions
```shell
paperless@9da1865df327:~/.gnupg$ ls -al

View File

@@ -66,10 +66,10 @@ Full text searching is available on the `/api/documents/` endpoint. Two
specific query parameters cause the API to return full text search
results:
- `/api/documents/?query=your%20search%20query`: Search for a document
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage.md#basic-usage_searching).
- `/api/documents/?more_like_id=1234`: Search for documents similar to
the document with id 1234.
- `/api/documents/?query=your%20search%20query`: Search for a document
using a full text query. For details on the syntax, see [Basic Usage - Searching](usage.md#basic-usage_searching).
- `/api/documents/?more_like_id=1234`: Search for documents similar to
the document with id 1234.
Pagination works exactly the same as it does for normal requests on this
endpoint.
@@ -106,12 +106,12 @@ attribute with various information about the search results:
}
```
- `score` is an indication how well this document matches the query
relative to the other search results.
- `highlights` is an excerpt from the document content and highlights
the search terms with `<span>` tags as shown above.
- `rank` is the index of the search results. The first result will
have rank 0.
- `score` is an indication how well this document matches the query
relative to the other search results.
- `highlights` is an excerpt from the document content and highlights
the search terms with `<span>` tags as shown above.
- `rank` is the index of the search results. The first result will
have rank 0.
### Filtering by custom fields
@@ -122,33 +122,33 @@ use cases:
1. Documents with a custom field "due" (date) between Aug 1, 2024 and
Sept 1, 2024 (inclusive):
`?custom_field_query=["due", "range", ["2024-08-01", "2024-09-01"]]`
`?custom_field_query=["due", "range", ["2024-08-01", "2024-09-01"]]`
2. Documents with a custom field "customer" (text) that equals "bob"
(case sensitive):
`?custom_field_query=["customer", "exact", "bob"]`
`?custom_field_query=["customer", "exact", "bob"]`
3. Documents with a custom field "answered" (boolean) set to `true`:
`?custom_field_query=["answered", "exact", true]`
`?custom_field_query=["answered", "exact", true]`
4. Documents with a custom field "favorite animal" (select) set to either
"cat" or "dog":
`?custom_field_query=["favorite animal", "in", ["cat", "dog"]]`
`?custom_field_query=["favorite animal", "in", ["cat", "dog"]]`
5. Documents with a custom field "address" (text) that is empty:
`?custom_field_query=["OR", [["address", "isnull", true], ["address", "exact", ""]]]`
`?custom_field_query=["OR", [["address", "isnull", true], ["address", "exact", ""]]]`
6. Documents that don't have a field called "foo":
`?custom_field_query=["foo", "exists", false]`
`?custom_field_query=["foo", "exists", false]`
7. Documents that have document links "references" to both document 3 and 7:
`?custom_field_query=["references", "contains", [3, 7]]`
`?custom_field_query=["references", "contains", [3, 7]]`
All field types support basic operations including `exact`, `in`, `isnull`,
and `exists`. String, URL, and monetary fields support case-insensitive
@@ -164,8 +164,8 @@ Get auto completions for a partial search term.
Query parameters:
- `term`: The incomplete term.
- `limit`: Amount of results. Defaults to 10.
- `term`: The incomplete term.
- `limit`: Amount of results. Defaults to 10.
Results returned by the endpoint are ordered by importance of the term
in the document index. The first result is the term that has the highest
@@ -189,19 +189,19 @@ from there.
The endpoint supports the following optional form fields:
- `title`: Specify a title that the consumer should use for the
document.
- `created`: Specify a DateTime where the document was created (e.g.
"2016-04-19" or "2016-04-19 06:15:00+02:00").
- `correspondent`: Specify the ID of a correspondent that the consumer
should use for the document.
- `document_type`: Similar to correspondent.
- `storage_path`: Similar to correspondent.
- `tags`: Similar to correspondent. Specify this multiple times to
have multiple tags added to the document.
- `archive_serial_number`: An optional archive serial number to set.
- `custom_fields`: Either an array of custom field ids to assign (with an empty
value) to the document or an object mapping field id -> value.
- `title`: Specify a title that the consumer should use for the
document.
- `created`: Specify a DateTime where the document was created (e.g.
"2016-04-19" or "2016-04-19 06:15:00+02:00").
- `correspondent`: Specify the ID of a correspondent that the consumer
should use for the document.
- `document_type`: Similar to correspondent.
- `storage_path`: Similar to correspondent.
- `tags`: Similar to correspondent. Specify this multiple times to
have multiple tags added to the document.
- `archive_serial_number`: An optional archive serial number to set.
- `custom_fields`: Either an array of custom field ids to assign (with an empty
value) to the document or an object mapping field id -> value.
The endpoint will immediately return HTTP 200 if the document consumption
process was started successfully, with the UUID of the consumption task
@@ -215,16 +215,16 @@ consumption including the ID of a created document if consumption succeeded.
Document versions are file-level versions linked to one root document.
- Root document metadata (title, tags, correspondent, document type, storage path, custom fields, permissions) remains shared.
- Version-specific file data (file, mime type, checksums, archive info, extracted text content) belongs to the selected/latest version.
- Root document metadata (title, tags, correspondent, document type, storage path, custom fields, permissions) remains shared.
- Version-specific file data (file, mime type, checksums, archive info, extracted text content) belongs to the selected/latest version.
Version-aware endpoints:
- `GET /api/documents/{id}/`: returns root document data; `content` resolves to latest version content by default. Use `?version={version_id}` to resolve content for a specific version.
- `PATCH /api/documents/{id}/`: content updates target the selected version (`?version={version_id}`) or latest version by default; non-content metadata updates target the root document.
- `GET /api/documents/{id}/download/`, `GET /api/documents/{id}/preview/`, `GET /api/documents/{id}/thumb/`, `GET /api/documents/{id}/metadata/`: accept `?version={version_id}`.
- `POST /api/documents/{id}/update_version/`: uploads a new version using multipart form field `document` and optional `version_label`.
- `DELETE /api/documents/{root_id}/versions/{version_id}/`: deletes a non-root version.
- `GET /api/documents/{id}/`: returns root document data; `content` resolves to latest version content by default. Use `?version={version_id}` to resolve content for a specific version.
- `PATCH /api/documents/{id}/`: content updates target the selected version (`?version={version_id}`) or latest version by default; non-content metadata updates target the root document.
- `GET /api/documents/{id}/download/`, `GET /api/documents/{id}/preview/`, `GET /api/documents/{id}/thumb/`, `GET /api/documents/{id}/metadata/`: accept `?version={version_id}`.
- `POST /api/documents/{id}/update_version/`: uploads a new version using multipart form field `document` and optional `version_label`.
- `DELETE /api/documents/{root_id}/versions/{version_id}/`: deletes a non-root version.
## Permissions
@@ -282,38 +282,74 @@ a json payload of the format:
The following methods are supported:
- `set_correspondent`
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
- `set_document_type`
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
- `set_storage_path`
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
- `add_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `remove_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `modify_tags`
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and `{ "remove_tags": [LIST_OF_TAG_IDS] }`
- `delete`
- No `parameters` required
- `reprocess`
- No `parameters` required
- `set_permissions`
- Requires `parameters`:
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
- `"owner": OWNER_ID or null`
- `"merge": true or false` (defaults to false)
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
removing them) or be merged with existing permissions.
- `modify_custom_fields`
- Requires `parameters`:
- `"add_custom_fields": { CUSTOM_FIELD_ID: VALUE }`: JSON object consisting of custom field id:value pairs to add to the document, can also be a list of custom field IDs
to add with empty values.
- `"remove_custom_fields": [CUSTOM_FIELD_ID]`: custom field ids to remove from the document.
#### Document-editing operations
Beginning with version 10+, the API supports individual endpoints for document-editing operations (`merge`, `rotate`, `edit_pdf`, etc), thus their documentation can be found in the API spec / viewer. Legacy document-editing methods via `/api/documents/bulk_edit/` are still supported for compatibility, are deprecated and clients should migrate to the individual endpoints before they are removed in a future version.
- `set_correspondent`
- Requires `parameters`: `{ "correspondent": CORRESPONDENT_ID }`
- `set_document_type`
- Requires `parameters`: `{ "document_type": DOCUMENT_TYPE_ID }`
- `set_storage_path`
- Requires `parameters`: `{ "storage_path": STORAGE_PATH_ID }`
- `add_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `remove_tag`
- Requires `parameters`: `{ "tag": TAG_ID }`
- `modify_tags`
- Requires `parameters`: `{ "add_tags": [LIST_OF_TAG_IDS] }` and `{ "remove_tags": [LIST_OF_TAG_IDS] }`
- `delete`
- No `parameters` required
- `reprocess`
- No `parameters` required
- `set_permissions`
- Requires `parameters`:
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
- `"owner": OWNER_ID or null`
- `"merge": true or false` (defaults to false)
- The `merge` flag determines if the supplied permissions will overwrite all existing permissions (including
removing them) or be merged with existing permissions.
- `edit_pdf`
- Requires `parameters`:
- `"doc_ids": [DOCUMENT_ID]` A list of a single document ID to edit.
- `"operations": [OPERATION, ...]` A list of operations to perform on the documents. Each operation is a dictionary
with the following keys:
- `"page": PAGE_NUMBER` The page number to edit (1-based).
- `"rotate": DEGREES` Optional rotation in degrees (90, 180, 270).
- `"doc": OUTPUT_DOCUMENT_INDEX` Optional index of the output document for split operations.
- Optional `parameters`:
- `"delete_original": true` to delete the original documents after editing.
- `"update_document": true` to update the existing document with the edited PDF.
- `"include_metadata": true` to copy metadata from the original document to the edited document.
- `remove_password`
- Requires `parameters`:
- `"password": "PASSWORD_STRING"` The password to remove from the PDF documents.
- Optional `parameters`:
- `"update_document": true` to replace the existing document with the password-less PDF.
- `"delete_original": true` to delete the original document after editing.
- `"include_metadata": true` to copy metadata from the original document to the new password-less document.
- `merge`
- No additional `parameters` required.
- The ordering of the merged document is determined by the list of IDs.
- Optional `parameters`:
- `"metadata_document_id": DOC_ID` apply metadata (tags, correspondent, etc.) from this document to the merged document.
- `"delete_originals": true` to delete the original documents. This requires the calling user being the owner of
all documents that are merged.
- `split`
- Requires `parameters`:
- `"pages": [..]` The list should be a list of pages and/or a ranges, separated by commas e.g. `"[1,2-3,4,5-7]"`
- Optional `parameters`:
- `"delete_originals": true` to delete the original document after consumption. This requires the calling user being the owner of
the document.
- The split operation only accepts a single document.
- `rotate`
- Requires `parameters`:
- `"degrees": DEGREES`. Must be an integer i.e. 90, 180, 270
- `delete_pages`
- Requires `parameters`:
- `"pages": [..]` The list should be a list of integers e.g. `"[2,3,4]"`
- The delete_pages operation only accepts a single document.
- `modify_custom_fields`
- Requires `parameters`:
- `"add_custom_fields": { CUSTOM_FIELD_ID: VALUE }`: JSON object consisting of custom field id:value pairs to add to the document, can also be a list of custom field IDs
to add with empty values.
- `"remove_custom_fields": [CUSTOM_FIELD_ID]`: custom field ids to remove from the document.
### Objects
@@ -333,38 +369,41 @@ operations, using the endpoint: `/api/bulk_edit_objects/`, which requires a json
## API Versioning
The REST API is versioned.
The REST API is versioned since Paperless-ngx 1.3.0.
- Versioning ensures that changes to the API don't break older
clients.
- Clients specify the specific version of the API they wish to use
with every request and Paperless will handle the request using the
specified API version.
- Even if the underlying data model changes, supported older API
versions continue to serve compatible data.
- If no version is specified, Paperless serves the configured default
API version (currently `10`).
- Supported API versions are currently `9` and `10`.
- Versioning ensures that changes to the API don't break older
clients.
- Clients specify the specific version of the API they wish to use
with every request and Paperless will handle the request using the
specified API version.
- Even if the underlying data model changes, older API versions will
always serve compatible data.
- If no version is specified, Paperless will serve version 1 to ensure
compatibility with older clients that do not request a specific API
version.
API versions are specified by submitting an additional HTTP `Accept`
header with every request:
```
Accept: application/json; version=10
Accept: application/json; version=6
```
If an invalid version is specified, Paperless responds with
`406 Not Acceptable` and an error message in the body.
If an invalid version is specified, Paperless 1.3.0 will respond with
"406 Not Acceptable" and an error message in the body. Earlier
versions of Paperless will serve API version 1 regardless of whether a
version is specified via the `Accept` header.
If a client wishes to verify whether it is compatible with any given
server, the following procedure should be performed:
1. Perform an _authenticated_ request against any API endpoint. The
server will add two custom headers to the response:
1. Perform an _authenticated_ request against any API endpoint. If the
server is on version 1.3.0 or newer, the server will add two custom
headers to the response:
```
X-Api-Version: 10
X-Version: <server-version>
X-Api-Version: 2
X-Version: 1.3.0
```
2. Determine whether the client is compatible with this server based on
@@ -384,56 +423,46 @@ Initial API version.
#### Version 2
- Added field `Tag.color`. This read/write string field contains a hex
color such as `#a6cee3`.
- Added read-only field `Tag.text_color`. This field contains the text
color to use for a specific tag, which is either black or white
depending on the brightness of `Tag.color`.
- Removed field `Tag.colour`.
- Added field `Tag.color`. This read/write string field contains a hex
color such as `#a6cee3`.
- Added read-only field `Tag.text_color`. This field contains the text
color to use for a specific tag, which is either black or white
depending on the brightness of `Tag.color`.
- Removed field `Tag.colour`.
#### Version 3
- Permissions endpoints have been added.
- The format of the `/api/ui_settings/` has changed.
- Permissions endpoints have been added.
- The format of the `/api/ui_settings/` has changed.
#### Version 4
- Consumption templates were refactored to workflows and API endpoints
changed as such.
- Consumption templates were refactored to workflows and API endpoints
changed as such.
#### Version 5
- Added bulk deletion methods for documents and objects.
- Added bulk deletion methods for documents and objects.
#### Version 6
- Moved acknowledge tasks endpoint to be under `/api/tasks/acknowledge/`.
- Moved acknowledge tasks endpoint to be under `/api/tasks/acknowledge/`.
#### Version 7
- The format of select type custom fields has changed to return the options
as an array of objects with `id` and `label` fields as opposed to a simple
list of strings. When creating or updating a custom field value of a
document for a select type custom field, the value should be the `id` of
the option whereas previously was the index of the option.
- The format of select type custom fields has changed to return the options
as an array of objects with `id` and `label` fields as opposed to a simple
list of strings. When creating or updating a custom field value of a
document for a select type custom field, the value should be the `id` of
the option whereas previously was the index of the option.
#### Version 8
- The user field of document notes now returns a simplified user object
rather than just the user ID.
- The user field of document notes now returns a simplified user object
rather than just the user ID.
#### Version 9
- The document `created` field is now a date, not a datetime. The
`created_date` field is considered deprecated and will be removed in a
future version.
#### Version 10
- The `show_on_dashboard` and `show_in_sidebar` fields of saved views have been
removed. Relevant settings are now stored in the UISettings model. Compatibility is maintained
for versions < 10 until support for API v9 is dropped.
- Document-editing operations such as `merge`, `rotate`, and `edit_pdf` have been
moved from the bulk edit endpoint to their own individual endpoints. Using these methods via
the bulk edit endpoint is still supported for compatibility with versions < 10 until support
for API v9 is dropped.
- The document `created` field is now a date, not a datetime. The
`created_date` field is considered deprecated and will be removed in a
future version.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -8,17 +8,17 @@ common [OCR](#ocr) related settings and some frontend settings. If set, these wi
preference over the settings via environment variables. If not set, the environment setting
or applicable default will be utilized instead.
- If you run paperless on docker, `paperless.conf` is not used.
Rather, configure paperless by copying necessary options to
`docker-compose.env`.
- If you run paperless on docker, `paperless.conf` is not used.
Rather, configure paperless by copying necessary options to
`docker-compose.env`.
- If you are running paperless on anything else, paperless will search
for the configuration file in these locations and use the first one
it finds:
- The environment variable `PAPERLESS_CONFIGURATION_PATH`
- `/path/to/paperless/paperless.conf`
- `/etc/paperless.conf`
- `/usr/local/etc/paperless.conf`
- If you are running paperless on anything else, paperless will search
for the configuration file in these locations and use the first one
it finds:
- The environment variable `PAPERLESS_CONFIGURATION_PATH`
- `/path/to/paperless/paperless.conf`
- `/etc/paperless.conf`
- `/usr/local/etc/paperless.conf`
## Required services
@@ -51,172 +51,137 @@ matcher.
### Database
By default, Paperless uses **SQLite** with a database stored at `data/db.sqlite3`.
For multi-user or higher-throughput deployments, **PostgreSQL** (recommended) or
**MariaDB** can be used instead by setting [`PAPERLESS_DBENGINE`](#PAPERLESS_DBENGINE)
and the relevant connection variables.
#### [`PAPERLESS_DBENGINE=<engine>`](#PAPERLESS_DBENGINE) {#PAPERLESS_DBENGINE}
: Specifies the database engine to use. Accepted values are `sqlite`, `postgresql`,
and `mariadb`.
Defaults to `sqlite` if not set.
PostgreSQL and MariaDB both require [`PAPERLESS_DBHOST`](#PAPERLESS_DBHOST) to be
set. SQLite does not use any other connection variables; the database file is always
located at `<PAPERLESS_DATA_DIR>/db.sqlite3`.
!!! warning
Using MariaDB comes with some caveats.
See [MySQL Caveats](advanced_usage.md#mysql-caveats).
To switch to **PostgreSQL** or **MariaDB**, set [`PAPERLESS_DBHOST`](#PAPERLESS_DBHOST) and optionally configure other
database-related environment variables.
#### [`PAPERLESS_DBHOST=<hostname>`](#PAPERLESS_DBHOST) {#PAPERLESS_DBHOST}
: Hostname of the PostgreSQL or MariaDB database server. Required when
`PAPERLESS_DBENGINE` is `postgresql` or `mariadb`.
: If unset, Paperless uses **SQLite** by default.
Set `PAPERLESS_DBHOST` to switch to PostgreSQL or MariaDB instead.
#### [`PAPERLESS_DBENGINE=<engine_name>`](#PAPERLESS_DBENGINE) {#PAPERLESS_DBENGINE}
: Optional. Specifies the database engine to use when connecting to a remote database.
Available options are `postgresql` and `mariadb`.
Defaults to `postgresql` if `PAPERLESS_DBHOST` is set.
!!! warning
Using MariaDB comes with some caveats. See [MySQL Caveats](advanced_usage.md#mysql-caveats).
#### [`PAPERLESS_DBPORT=<port>`](#PAPERLESS_DBPORT) {#PAPERLESS_DBPORT}
: Port to use when connecting to PostgreSQL or MariaDB.
Defaults to `5432` for PostgreSQL and `3306` for MariaDB.
Default is `5432` for PostgreSQL and `3306` for MariaDB.
#### [`PAPERLESS_DBNAME=<name>`](#PAPERLESS_DBNAME) {#PAPERLESS_DBNAME}
: Name of the PostgreSQL or MariaDB database to connect to.
: Name of the database to connect to when using PostgreSQL or MariaDB.
Defaults to `paperless`.
Defaults to "paperless".
#### [`PAPERLESS_DBUSER=<user>`](#PAPERLESS_DBUSER) {#PAPERLESS_DBUSER}
#### [`PAPERLESS_DBUSER=<name>`](#PAPERLESS_DBUSER) {#PAPERLESS_DBUSER}
: Username for authenticating with the PostgreSQL or MariaDB database.
Defaults to `paperless`.
Defaults to "paperless".
#### [`PAPERLESS_DBPASS=<password>`](#PAPERLESS_DBPASS) {#PAPERLESS_DBPASS}
: Password for the PostgreSQL or MariaDB database user.
Defaults to `paperless`.
Defaults to "paperless".
#### [`PAPERLESS_DB_OPTIONS=<options>`](#PAPERLESS_DB_OPTIONS) {#PAPERLESS_DB_OPTIONS}
#### [`PAPERLESS_DBSSLMODE=<mode>`](#PAPERLESS_DBSSLMODE) {#PAPERLESS_DBSSLMODE}
: Advanced database connection options as a semicolon-delimited key-value string.
Keys and values are separated by `=`. Dot-notation produces nested option
dictionaries; for example, `pool.max_size=20` sets
`OPTIONS["pool"]["max_size"] = 20`.
: SSL mode to use when connecting to PostgreSQL or MariaDB.
Options specified here are merged over the engine defaults. Unrecognised keys
are passed through to the underlying database driver without validation, so a
typo will be silently ignored rather than producing an error.
See [the official documentation about
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
Refer to your database driver's documentation for the full set of accepted keys:
See [the official documentation about
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-mode).
- PostgreSQL: [libpq connection parameters](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS)
- MariaDB: [MariaDB Connector/Python](https://mariadb.com/kb/en/mariadb-connector-python/)
- SQLite: [SQLite PRAGMA statements](https://www.sqlite.org/pragma.html)
*Note*: SSL mode values differ between PostgreSQL and MariaDB.
!!! note "PostgreSQL connection pooling"
Default is `prefer` for PostgreSQL and `PREFERRED` for MariaDB.
Pool size is controlled via `pool.min_size` and `pool.max_size`. When
configuring pooling, ensure your PostgreSQL `max_connections` is large enough
to handle all pool connections across all workers:
`(web_workers + celery_workers) * pool.max_size + safety_margin`.
#### [`PAPERLESS_DBSSLROOTCERT=<ca-path>`](#PAPERLESS_DBSSLROOTCERT) {#PAPERLESS_DBSSLROOTCERT}
**Examples:**
: Path to the SSL root certificate used to verify the database server.
```bash title="PostgreSQL: require SSL, set a custom CA certificate, and limit the pool size"
PAPERLESS_DB_OPTIONS="sslmode=require;sslrootcert=/certs/ca.pem;pool.max_size=5"
```
See [the official documentation about
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
Changes the location of `root.crt`.
```bash title="MariaDB: require SSL with a custom CA certificate"
PAPERLESS_DB_OPTIONS="ssl_mode=REQUIRED;ssl.ca=/certs/ca.pem"
```
See [the official documentation about
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-ca).
```bash title="SQLite: set a busy timeout of 30 seconds"
# PostgreSQL: set a connection timeout
PAPERLESS_DB_OPTIONS="connect_timeout=10"
```
Defaults to unset, using the standard location in the home directory.
#### ~~[`PAPERLESS_DBSSLMODE`](#PAPERLESS_DBSSLMODE)~~ {#PAPERLESS_DBSSLMODE}
#### [`PAPERLESS_DBSSLCERT=<client-cert-path>`](#PAPERLESS_DBSSLCERT) {#PAPERLESS_DBSSLCERT}
!!! failure "Removed in v3"
: Path to the client SSL certificate used when connecting securely.
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
See [the official documentation about
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
```bash title="PostgreSQL"
PAPERLESS_DB_OPTIONS="sslmode=require"
```
See [the official documentation about
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-cert).
```bash title="MariaDB"
PAPERLESS_DB_OPTIONS="ssl_mode=REQUIRED"
```
Changes the location of `postgresql.crt`.
#### ~~[`PAPERLESS_DBSSLROOTCERT`](#PAPERLESS_DBSSLROOTCERT)~~ {#PAPERLESS_DBSSLROOTCERT}
Defaults to unset, using the standard location in the home directory.
!!! failure "Removed in v3"
#### [`PAPERLESS_DBSSLKEY=<client-cert-key>`](#PAPERLESS_DBSSLKEY) {#PAPERLESS_DBSSLKEY}
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
: Path to the client SSL private key used when connecting securely.
```bash title="PostgreSQL"
PAPERLESS_DB_OPTIONS="sslrootcert=/path/to/ca.pem"
```
See [the official documentation about
sslmode for PostgreSQL](https://www.postgresql.org/docs/current/libpq-ssl.html).
```bash title="MariaDB"
PAPERLESS_DB_OPTIONS="ssl.ca=/path/to/ca.pem"
```
See [the official documentation about
sslmode for MySQL and MariaDB](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-key).
#### ~~[`PAPERLESS_DBSSLCERT`](#PAPERLESS_DBSSLCERT)~~ {#PAPERLESS_DBSSLCERT}
Changes the location of `postgresql.key`.
!!! failure "Removed in v3"
Defaults to unset, using the standard location in the home directory.
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
#### [`PAPERLESS_DB_TIMEOUT=<int>`](#PAPERLESS_DB_TIMEOUT) {#PAPERLESS_DB_TIMEOUT}
```bash title="PostgreSQL"
PAPERLESS_DB_OPTIONS="sslcert=/path/to/client.crt"
```
: Sets how long a database connection should wait before timing out.
```bash title="MariaDB"
PAPERLESS_DB_OPTIONS="ssl.cert=/path/to/client.crt"
```
For SQLite, this sets how long to wait if the database is locked.
For PostgreSQL or MariaDB, this sets the connection timeout.
#### ~~[`PAPERLESS_DBSSLKEY`](#PAPERLESS_DBSSLKEY)~~ {#PAPERLESS_DBSSLKEY}
Defaults to unset, which uses Djangos built-in defaults.
!!! failure "Removed in v3"
#### [`PAPERLESS_DB_POOLSIZE=<int>`](#PAPERLESS_DB_POOLSIZE) {#PAPERLESS_DB_POOLSIZE}
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
: Defines the maximum number of database connections to keep in the pool.
```bash title="PostgreSQL"
PAPERLESS_DB_OPTIONS="sslkey=/path/to/client.key"
```
Only applies to PostgreSQL. This setting is ignored for other database engines.
```bash title="MariaDB"
PAPERLESS_DB_OPTIONS="ssl.key=/path/to/client.key"
```
The value must be greater than or equal to 1 to be used.
Defaults to unset, which disables connection pooling.
#### ~~[`PAPERLESS_DB_TIMEOUT`](#PAPERLESS_DB_TIMEOUT)~~ {#PAPERLESS_DB_TIMEOUT}
!!! note
!!! failure "Removed in v3"
A pool of 8-10 connections per worker is typically sufficient.
If you encounter error messages such as `couldn't get a connection`
or database connection timeouts, you probably need to increase the pool size.
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
!!! warning
Make sure your PostgreSQL `max_connections` setting is large enough to handle the connection pools:
`(NB_PAPERLESS_WORKERS + NB_CELERY_WORKERS) × POOL_SIZE + SAFETY_MARGIN`. For example, with
4 Paperless workers and 2 Celery workers, and a pool size of 8:``(4 + 2) × 8 + 10 = 58`,
so `max_connections = 60` (or even more) is appropriate.
```bash title="SQLite"
PAPERLESS_DB_OPTIONS="timeout=30"
```
```bash title="PostgreSQL or MariaDB"
PAPERLESS_DB_OPTIONS="connect_timeout=30"
```
#### ~~[`PAPERLESS_DB_POOLSIZE`](#PAPERLESS_DB_POOLSIZE)~~ {#PAPERLESS_DB_POOLSIZE}
!!! failure "Removed in v3"
Use [`PAPERLESS_DB_OPTIONS`](#PAPERLESS_DB_OPTIONS) instead.
```bash
PAPERLESS_DB_OPTIONS="pool.max_size=10"
```
This assumes only Paperless-ngx connects to your PostgreSQL instance. If you have other applications,
you should increase `max_connections` accordingly.
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
@@ -1257,6 +1222,14 @@ using Python's `re.match()`, which anchors at the start of the filename.
The default ignores are `[.stfolder, .stversions, .localized, @eaDir, .Spotlight-V100, .Trashes, __MACOSX]` and cannot be overridden.
#### [`PAPERLESS_CONSUMER_BARCODE_SCANNER=<string>`](#PAPERLESS_CONSUMER_BARCODE_SCANNER) {#PAPERLESS_CONSUMER_BARCODE_SCANNER}
: Sets the barcode scanner used for barcode functionality.
Currently, "PYZBAR" (the default) or "ZXING" might be selected.
If you have problems that your Barcodes/QR-Codes are not detected
(especially with bad scan quality and/or small codes), try the other one.
#### [`PAPERLESS_PRE_CONSUME_SCRIPT=<filename>`](#PAPERLESS_PRE_CONSUME_SCRIPT) {#PAPERLESS_PRE_CONSUME_SCRIPT}
: After some initial validation, Paperless can trigger an arbitrary

View File

@@ -6,23 +6,23 @@ on Paperless-ngx.
Check out the source from GitHub. The repository is organized in the
following way:
- `main` always represents the latest release and will only see
changes when a new release is made.
- `dev` contains the code that will be in the next release.
- `feature-X` contains bigger changes that will be in some release, but
not necessarily the next one.
- `main` always represents the latest release and will only see
changes when a new release is made.
- `dev` contains the code that will be in the next release.
- `feature-X` contains bigger changes that will be in some release, but
not necessarily the next one.
When making functional changes to Paperless-ngx, _always_ make your changes
on the `dev` branch.
Apart from that, the folder structure is as follows:
- `docs/` - Documentation.
- `src-ui/` - Code of the front end.
- `src/` - Code of the back end.
- `scripts/` - Various scripts that help with different parts of
development.
- `docker/` - Files required to build the docker image.
- `docs/` - Documentation.
- `src-ui/` - Code of the front end.
- `src/` - Code of the back end.
- `scripts/` - Various scripts that help with different parts of
development.
- `docker/` - Files required to build the docker image.
## Contributing to Paperless-ngx
@@ -75,13 +75,13 @@ first-time setup.
4. Install the Python dependencies:
```bash
uv sync --group dev
$ uv sync --group dev
```
5. Install pre-commit hooks:
```bash
uv run prek install
$ uv run prek install
```
6. Apply migrations and create a superuser (also can be done via the web UI) for your development instance:
@@ -89,22 +89,23 @@ first-time setup.
```bash
# src/
uv run manage.py migrate
uv run manage.py createsuperuser
$ uv run manage.py migrate
$ uv run manage.py createsuperuser
```
7. You can now either ...
- install Redis or
- use the included `scripts/start_services.sh` to use Docker to fire
up a Redis instance (and some other services such as Tika,
Gotenberg and a database server) or
- install Redis or
- spin up a bare Redis container
- use the included `scripts/start_services.sh` to use Docker to fire
up a Redis instance (and some other services such as Tika,
Gotenberg and a database server) or
```bash
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
```
- spin up a bare Redis container
```
docker run -d -p 6379:6379 --restart unless-stopped redis:latest
```
8. Continue with either back-end or front-end development or both :-).
@@ -117,18 +118,18 @@ work well for development, but you can use whatever you want.
Configure the IDE to use the `src/`-folder as the base source folder.
Configure the following launch configurations in your IDE:
- `uv run manage.py runserver`
- `uv run manage.py document_consumer`
- `uv run celery --app paperless worker -l DEBUG` (or any other log level)
- `python3 manage.py runserver`
- `python3 manage.py document_consumer`
- `celery --app paperless worker -l DEBUG` (or any other log level)
To start them all:
```bash
# src/
uv run manage.py runserver & \
uv run manage.py document_consumer & \
uv run celery --app paperless worker -l DEBUG
$ python3 manage.py runserver & \
python3 manage.py document_consumer & \
celery --app paperless worker -l DEBUG
```
You might need the front end to test your back end code.
@@ -139,17 +140,17 @@ To build the front end once use this command:
```bash
# src-ui/
pnpm install
pnpm ng build --configuration production
$ pnpm install
$ ng build --configuration production
```
### Testing
- Run `pytest` in the `src/` directory to execute all tests. This also
generates a HTML coverage report. When running tests, `paperless.conf`
is loaded as well. However, the tests rely on the default
configuration. This is not ideal. But for now, make sure no settings
except for DEBUG are overridden when testing.
- Run `pytest` in the `src/` directory to execute all tests. This also
generates a HTML coverage report. When running tests, `paperless.conf`
is loaded as well. However, the tests rely on the default
configuration. This is not ideal. But for now, make sure no settings
except for DEBUG are overridden when testing.
!!! note
@@ -198,7 +199,7 @@ The front end is built using AngularJS. In order to get started, you need Node.j
4. You can launch a development server by running:
```bash
pnpm ng serve
ng serve
```
This will automatically update whenever you save. However, in-place
@@ -216,21 +217,21 @@ commit. See [above](#code-formatting-with-pre-commit-hooks) for installation ins
command such as
```bash
git ls-files -- '*.ts' | xargs uv run prek run prettier --files
$ git ls-files -- '*.ts' | xargs prek run prettier --files
```
Front end testing uses Jest and Playwright. Unit tests and e2e tests,
respectively, can be run non-interactively with:
```bash
pnpm ng test
pnpm playwright test
$ ng test
$ npx playwright test
```
Playwright also includes a UI which can be run with:
```bash
pnpm playwright test --ui
$ npx playwright test --ui
```
### Building the frontend
@@ -238,7 +239,7 @@ pnpm playwright test --ui
In order to build the front end and serve it as part of Django, execute:
```bash
pnpm ng build --configuration production
$ ng build --configuration production
```
This will build the front end and put it in a location from which the
@@ -253,14 +254,14 @@ these parts have to be translated separately.
### Front end localization
- The AngularJS front end does localization according to the [Angular
documentation](https://angular.io/guide/i18n).
- The source language of the project is "en_US".
- The source strings end up in the file `src-ui/messages.xlf`.
- The translated strings need to be placed in the
`src-ui/src/locale/` folder.
- In order to extract added or changed strings from the source files,
call `ng extract-i18n`.
- The AngularJS front end does localization according to the [Angular
documentation](https://angular.io/guide/i18n).
- The source language of the project is "en_US".
- The source strings end up in the file `src-ui/messages.xlf`.
- The translated strings need to be placed in the
`src-ui/src/locale/` folder.
- In order to extract added or changed strings from the source files,
call `ng extract-i18n`.
Adding new languages requires adding the translated files in the
`src-ui/src/locale/` folder and adjusting a couple files.
@@ -306,18 +307,18 @@ A majority of the strings that appear in the back end appear only when
the admin is used. However, some of these are still shown on the front
end (such as error messages).
- The django application does localization according to the [Django
documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
- The source language of the project is "en_US".
- Localization files end up in the folder `src/locale/`.
- In order to extract strings from the application, call
`uv run manage.py makemessages -l en_US`. This is important after
making changes to translatable strings.
- The message files need to be compiled for them to show up in the
application. Call `uv run manage.py compilemessages` to do this.
The generated files don't get committed into git, since these are
derived artifacts. The build pipeline takes care of executing this
command.
- The django application does localization according to the [Django
documentation](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
- The source language of the project is "en_US".
- Localization files end up in the folder `src/locale/`.
- In order to extract strings from the application, call
`python3 manage.py makemessages -l en_US`. This is important after
making changes to translatable strings.
- The message files need to be compiled for them to show up in the
application. Call `python3 manage.py compilemessages` to do this.
The generated files don't get committed into git, since these are
derived artifacts. The build pipeline takes care of executing this
command.
Adding new languages requires adding the translated files in the
`src/locale/`-folder and adjusting the file
@@ -357,7 +358,7 @@ If you want to build the documentation locally, this is how you do it:
$ uv run zensical serve
```
## Building the Docker image {#docker_build}
## Building the Docker image
The docker image is primarily built by the GitHub actions workflow, but
it can be faster when developing to build and tag an image locally.
@@ -380,10 +381,10 @@ base code.
Paperless-ngx uses parsers to add documents. A parser is
responsible for:
- Retrieving the content from the original
- Creating a thumbnail
- _optional:_ Retrieving a created date from the original
- _optional:_ Creating an archived document from the original
- Retrieving the content from the original
- Creating a thumbnail
- _optional:_ Retrieving a created date from the original
- _optional:_ Creating an archived document from the original
Custom parsers can be added to Paperless-ngx to support more file types. In
order to do that, you need to write the parser itself and announce its
@@ -441,17 +442,17 @@ def myparser_consumer_declaration(sender, **kwargs):
}
```
- `parser` is a reference to a class that extends `DocumentParser`.
- `weight` is used whenever two or more parsers are able to parse a
file: The parser with the higher weight wins. This can be used to
override the parsers provided by Paperless-ngx.
- `mime_types` is a dictionary. The keys are the mime types your
parser supports and the value is the default file extension that
Paperless-ngx should use when storing files and serving them for
download. We could guess that from the file extensions, but some
mime types have many extensions associated with them and the Python
methods responsible for guessing the extension do not always return
the same value.
- `parser` is a reference to a class that extends `DocumentParser`.
- `weight` is used whenever two or more parsers are able to parse a
file: The parser with the higher weight wins. This can be used to
override the parsers provided by Paperless-ngx.
- `mime_types` is a dictionary. The keys are the mime types your
parser supports and the value is the default file extension that
Paperless-ngx should use when storing files and serving them for
download. We could guess that from the file extensions, but some
mime types have many extensions associated with them and the Python
methods responsible for guessing the extension do not always return
the same value.
## Using Visual Studio Code devcontainer
@@ -470,8 +471,9 @@ To get started:
2. VS Code will prompt you with "Reopen in container". Do so and wait for the environment to start.
3. In case your host operating system is Windows:
- The Source Control view in Visual Studio Code might show: "The detected Git repository is potentially unsafe as the folder is owned by someone other than the current user." Use "Manage Unsafe Repositories" to fix this.
- Git might have detecteded modifications for all files, because Windows is using CRLF line endings. Run `git checkout .` in the containers terminal to fix this issue.
- The Source Control view in Visual Studio Code might show: "The detected Git repository is potentially unsafe as the folder is owned by someone other than the current user." Use "Manage Unsafe Repositories" to fix this.
- Git might have detecteded modifications for all files, because Windows is using CRLF line endings. Run `git checkout .` in the containers terminal to fix this issue.
4. Initialize the project by running the task **Project Setup: Run all Init Tasks**. This
will initialize the database tables and create a superuser. Then you can compile the front end
@@ -536,12 +538,12 @@ class MyDateParserPlugin(DateParserPluginBase):
Your parser instance is initialized with a `DateParserConfig` object accessible via `self.config`. This provides:
- `languages: list[str]` - List of language codes for date parsing
- `timezone_str: str` - Timezone string for date localization
- `ignore_dates: set[datetime.date]` - Dates that should be filtered out
- `reference_time: datetime.datetime` - Current time for filtering future dates
- `filename_date_order: str | None` - Date order preference for filenames (e.g., "DMY", "MDY")
- `content_date_order: str` - Date order preference for content
- `languages: list[str]` - List of language codes for date parsing
- `timezone_str: str` - Timezone string for date localization
- `ignore_dates: set[datetime.date]` - Dates that should be filtered out
- `reference_time: datetime.datetime` - Current time for filtering future dates
- `filename_date_order: str | None` - Date order preference for filenames (e.g., "DMY", "MDY")
- `content_date_order: str` - Date order preference for content
The base class provides two helper methods you can use:

View File

@@ -44,28 +44,28 @@ system. On Linux, chances are high that this location is
You can always drag those files out of that folder to use them
elsewhere. Here are a couple notes about that.
- Paperless-ngx never modifies your original documents. It keeps
checksums of all documents and uses a scheduled sanity checker to
check that they remain the same.
- By default, paperless uses the internal ID of each document as its
filename. This might not be very convenient for export. However, you
can adjust the way files are stored in paperless by
[configuring the filename format](advanced_usage.md#file-name-handling).
- [The exporter](administration.md#exporter) is
another easy way to get your files out of paperless with reasonable
file names.
- Paperless-ngx never modifies your original documents. It keeps
checksums of all documents and uses a scheduled sanity checker to
check that they remain the same.
- By default, paperless uses the internal ID of each document as its
filename. This might not be very convenient for export. However, you
can adjust the way files are stored in paperless by
[configuring the filename format](advanced_usage.md#file-name-handling).
- [The exporter](administration.md#exporter) is
another easy way to get your files out of paperless with reasonable
file names.
## _What file types does paperless-ngx support?_
**A:** Currently, the following files are supported:
- PDF documents, PNG images, JPEG images, TIFF images, GIF images and
WebP images are processed with OCR and converted into PDF documents.
- Plain text documents are supported as well and are added verbatim to
paperless.
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
Paperless also supports various Office documents (.docx, .doc, odt,
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
- PDF documents, PNG images, JPEG images, TIFF images, GIF images and
WebP images are processed with OCR and converted into PDF documents.
- Plain text documents are supported as well and are added verbatim to
paperless.
- With the optional Tika integration enabled (see [Tika configuration](https://docs.paperless-ngx.com/configuration#tika)),
Paperless also supports various Office documents (.docx, .doc, odt,
.ppt, .pptx, .odp, .xls, .xlsx, .ods).
Paperless-ngx determines the type of a file by inspecting its content
rather than its file extensions. However, files processed via the

View File

@@ -28,36 +28,36 @@ physical documents into a searchable online archive so you can keep, well, _less
## Features
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
- _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way, unless you explicitly choose to do so.
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
- _New!_ Supports remote OCR with Azure AI (opt-in).
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
- Uses machine-learning to automatically add tags, correspondents and document types to your documents.
- **New**: Paperless-ngx can now leverage AI (Large Language Models or LLMs) for document suggestions. This is an optional feature that can be enabled (and is disabled by default).
- Supports PDF documents, images, plain text files, Office documents (Word, Excel, PowerPoint, and LibreOffice equivalents)[^1] and more.
- Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents.
- **Beautiful, modern web application** that features:
- Customizable dashboard with statistics.
- Filtering by tags, correspondents, types, and more.
- Bulk editing of tags, correspondents, types and more.
- Drag-and-drop uploading of documents throughout the app.
- Customizable views can be saved and displayed on the dashboard and / or sidebar.
- Support for custom fields of various data types.
- Shareable public links with optional expiration.
- **Full text search** helps you find what you need:
- Auto completion suggests relevant words from your documents.
- Results are sorted by relevance to your search query.
- Highlighting shows you which parts of the document matched the query.
- Searching for similar documents ("More like this")
- **Email processing**[^1]: import documents from your email accounts:
- Configure multiple accounts and rules for each account.
- After processing, paperless can perform actions on the messages such as marking as read, deleting and more.
- A built-in robust **multi-user permissions** system that supports 'global' permissions as well as per document or object.
- A powerful workflow system that gives you even more control.
- **Optimized** for multi core systems: Paperless-ngx consumes multiple documents in parallel.
- The integrated sanity checker makes sure that your document archive is in good health.
- **Organize and index** your scanned documents with tags, correspondents, types, and more.
- _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way, unless you explicitly choose to do so.
- Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images.
- Utilizes the open-source Tesseract engine to recognize more than 100 languages.
- _New!_ Supports remote OCR with Azure AI (opt-in).
- Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals.
- Uses machine-learning to automatically add tags, correspondents and document types to your documents.
- **New**: Paperless-ngx can now leverage AI (Large Language Models or LLMs) for document suggestions. This is an optional feature that can be enabled (and is disabled by default).
- Supports PDF documents, images, plain text files, Office documents (Word, Excel, PowerPoint, and LibreOffice equivalents)[^1] and more.
- Paperless stores your documents plain on disk. Filenames and folders are managed by paperless and their format can be configured freely with different configurations assigned to different documents.
- **Beautiful, modern web application** that features:
- Customizable dashboard with statistics.
- Filtering by tags, correspondents, types, and more.
- Bulk editing of tags, correspondents, types and more.
- Drag-and-drop uploading of documents throughout the app.
- Customizable views can be saved and displayed on the dashboard and / or sidebar.
- Support for custom fields of various data types.
- Shareable public links with optional expiration.
- **Full text search** helps you find what you need:
- Auto completion suggests relevant words from your documents.
- Results are sorted by relevance to your search query.
- Highlighting shows you which parts of the document matched the query.
- Searching for similar documents ("More like this")
- **Email processing**[^1]: import documents from your email accounts:
- Configure multiple accounts and rules for each account.
- After processing, paperless can perform actions on the messages such as marking as read, deleting and more.
- A built-in robust **multi-user permissions** system that supports 'global' permissions as well as per document or object.
- A powerful workflow system that gives you even more control.
- **Optimized** for multi core systems: Paperless-ngx consumes multiple documents in parallel.
- The integrated sanity checker makes sure that your document archive is in good health.
[^1]: Office document and email consumption support is optional and provided by Apache Tika (see [configuration](https://docs.paperless-ngx.com/configuration/#tika))

View File

@@ -1,105 +0,0 @@
# v3 Migration Guide
## Consumer Settings Changes
The v3 consumer command uses a [different library](https://watchfiles.helpmanual.io/) to unify
the watching for new files in the consume directory. For the user, this removes several configuration options related to delays and retries
and replaces with a single unified setting. It also adjusts how the consumer ignore filtering happens, replaced `fnmatch` with `regex` and
separating the directory ignore from the file ignore.
### Summary
| Old Setting | New Setting | Notes |
| ------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `CONSUMER_POLLING` | [`CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL) | Renamed for clarity |
| `CONSUMER_INOTIFY_DELAY` | [`CONSUMER_STABILITY_DELAY`](configuration.md#PAPERLESS_CONSUMER_STABILITY_DELAY) | Unified for all modes |
| `CONSUMER_POLLING_DELAY` | _Removed_ | Use `CONSUMER_STABILITY_DELAY` |
| `CONSUMER_POLLING_RETRY_COUNT` | _Removed_ | Automatic with stability tracking |
| `CONSUMER_IGNORE_PATTERNS` | [`CONSUMER_IGNORE_PATTERNS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_PATTERNS) | **Now regex, not fnmatch**; user patterns are added to (not replacing) default ones |
| _New_ | [`CONSUMER_IGNORE_DIRS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_DIRS) | Additional directories to ignore; user entries are added to (not replacing) defaults |
## Encryption Support
Document and thumbnail encryption is no longer supported. This was previously deprecated in [paperless-ng 0.9.3](https://github.com/paperless-ngx/paperless-ngx/blob/dev/docs/changelog.md#paperless-ng-093)
Users must decrypt their document using the `decrypt_documents` command before upgrading.
## Barcode Scanner Changes
Support for [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) has been removed. The underlying libzbar library has
seen no updates in 16 years and is largely unmaintained, and the pyzbar Python wrapper last saw a release in March 2022. In
practice, pyzbar struggled with barcode detection reliability, particularly on skewed, low-contrast, or partially
obscured barcodes. [zxing-cpp](https://github.com/zxing-cpp/zxing-cpp) is actively maintained, significantly more
reliable at finding barcodes, and now ships pre-built wheels for both x86_64 and arm64, removing the need to build the library.
The `CONSUMER_BARCODE_SCANNER` setting has been removed. zxing-cpp is now the only backend.
### Summary
| Old Setting | New Setting | Notes |
| -------------------------- | ----------- | --------------------------------- |
| `CONSUMER_BARCODE_SCANNER` | _Removed_ | zxing-cpp is now the only backend |
### Action Required
- If you were already using `CONSUMER_BARCODE_SCANNER=ZXING`, simply remove the setting.
- If you had `CONSUMER_BARCODE_SCANNER=PYZBAR` or were using the default, no functional changes are needed beyond
removing the setting. zxing-cpp supports all the same barcode formats and you should see improved detection
reliability.
- The `libzbar0` / `libzbar-dev` system packages are no longer required and can be removed from any custom Docker
images or host installations.
## Database Engine
`PAPERLESS_DBENGINE` is now required to use PostgreSQL or MariaDB. Previously, the
engine was inferred from the presence of `PAPERLESS_DBHOST`, with `PAPERLESS_DBENGINE`
only needed to select MariaDB over PostgreSQL.
SQLite users require no changes, though they may explicitly set their engine if desired.
#### Action Required
PostgreSQL and MariaDB users must add `PAPERLESS_DBENGINE` to their environment:
```yaml
# v2 (PostgreSQL inferred from PAPERLESS_DBHOST)
PAPERLESS_DBHOST: postgres
# v3 (engine must be explicit)
PAPERLESS_DBENGINE: postgresql
PAPERLESS_DBHOST: postgres
```
See [`PAPERLESS_DBENGINE`](configuration.md#PAPERLESS_DBENGINE) for accepted values.
## Database Advanced Options
The individual SSL, timeout, and pooling variables have been removed in favor of a
single [`PAPERLESS_DB_OPTIONS`](configuration.md#PAPERLESS_DB_OPTIONS) string. This
consolidates a growing set of engine-specific variables into one place, and allows
any option supported by the underlying database driver to be set without requiring a
dedicated environment variable for each.
The removed variables and their replacements are:
| Removed Variable | Replacement in `PAPERLESS_DB_OPTIONS` |
| ------------------------- | ---------------------------------------------------------------------------- |
| `PAPERLESS_DBSSLMODE` | `sslmode=<value>` (PostgreSQL) or `ssl_mode=<value>` (MariaDB) |
| `PAPERLESS_DBSSLROOTCERT` | `sslrootcert=<path>` (PostgreSQL) or `ssl.ca=<path>` (MariaDB) |
| `PAPERLESS_DBSSLCERT` | `sslcert=<path>` (PostgreSQL) or `ssl.cert=<path>` (MariaDB) |
| `PAPERLESS_DBSSLKEY` | `sslkey=<path>` (PostgreSQL) or `ssl.key=<path>` (MariaDB) |
| `PAPERLESS_DB_POOLSIZE` | `pool.max_size=<value>` (PostgreSQL only) |
| `PAPERLESS_DB_TIMEOUT` | `timeout=<value>` (SQLite) or `connect_timeout=<value>` (PostgreSQL/MariaDB) |
The deprecated variables will continue to function for now but will be removed in a
future release. A deprecation warning is logged at startup for each deprecated variable
that is still set.
#### Action Required
Users with any of the deprecated variables set should migrate to `PAPERLESS_DB_OPTIONS`.
Multiple options are combined in a single value:
```bash
PAPERLESS_DB_OPTIONS="sslmode=require;sslrootcert=/certs/ca.pem;pool.max_size=10"
```

25
docs/migration.md Normal file
View File

@@ -0,0 +1,25 @@
# v3 Migration Guide
## Consumer Settings Changes
The v3 consumer command uses a [different library](https://watchfiles.helpmanual.io/) to unify
the watching for new files in the consume directory. For the user, this removes several configuration options related to delays and retries
and replaces with a single unified setting. It also adjusts how the consumer ignore filtering happens, replaced `fnmatch` with `regex` and
separating the directory ignore from the file ignore.
### Summary
| Old Setting | New Setting | Notes |
| ------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `CONSUMER_POLLING` | [`CONSUMER_POLLING_INTERVAL`](configuration.md#PAPERLESS_CONSUMER_POLLING_INTERVAL) | Renamed for clarity |
| `CONSUMER_INOTIFY_DELAY` | [`CONSUMER_STABILITY_DELAY`](configuration.md#PAPERLESS_CONSUMER_STABILITY_DELAY) | Unified for all modes |
| `CONSUMER_POLLING_DELAY` | _Removed_ | Use `CONSUMER_STABILITY_DELAY` |
| `CONSUMER_POLLING_RETRY_COUNT` | _Removed_ | Automatic with stability tracking |
| `CONSUMER_IGNORE_PATTERNS` | [`CONSUMER_IGNORE_PATTERNS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_PATTERNS) | **Now regex, not fnmatch**; user patterns are added to (not replacing) default ones |
| _New_ | [`CONSUMER_IGNORE_DIRS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_DIRS) | Additional directories to ignore; user entries are added to (not replacing) defaults |
## Encryption Support
Document and thumbnail encryption is no longer supported. This was previously deprecated in [paperless-ng 0.9.3](https://github.com/paperless-ngx/paperless-ngx/blob/dev/docs/changelog.md#paperless-ng-093)
Users must decrypt their document using the `decrypt_documents` command before upgrading.

File diff suppressed because it is too large Load Diff

View File

@@ -4,27 +4,27 @@
Check for the following issues:
- Ensure that the directory you're putting your documents in is the
folder paperless is watching. With docker, this setting is performed
in the `docker-compose.yml` file. Without Docker, look at the
`CONSUMPTION_DIR` setting. Don't adjust this setting if you're
using docker.
- Ensure that the directory you're putting your documents in is the
folder paperless is watching. With docker, this setting is performed
in the `docker-compose.yml` file. Without Docker, look at the
`CONSUMPTION_DIR` setting. Don't adjust this setting if you're
using docker.
- Ensure that redis is up and running. Paperless does its task
processing asynchronously, and for documents to arrive at the task
processor, it needs redis to run.
- Ensure that redis is up and running. Paperless does its task
processing asynchronously, and for documents to arrive at the task
processor, it needs redis to run.
- Ensure that the task processor is running. Docker does this
automatically. Manually invoke the task processor by executing
- Ensure that the task processor is running. Docker does this
automatically. Manually invoke the task processor by executing
```shell-session
celery --app paperless worker
```
```shell-session
celery --app paperless worker
```
- Look at the output of paperless and inspect it for any errors.
- Look at the output of paperless and inspect it for any errors.
- Go to the admin interface, and check if there are failed tasks. If
so, the tasks will contain an error message.
- Go to the admin interface, and check if there are failed tasks. If
so, the tasks will contain an error message.
## Consumer warns `OCR for XX failed`
@@ -78,12 +78,12 @@ Ensure that `chown` is possible on these directories.
This indicates that the Auto matching algorithm found no documents to
learn from. This may have two reasons:
- You don't use the Auto matching algorithm: The error can be safely
ignored in this case.
- You are using the Auto matching algorithm: The classifier explicitly
excludes documents with Inbox tags. Verify that there are documents
in your archive without inbox tags. The algorithm will only learn
from documents not in your inbox.
- You don't use the Auto matching algorithm: The error can be safely
ignored in this case.
- You are using the Auto matching algorithm: The classifier explicitly
excludes documents with Inbox tags. Verify that there are documents
in your archive without inbox tags. The algorithm will only learn
from documents not in your inbox.
## UserWarning in sklearn on every single document
@@ -127,10 +127,10 @@ change in the `docker-compose.yml` file:
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
command:
- 'gotenberg'
- '--chromium-disable-javascript=true'
- '--chromium-allow-list=file:///tmp/.*'
- '--api-timeout=60s'
- 'gotenberg'
- '--chromium-disable-javascript=true'
- '--chromium-allow-list=file:///tmp/.*'
- '--api-timeout=60s'
```
## Permission denied errors in the consumption directory

View File

@@ -14,42 +14,42 @@ for finding and managing your documents.
Paperless essentially consists of two different parts for managing your
documents:
- The _consumer_ watches a specified folder and adds all documents in
that folder to paperless.
- The _web server_ (web UI) provides a UI that you use to manage and
search documents.
- The _consumer_ watches a specified folder and adds all documents in
that folder to paperless.
- The _web server_ (web UI) provides a UI that you use to manage and
search documents.
Each document has data fields that you can assign to them:
- A _Document_ is a piece of paper that sometimes contains valuable
information.
- The _correspondent_ of a document is the person, institution or
company that a document either originates from, or is sent to.
- A _tag_ is a label that you can assign to documents. Think of labels
as more powerful folders: Multiple documents can be grouped together
with a single tag, however, a single document can also have multiple
tags. This is not possible with folders. The reason folders are not
implemented in paperless is simply that tags are much more versatile
than folders.
- A _document type_ is used to demarcate the type of a document such
as letter, bank statement, invoice, contract, etc. It is used to
identify what a document is about.
- The document _storage path_ is the location where the document files
are stored. See [Storage Paths](advanced_usage.md#storage-paths) for
more information.
- The _date added_ of a document is the date the document was scanned
into paperless. You cannot and should not change this date.
- The _date created_ of a document is the date the document was
initially issued. This can be the date you bought a product, the
date you signed a contract, or the date a letter was sent to you.
- The _archive serial number_ (short: ASN) of a document is the
identifier of the document in your physical document binders. See
[recommended workflow](#usage-recommended-workflow) below.
- The _content_ of a document is the text that was OCR'ed from the
document. This text is fed into the search engine and is used for
matching tags, correspondents and document types.
- Paperless-ngx also supports _custom fields_ which can be used to
store additional metadata about a document.
- A _Document_ is a piece of paper that sometimes contains valuable
information.
- The _correspondent_ of a document is the person, institution or
company that a document either originates from, or is sent to.
- A _tag_ is a label that you can assign to documents. Think of labels
as more powerful folders: Multiple documents can be grouped together
with a single tag, however, a single document can also have multiple
tags. This is not possible with folders. The reason folders are not
implemented in paperless is simply that tags are much more versatile
than folders.
- A _document type_ is used to demarcate the type of a document such
as letter, bank statement, invoice, contract, etc. It is used to
identify what a document is about.
- The document _storage path_ is the location where the document files
are stored. See [Storage Paths](advanced_usage.md#storage-paths) for
more information.
- The _date added_ of a document is the date the document was scanned
into paperless. You cannot and should not change this date.
- The _date created_ of a document is the date the document was
initially issued. This can be the date you bought a product, the
date you signed a contract, or the date a letter was sent to you.
- The _archive serial number_ (short: ASN) of a document is the
identifier of the document in your physical document binders. See
[recommended workflow](#usage-recommended-workflow) below.
- The _content_ of a document is the text that was OCR'ed from the
document. This text is fed into the search engine and is used for
matching tags, correspondents and document types.
- Paperless-ngx also supports _custom fields_ which can be used to
store additional metadata about a document.
## The Web UI
@@ -93,12 +93,11 @@ download the document or share it via a share link.
Think of versions as **file history** for a document.
- Versions track the underlying file and extracted text content (OCR/text).
- Metadata such as tags, correspondent, document type, storage path and custom fields stay on the "root" document.
- Version files follow normal filename formatting (including storage paths) and add a `_vN` suffix (for example `_v1`, `_v2`).
- By default, search and document content use the latest version.
- In document detail, selecting a version switches the preview, file metadata and content (and download etc buttons) to that version.
- Deleting a non-root version keeps metadata and falls back to the latest remaining version.
- Versions track the underlying file and extracted text content (OCR/text).
- Metadata such as tags, correspondent, document type, storage path and custom fields stay on the "root" document.
- By default, search and document content use the latest version.
- In document detail, selecting a version switches the preview, file metadata and content (and download etc buttons) to that version.
- Deleting a non-root version keeps metadata and falls back to the latest remaining version.
### Management Lists
@@ -218,20 +217,21 @@ patterns can include wildcards and multiple patterns separated by a comma.
The actions all ensure that the same mail is not consumed twice by
different means. These are as follows:
- **Delete:** Immediately deletes mail that paperless has consumed
documents from. Use with caution.
- **Mark as read:** Mark consumed mail as read. Paperless will not
consume documents from already read mails. If you read a mail before
paperless sees it, it will be ignored.
- **Flag:** Sets the 'important' flag on mails with consumed
documents. Paperless will not consume flagged mails.
- **Move to folder:** Moves consumed mails out of the way so that
paperless won't consume them again.
- **Add custom Tag:** Adds a custom tag to mails with consumed
documents (the IMAP standard calls these "keywords"). Paperless
will not consume mails already tagged. Not all mail servers support
this feature!
- **Apple Mail support:** Apple Mail clients allow differently colored tags. For this to work use `apple:<color>` (e.g. _apple:green_) as a custom tag. Available colors are _red_, _orange_, _yellow_, _blue_, _green_, _violet_ and _grey_.
- **Delete:** Immediately deletes mail that paperless has consumed
documents from. Use with caution.
- **Mark as read:** Mark consumed mail as read. Paperless will not
consume documents from already read mails. If you read a mail before
paperless sees it, it will be ignored.
- **Flag:** Sets the 'important' flag on mails with consumed
documents. Paperless will not consume flagged mails.
- **Move to folder:** Moves consumed mails out of the way so that
paperless won't consume them again.
- **Add custom Tag:** Adds a custom tag to mails with consumed
documents (the IMAP standard calls these "keywords"). Paperless
will not consume mails already tagged. Not all mail servers support
this feature!
- **Apple Mail support:** Apple Mail clients allow differently colored tags. For this to work use `apple:<color>` (e.g. _apple:green_) as a custom tag. Available colors are _red_, _orange_, _yellow_, _blue_, _green_, _violet_ and _grey_.
!!! warning
@@ -324,12 +324,12 @@ or using [email](#workflow-action-email) or [webhook](#workflow-action-webhook)
"Share links" are public links to files (or an archive of files) and can be created and managed under the 'Send' button on the document detail screen or from the bulk editor.
- Share links do not require a user to login and thus link directly to a file or bundled download.
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
- Links can optionally have an expiration time set.
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
- From the document detail screen you can create a share link for that single document.
- From the bulk editor you can create a **share link bundle** for any selection. Paperless-ngx prepares a ZIP archive in the background and exposes a single share link. You can revisit the "Manage share link bundles" dialog to monitor progress, retry failed bundles, or delete links.
- Share links do not require a user to login and thus link directly to a file or bundled download.
- Links are unique and are of the form `{paperless-url}/share/{randomly-generated-slug}`.
- Links can optionally have an expiration time set.
- After a link expires or is deleted users will be redirected to the regular paperless-ngx login.
- From the document detail screen you can create a share link for that single document.
- From the bulk editor you can create a **share link bundle** for any selection. Paperless-ngx prepares a ZIP archive in the background and exposes a single share link. You can revisit the "Manage share link bundles" dialog to monitor progress, retry failed bundles, or delete links.
!!! tip
@@ -382,11 +382,6 @@ permissions can be granted to limit access to certain parts of the UI (and corre
Superusers can access all parts of the front and backend application as well as any and all objects. Superuser status can only be granted by another superuser.
!!! tip
Because superuser accounts can see all objects and documents, you may want to use a regular account for day-to-day use. Additional superuser accounts can
be created via [cli](administration.md#create-superuser) or granted superuser status from an existing superuser account.
#### Admin Status
Admin status (Django 'staff status') grants access to viewing the paperless logs and the system status dialog
@@ -513,25 +508,25 @@ flowchart TD
Workflows allow you to filter by:
- Source, e.g. documents uploaded via consume folder, API (& the web UI) and mail fetch
- File name, including wildcards e.g. \*.pdf will apply to all pdfs.
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
example, automatically assigning documents to different owners based on the upload directory.
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
- Content matching (`Added`, `Updated` and `Scheduled` triggers only). Filter document content using the matching settings.
- Source, e.g. documents uploaded via consume folder, API (& the web UI) and mail fetch
- File name, including wildcards e.g. \*.pdf will apply to all pdfs.
- File path, including wildcards. Note that enabling `PAPERLESS_CONSUMER_RECURSIVE` would allow, for
example, automatically assigning documents to different owners based on the upload directory.
- Mail rule. Choosing this option will force 'mail fetch' to be the workflow source.
- Content matching (`Added`, `Updated` and `Scheduled` triggers only). Filter document content using the matching settings.
There are also 'advanced' filters available for `Added`, `Updated` and `Scheduled` triggers:
- Any Tags: Filter for documents with any of the specified tags.
- All Tags: Filter for documents with all of the specified tags.
- No Tags: Filter for documents with none of the specified tags.
- Document type: Filter documents with this document type.
- Not Document types: Filter documents without any of these document types.
- Correspondent: Filter documents with this correspondent.
- Not Correspondents: Filter documents without any of these correspondents.
- Storage path: Filter documents with this storage path.
- Not Storage paths: Filter documents without any of these storage paths.
- Custom field query: Filter documents with a custom field query (the same as used for the document list filters).
- Any Tags: Filter for documents with any of the specified tags.
- All Tags: Filter for documents with all of the specified tags.
- No Tags: Filter for documents with none of the specified tags.
- Document type: Filter documents with this document type.
- Not Document types: Filter documents without any of these document types.
- Correspondent: Filter documents with this correspondent.
- Not Correspondents: Filter documents without any of these correspondents.
- Storage path: Filter documents with this storage path.
- Not Storage paths: Filter documents without any of these storage paths.
- Custom field query: Filter documents with a custom field query (the same as used for the document list filters).
### Workflow Actions
@@ -543,54 +538,42 @@ The following workflow action types are available:
"Assignment" actions can assign:
- Title, see [workflow placeholders](usage.md#workflow-placeholders) below
- Tags, correspondent, document type and storage path
- Document owner
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
- Title, see [workflow placeholders](usage.md#workflow-placeholders) below
- Tags, correspondent, document type and storage path
- Document owner
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
##### Removal {#workflow-action-removal}
"Removal" actions can remove either all of or specific sets of the following:
- Tags, correspondents, document types or storage paths
- Document owner
- View and / or edit permissions
- Custom fields
- Tags, correspondents, document types or storage paths
- Document owner
- View and / or edit permissions
- Custom fields
##### Email {#workflow-action-email}
"Email" actions can send documents via email. This action requires a mail server to be [configured](configuration.md#email-sending). You can specify:
- The recipient email address(es) separated by commas
- The subject and body of the email, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below
- Whether to include the document as an attachment
- The recipient email address(es) separated by commas
- The subject and body of the email, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below
- Whether to include the document as an attachment
##### Webhook {#workflow-action-webhook}
"Webhook" actions send a POST request to a specified URL. You can specify:
- The URL to send the request to
- The request body as text or as key-value pairs, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below.
- Encoding for the request body, either JSON or form data
- The request headers as key-value pairs
- The URL to send the request to
- The request body as text or as key-value pairs, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below.
- Encoding for the request body, either JSON or form data
- The request headers as key-value pairs
For security reasons, webhooks can be limited to specific ports and disallowed from connecting to local URLs. See the relevant
[configuration settings](configuration.md#workflow-webhooks) to change this behavior. If you are allowing non-admins to create workflows,
you may want to adjust these settings to prevent abuse.
##### Move to Trash {#workflow-action-move-to-trash}
"Move to Trash" actions move the document to the trash. The document can be restored
from the trash until the trash is emptied (after the configured delay or manually).
The "Move to Trash" action will always be executed at the end of the workflow run,
regardless of its position in the action list. After a "Move to Trash" action is executed
no other workflow will be executed on the document.
If a "Move to Trash" action is executed in a consume pipeline, the consumption
will be aborted and the file will be deleted.
#### Workflow placeholders
Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
@@ -604,33 +587,33 @@ The available inputs differ depending on the type of workflow trigger.
This is because at the time of consumption (when the text is to be set), no automatic tags etc. have been
applied. You can use the following placeholders in the template with any trigger type:
- `{{correspondent}}`: assigned correspondent name
- `{{document_type}}`: assigned document type name
- `{{owner_username}}`: assigned owner username
- `{{added}}`: added datetime
- `{{added_year}}`: added year
- `{{added_year_short}}`: added year
- `{{added_month}}`: added month
- `{{added_month_name}}`: added month name
- `{{added_month_name_short}}`: added month short name
- `{{added_day}}`: added day
- `{{added_time}}`: added time in HH:MM format
- `{{original_filename}}`: original file name without extension
- `{{filename}}`: current file name without extension (for "added" workflows this may not be final yet, you can use `{{original_filename}}`)
- `{{doc_title}}`: current document title (cannot be used in title assignment)
- `{{correspondent}}`: assigned correspondent name
- `{{document_type}}`: assigned document type name
- `{{owner_username}}`: assigned owner username
- `{{added}}`: added datetime
- `{{added_year}}`: added year
- `{{added_year_short}}`: added year
- `{{added_month}}`: added month
- `{{added_month_name}}`: added month name
- `{{added_month_name_short}}`: added month short name
- `{{added_day}}`: added day
- `{{added_time}}`: added time in HH:MM format
- `{{original_filename}}`: original file name without extension
- `{{filename}}`: current file name without extension
- `{{doc_title}}`: current document title (cannot be used in title assignment)
The following placeholders are only available for "added" or "updated" triggers
- `{{created}}`: created datetime
- `{{created_year}}`: created year
- `{{created_year_short}}`: created year
- `{{created_month}}`: created month
- `{{created_month_name}}`: created month name
- `{{created_month_name_short}}`: created month short name
- `{{created_day}}`: created day
- `{{created_time}}`: created time in HH:MM format
- `{{doc_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
- `{{doc_id}}`: Document ID
- `{{created}}`: created datetime
- `{{created_year}}`: created year
- `{{created_year_short}}`: created year
- `{{created_month}}`: created month
- `{{created_month_name}}`: created month name
- `{created_month_name_short}}`: created month short name
- `{{created_day}}`: created day
- `{{created_time}}`: created time in HH:MM format
- `{{doc_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
- `{{doc_id}}`: Document ID
##### Examples
@@ -675,26 +658,26 @@ Multiple fields may be attached to a document but the same field name cannot be
The following custom field types are supported:
- `Text`: any text
- `Boolean`: true / false (check / unchecked) field
- `Date`: date
- `URL`: a valid url
- `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456
- `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
- `Select`: a pre-defined list of strings from which the user can choose
- `Text`: any text
- `Boolean`: true / false (check / unchecked) field
- `Date`: date
- `URL`: a valid url
- `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456
- `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30
- `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
- `Select`: a pre-defined list of strings from which the user can choose
## PDF Actions
Paperless-ngx supports basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files). When viewing an individual document you can
open the 'PDF Editor' to use a simple UI for re-arranging, rotating, deleting pages and splitting documents.
- Merging documents: available when selecting multiple documents for 'bulk editing'.
- Rotating documents: available when selecting multiple documents for 'bulk editing' and via the pdf editor on an individual document's details page.
- Splitting documents: via the pdf editor on an individual document's details page.
- Deleting pages: via the pdf editor on an individual document's details page.
- Re-arranging pages: via the pdf editor on an individual document's details page.
- Merging documents: available when selecting multiple documents for 'bulk editing'.
- Rotating documents: available when selecting multiple documents for 'bulk editing' and via the pdf editor on an individual document's details page.
- Splitting documents: via the pdf editor on an individual document's details page.
- Deleting pages: via the pdf editor on an individual document's details page.
- Re-arranging pages: via the pdf editor on an individual document's details page.
!!! important
@@ -772,18 +755,18 @@ the system.
Here are a couple examples of tags and types that you could use in your
collection.
- An `inbox` tag for newly added documents that you haven't manually
edited yet.
- A tag `car` for everything car related (repairs, registration,
insurance, etc)
- A tag `todo` for documents that you still need to do something with,
such as reply, or perform some task online.
- A tag `bank account x` for all bank statement related to that
account.
- A tag `mail` for anything that you added to paperless via its mail
processing capabilities.
- A tag `missing_metadata` when you still need to add some metadata to
a document, but can't or don't want to do this right now.
- An `inbox` tag for newly added documents that you haven't manually
edited yet.
- A tag `car` for everything car related (repairs, registration,
insurance, etc)
- A tag `todo` for documents that you still need to do something with,
such as reply, or perform some task online.
- A tag `bank account x` for all bank statement related to that
account.
- A tag `mail` for anything that you added to paperless via its mail
processing capabilities.
- A tag `missing_metadata` when you still need to add some metadata to
a document, but can't or don't want to do this right now.
## Searching {#basic-usage_searching}
@@ -872,8 +855,8 @@ The following diagram shows how easy it is to manage your documents.
### Preparations in paperless
- Create an inbox tag that gets assigned to all new documents.
- Create a TODO tag.
- Create an inbox tag that gets assigned to all new documents.
- Create a TODO tag.
### Processing of the physical documents
@@ -947,15 +930,15 @@ Some documents require attention and require you to act on the document.
You may take two different approaches to handle these documents based on
how regularly you intend to scan documents and use paperless.
- If you scan and process your documents in paperless regularly,
assign a TODO tag to all scanned documents that you need to process.
Create a saved view on the dashboard that shows all documents with
this tag.
- If you do not scan documents regularly and use paperless solely for
archiving, create a physical todo box next to your physical inbox
and put documents you need to process in the TODO box. When you
performed the task associated with the document, move it to the
inbox.
- If you scan and process your documents in paperless regularly,
assign a TODO tag to all scanned documents that you need to process.
Create a saved view on the dashboard that shows all documents with
this tag.
- If you do not scan documents regularly and use paperless solely for
archiving, create a physical todo box next to your physical inbox
and put documents you need to process in the TODO box. When you
performed the task associated with the document, move it to the
inbox.
## Remote OCR
@@ -976,63 +959,64 @@ or page limitations (e.g. with a free tier).
Paperless-ngx consists of the following components:
- **The webserver:** This serves the administration pages, the API,
and the new frontend. This is the main tool you'll be using to interact
with paperless. You may start the webserver directly with
- **The webserver:** This serves the administration pages, the API,
and the new frontend. This is the main tool you'll be using to interact
with paperless. You may start the webserver directly with
```shell-session
cd /path/to/paperless/src/
granian --interface asginl --ws "paperless.asgi:application"
```
```shell-session
cd /path/to/paperless/src/
granian --interface asginl --ws "paperless.asgi:application"
```
or by any other means such as Apache `mod_wsgi`.
or by any other means such as Apache `mod_wsgi`.
- **The consumer:** This is what watches your consumption folder for
documents. However, the consumer itself does not really consume your
documents. Now it notifies a task processor that a new file is ready
for consumption. I suppose it should be named differently. This was
also used to check your emails, but that's now done elsewhere as
well.
- **The consumer:** This is what watches your consumption folder for
documents. However, the consumer itself does not really consume your
documents. Now it notifies a task processor that a new file is ready
for consumption. I suppose it should be named differently. This was
also used to check your emails, but that's now done elsewhere as
well.
Start the consumer with the management command `document_consumer`:
Start the consumer with the management command `document_consumer`:
```shell-session
cd /path/to/paperless/src/
python3 manage.py document_consumer
```
```shell-session
cd /path/to/paperless/src/
python3 manage.py document_consumer
```
- **The task processor:** Paperless relies on [Celery - Distributed
Task Queue](https://docs.celeryq.dev/en/stable/index.html) for doing
most of the heavy lifting. This is a task queue that accepts tasks
from multiple sources and processes these in parallel. It also comes
with a scheduler that executes certain commands periodically.
- **The task processor:** Paperless relies on [Celery - Distributed
Task Queue](https://docs.celeryq.dev/en/stable/index.html) for doing
most of the heavy lifting. This is a task queue that accepts tasks
from multiple sources and processes these in parallel. It also comes
with a scheduler that executes certain commands periodically.
This task processor is responsible for:
- Consuming documents. When the consumer finds new documents, it
notifies the task processor to start a consumption task.
- The task processor also performs the consumption of any
documents you upload through the web interface.
- Consuming emails. It periodically checks your configured
accounts for new emails and notifies the task processor to
consume the attachment of an email.
- Maintaining the search index and the automatic matching
algorithm. These are things that paperless needs to do from time
to time in order to operate properly.
This task processor is responsible for:
This allows paperless to process multiple documents from your
consumption folder in parallel! On a modern multi core system, this
makes the consumption process with full OCR blazingly fast.
- Consuming documents. When the consumer finds new documents, it
notifies the task processor to start a consumption task.
- The task processor also performs the consumption of any
documents you upload through the web interface.
- Consuming emails. It periodically checks your configured
accounts for new emails and notifies the task processor to
consume the attachment of an email.
- Maintaining the search index and the automatic matching
algorithm. These are things that paperless needs to do from time
to time in order to operate properly.
The task processor comes with a built-in admin interface that you
can use to check whenever any of the tasks fail and inspect the
errors (i.e., wrong email credentials, errors during consuming a
specific file, etc).
This allows paperless to process multiple documents from your
consumption folder in parallel! On a modern multi core system, this
makes the consumption process with full OCR blazingly fast.
- A [redis](https://redis.io/) message broker: This is a really
lightweight service that is responsible for getting the tasks from
the webserver and the consumer to the task scheduler. These run in a
different process (maybe even on different machines!), and
therefore, this is necessary.
The task processor comes with a built-in admin interface that you
can use to check whenever any of the tasks fail and inspect the
errors (i.e., wrong email credentials, errors during consuming a
specific file, etc).
- Optional: A database server. Paperless supports PostgreSQL, MariaDB
and SQLite for storing its data.
- A [redis](https://redis.io/) message broker: This is a really
lightweight service that is responsible for getting the tasks from
the webserver and the consumer to the task scheduler. These run in a
different process (maybe even on different machines!), and
therefore, this is necessary.
- Optional: A database server. Paperless supports PostgreSQL, MariaDB
and SQLite for storing its data.

View File

@@ -1,11 +1,12 @@
[project]
name = "paperless-ngx"
version = "2.20.10"
version = "2.20.6"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
@@ -34,22 +35,20 @@ dependencies = [
"django-cors-headers~=4.9.0",
"django-extensions~=4.1",
"django-filter~=25.1",
"django-guardian~=3.3.0",
"django-guardian~=3.2.0",
"django-multiselectfield~=1.0.1",
"django-rich~=2.2.0",
"django-soft-delete~=1.0.18",
"django-treenode>=0.23.2",
"djangorestframework~=3.16",
"djangorestframework-guardian~=0.4.0",
"drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2026.3.1",
"drf-spectacular-sidecar~=2026.1.1",
"drf-writable-nested~=0.7.1",
"faiss-cpu>=1.10",
"filelock~=3.25.2",
"filelock~=3.20.0",
"flower~=2.0.1",
"gotenberg-client~=0.13.1",
"httpx-oauth~=0.16",
"ijson>=3.2",
"imap-tools~=1.11.0",
"jinja2~=3.1.5",
"langdetect~=1.0.9",
@@ -60,7 +59,7 @@ dependencies = [
"llama-index-llms-openai>=0.6.13",
"llama-index-vector-stores-faiss>=0.5.2",
"nltk~=3.9.1",
"ocrmypdf~=17.3.0",
"ocrmypdf~=16.13.0",
"openai>=1.76",
"pathvalidate~=3.3.1",
"pdf2image~=1.17.0",
@@ -69,18 +68,20 @@ dependencies = [
"python-gnupg~=0.5.4",
"python-ipware~=3.0.0",
"python-magic~=0.4.27",
"pyzbar~=0.1.9",
"rapidfuzz~=3.14.0",
"redis[hiredis]~=5.2.1",
"regex>=2025.9.18",
"scikit-learn~=1.8.0",
"scikit-learn~=1.7.0",
"sentence-transformers>=4.1",
"setproctitle~=1.3.4",
"tika-client~=0.10.0",
"torch~=2.10.0",
"tqdm~=4.67.1",
"watchfiles>=1.1.1",
"whitenoise~=6.11",
"whoosh-reloaded>=2.7.5",
"zxing-cpp~=3.0.0",
"zxing-cpp~=2.3.0",
]
optional-dependencies.mariadb = [
@@ -111,12 +112,11 @@ docs = [
testing = [
"daphne",
"factory-boy~=3.3.1",
"faker~=40.8.0",
"imagehash",
"pytest~=9.0.0",
"pytest-cov~=7.0.0",
"pytest-django~=4.12.0",
"pytest-env~=1.5.0",
"pytest-django~=4.11.1",
"pytest-env~=1.2.0",
"pytest-httpx",
"pytest-mock~=3.15.1",
#"pytest-randomly~=4.0.1",
@@ -150,6 +150,7 @@ typing = [
"types-pytz",
"types-redis",
"types-setuptools",
"types-tqdm",
]
[tool.uv]
@@ -171,13 +172,17 @@ psycopg-c = [
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-trixie-3.3.0/psycopg_c-3.3.0-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
]
zxing-cpp = [
{ url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_x86_64.whl", marker = "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.12'" },
{ url = "https://github.com/paperless-ngx/builder/releases/download/zxing-2.3.0/zxing_cpp-2.3.0-cp312-cp312-linux_aarch64.whl", marker = "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.12'" },
]
torch = [
{ index = "pytorch-cpu" },
]
[tool.ruff]
target-version = "py311"
target-version = "py310"
line-length = 88
src = [
"src",
@@ -304,13 +309,11 @@ markers = [
"tika: Tests requiring Tika service",
"greenmail: Tests requiring Greenmail service",
"date_parsing: Tests which cover date parsing from content or filename",
"management: Tests which cover management commands/functionality",
]
[tool.pytest_env]
PAPERLESS_DISABLE_DBHANDLER = "true"
PAPERLESS_CACHE_BACKEND = "django.core.cache.backends.locmem.LocMemCache"
PAPERLESS_CHANNELS_BACKEND = "channels.layers.InMemoryChannelLayer"
[tool.coverage.report]
exclude_also = [

View File

@@ -19,4 +19,6 @@ following additional information about it:
* Correspondent: ${DOCUMENT_CORRESPONDENT}
* Tags: ${DOCUMENT_TAGS}
It was consumed with the passphrase ${PASSPHRASE}
"

51
src-ui/.eslintrc.json Normal file
View File

@@ -0,0 +1,51 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*",
"/src/app/components/common/pdf-viewer/**"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "pngx",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "pngx",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

View File

@@ -52,11 +52,11 @@ test('dashboard saved view document links', async ({ page }) => {
test('test slim sidebar', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
await page.goto('/dashboard')
await page.locator('.sidebar-slim-toggler').click()
await page.locator('#sidebarMenu').getByRole('button').click()
await expect(
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
).toBeHidden()
await page.locator('.sidebar-slim-toggler').click()
await page.locator('#sidebarMenu').getByRole('button').click()
await expect(
page.getByRole('link', { name: 'Dashboard' }).getByText('Dashboard')
).toBeVisible()

View File

@@ -58,7 +58,7 @@
"content": {
"size": -1,
"mimeType": "application/json",
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
},
"headersSize": -1,
"bodySize": -1,

View File

@@ -58,7 +58,7 @@
"content": {
"size": -1,
"mimeType": "application/json",
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
},
"headersSize": -1,
"bodySize": -1,

View File

@@ -58,7 +58,7 @@
"content": {
"size": -1,
"mimeType": "application/json",
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
},
"headersSize": -1,
"bodySize": -1,

View File

@@ -58,7 +58,7 @@
"content": {
"size": -1,
"mimeType": "application/json",
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true,\"dashboard_views_visible_ids\":[7,4],\"sidebar_views_visible_ids\":[7,4,11]},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"delete_schedule\",\"view_note\",\"view_taskattributes\",\"delete_ormq\",\"add_ormq\",\"change_tag\",\"change_chordcounter\",\"delete_savedviewfilterrule\",\"delete_comment\",\"add_session\",\"add_mailrule\",\"add_tokenproxy\",\"delete_taskresult\",\"view_failure\",\"add_tag\",\"view_savedviewfilterrule\",\"view_paperlesstask\",\"change_mailaccount\",\"change_frontendsettings\",\"delete_group\",\"add_userobjectpermission\",\"add_failure\",\"delete_mailrule\",\"view_userobjectpermission\",\"change_schedule\",\"delete_note\",\"view_ormq\",\"add_note\",\"add_chordcounter\",\"delete_token\",\"change_failure\",\"add_savedviewfilterrule\",\"delete_user\",\"view_correspondent\",\"view_schedule\",\"change_tokenproxy\",\"view_user\",\"delete_task\",\"delete_correspondent\",\"delete_chordcounter\",\"delete_document\",\"view_taskresult\",\"change_document\",\"add_frontendsettings\",\"view_mailrule\",\"change_ormq\",\"delete_taskattributes\",\"view_logentry\",\"view_mailaccount\",\"view_log\",\"delete_success\",\"view_frontendsettings\",\"view_documenttype\",\"change_taskresult\",\"view_permission\",\"add_groupobjectpermission\",\"change_user\",\"view_document\",\"change_userobjectpermission\",\"add_user\",\"add_correspondent\",\"add_token\",\"add_mailaccount\",\"change_group\",\"add_group\",\"delete_processedmail\",\"delete_contenttype\",\"add_savedview\",\"view_chordcounter\",\"delete_tokenproxy\",\"change_groupresult\",\"delete_session\",\"view_savedview\",\"view_processedmail\",\"add_comment\",\"view_storagepath\",\"delete_documenttype\",\"add_processedmail\",\"view_group\",\"change_processedmail\",\"view_session\",\"delete_storagepath\",\"delete_paperlesstask\",\"add_groupresult\",\"delete_savedview\",\"delete_userobjectpermission\",\"view_tokenproxy\",\"add_task\",\"view_tag\",\"add_taskresult\",\"change_documenttype\",\"change_mailrule\",\"add_document\",\"change_comment\",\"view_task\",\"view_groupresult\",\"change_contenttype\",\"view_groupobjectpermission\",\"change_task\",\"add_log\",\"add_success\",\"change_savedview\",\"delete_frontendsettings\",\"view_success\",\"add_permission\",\"change_correspondent\",\"add_paperlesstask\",\"change_paperlesstask\",\"add_contenttype\",\"view_comment\",\"change_logentry\",\"delete_logentry\",\"delete_mailaccount\",\"change_session\",\"delete_groupresult\",\"add_logentry\",\"change_savedviewfilterrule\",\"change_success\",\"delete_tag\",\"add_taskattributes\",\"change_groupobjectpermission\",\"delete_failure\",\"add_uisettings\",\"view_token\",\"add_schedule\",\"delete_log\",\"delete_uisettings\",\"change_permission\",\"delete_groupobjectpermission\",\"change_token\",\"view_uisettings\",\"change_uisettings\",\"delete_permission\",\"add_storagepath\",\"change_storagepath\",\"view_contenttype\",\"change_note\",\"change_log\",\"change_taskattributes\",\"add_documenttype\"]}"
},
"headersSize": -1,
"bodySize": -1,

View File

@@ -33,9 +33,9 @@ test('should not allow user to view correspondents', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
page.getByRole('link', { name: 'Correspondents' })
).not.toBeAttached()
await page.goto('/attributes/correspondents')
await page.goto('/correspondents')
await expect(page.locator('body')).toHaveText(
/You don't have permissions to do that/i
)
@@ -44,10 +44,8 @@ test('should not allow user to view correspondents', async ({ page }) => {
test('should not allow user to view tags', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
).not.toBeAttached()
await page.goto('/attributes/tags')
await expect(page.getByRole('link', { name: 'Tags' })).not.toBeAttached()
await page.goto('/tags')
await expect(page.locator('body')).toHaveText(
/You don't have permissions to do that/i
)
@@ -57,9 +55,9 @@ test('should not allow user to view document types', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
page.getByRole('link', { name: 'Document Types' })
).not.toBeAttached()
await page.goto('/attributes/documenttypes')
await page.goto('/documenttypes')
await expect(page.locator('body')).toHaveText(
/You don't have permissions to do that/i
)
@@ -69,9 +67,9 @@ test('should not allow user to view storage paths', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
await page.goto('/dashboard')
await expect(
page.getByRole('link', { name: 'Attributes' })
page.getByRole('link', { name: 'Storage Paths' })
).not.toBeAttached()
await page.goto('/attributes/storagepaths')
await page.goto('/storagepaths')
await expect(page.locator('body')).toHaveText(
/You don't have permissions to do that/i
)

View File

@@ -1,58 +0,0 @@
const angularEslintPlugin = require('@angular-eslint/eslint-plugin')
const angularTemplatePlugin = require('@angular-eslint/eslint-plugin-template')
const angularTemplateParser = require('@angular-eslint/template-parser')
const tsParser = require('@typescript-eslint/parser')
module.exports = [
{
ignores: ['projects/**/*', 'src/app/components/common/pdf-viewer/**'],
},
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: ['tsconfig.json'],
createDefaultProgram: true,
ecmaVersion: 2020,
sourceType: 'module',
},
},
plugins: {
'@angular-eslint': angularEslintPlugin,
'@angular-eslint/template': angularTemplatePlugin,
},
processor: '@angular-eslint/template/extract-inline-html',
rules: {
...angularEslintPlugin.configs.recommended.rules,
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'pngx',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'pngx',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
languageOptions: {
parser: angularTemplateParser,
},
plugins: {
'@angular-eslint/template': angularTemplatePlugin,
},
rules: {
...angularTemplatePlugin.configs.recommended.rules,
},
},
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
"version": "2.20.10",
"version": "2.20.6",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
@@ -11,17 +11,17 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^21.2.2",
"@angular/common": "~21.2.4",
"@angular/compiler": "~21.2.4",
"@angular/core": "~21.2.4",
"@angular/forms": "~21.2.4",
"@angular/localize": "~21.2.4",
"@angular/platform-browser": "~21.2.4",
"@angular/platform-browser-dynamic": "~21.2.4",
"@angular/router": "~21.2.4",
"@angular/cdk": "^21.1.3",
"@angular/common": "~21.1.3",
"@angular/compiler": "~21.1.3",
"@angular/core": "~21.1.3",
"@angular/forms": "~21.1.3",
"@angular/localize": "~21.1.3",
"@angular/platform-browser": "~21.1.3",
"@angular/platform-browser-dynamic": "~21.1.3",
"@angular/router": "~21.1.3",
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
"@ng-select/ng-select": "^21.4.1",
"@ng-select/ng-select": "^21.2.0",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8",
@@ -37,38 +37,38 @@
"tslib": "^2.8.1",
"utif": "^3.1.0",
"uuid": "^13.0.0",
"zone.js": "^0.16.1"
"zone.js": "^0.16.0"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^21.0.3",
"@angular-builders/jest": "^21.0.3",
"@angular-devkit/core": "^21.2.2",
"@angular-devkit/schematics": "^21.2.2",
"@angular-eslint/builder": "21.3.0",
"@angular-eslint/eslint-plugin": "21.3.0",
"@angular-eslint/eslint-plugin-template": "21.3.0",
"@angular-eslint/schematics": "21.3.0",
"@angular-eslint/template-parser": "21.3.0",
"@angular/build": "^21.2.2",
"@angular/cli": "~21.2.2",
"@angular/compiler-cli": "~21.2.4",
"@angular-devkit/core": "^21.1.3",
"@angular-devkit/schematics": "^21.1.3",
"@angular-eslint/builder": "21.2.0",
"@angular-eslint/eslint-plugin": "21.2.0",
"@angular-eslint/eslint-plugin-template": "21.2.0",
"@angular-eslint/schematics": "21.2.0",
"@angular-eslint/template-parser": "21.2.0",
"@angular/build": "^21.1.3",
"@angular/cli": "~21.1.3",
"@angular/compiler-cli": "~21.1.3",
"@codecov/webpack-plugin": "^1.9.1",
"@playwright/test": "^1.58.2",
"@types/jest": "^30.0.0",
"@types/node": "^25.3.3",
"@types/node": "^25.2.1",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"@typescript-eslint/utils": "^8.54.0",
"eslint": "^10.0.2",
"eslint": "^9.39.2",
"jest": "30.2.0",
"jest-environment-jsdom": "^30.2.0",
"jest-junit": "^16.0.0",
"jest-preset-angular": "^16.1.1",
"jest-preset-angular": "^16.0.0",
"jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.1",
"typescript": "^5.9.3",
"webpack": "^5.105.3"
"webpack": "^5.105.0"
},
"packageManager": "pnpm@10.17.1",
"pnpm": {

2936
src-ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,13 @@ import { DashboardComponent } from './components/dashboard/dashboard.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DocumentListComponent } from './components/document-list/document-list.component'
import { DocumentAttributesComponent } from './components/manage/document-attributes/document-attributes.component'
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { MailComponent } from './components/manage/mail/mail.component'
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
import { TagListComponent } from './components/manage/tag-list/tag-list.component'
import { WorkflowsComponent } from './components/manage/workflows/workflows.component'
import { NotFoundComponent } from './components/not-found/not-found.component'
import { DirtyDocGuard } from './guards/dirty-doc.guard'
@@ -101,77 +105,53 @@ export const routes: Routes = [
componentName: 'DocumentAsnComponent',
},
},
{
path: 'attributes',
component: DocumentAttributesComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermissionAny: [
{ action: PermissionAction.View, type: PermissionType.Tag },
{
action: PermissionAction.View,
type: PermissionType.Correspondent,
},
{
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
{ action: PermissionAction.View, type: PermissionType.StoragePath },
{ action: PermissionAction.View, type: PermissionType.CustomField },
],
componentName: 'DocumentAttributesComponent',
},
},
{
path: 'attributes/:section',
component: DocumentAttributesComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermissionAny: [
{ action: PermissionAction.View, type: PermissionType.Tag },
{
action: PermissionAction.View,
type: PermissionType.Correspondent,
},
{
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
{ action: PermissionAction.View, type: PermissionType.StoragePath },
{ action: PermissionAction.View, type: PermissionType.CustomField },
],
componentName: 'DocumentAttributesComponent',
},
},
{
path: 'documentproperties',
redirectTo: '/attributes',
pathMatch: 'full',
},
{
path: 'documentproperties/:section',
redirectTo: '/attributes/:section',
pathMatch: 'full',
},
{
path: 'tags',
redirectTo: '/attributes/tags',
pathMatch: 'full',
},
{
path: 'correspondents',
redirectTo: '/attributes/correspondents',
pathMatch: 'full',
component: TagListComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.Tag,
},
componentName: 'TagListComponent',
},
},
{
path: 'documenttypes',
redirectTo: '/attributes/documenttypes',
pathMatch: 'full',
component: DocumentTypeListComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.DocumentType,
},
componentName: 'DocumentTypeListComponent',
},
},
{
path: 'correspondents',
component: CorrespondentListComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.Correspondent,
},
componentName: 'CorrespondentListComponent',
},
},
{
path: 'storagepaths',
redirectTo: '/attributes/storagepaths',
pathMatch: 'full',
component: StoragePathListComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.StoragePath,
},
componentName: 'StoragePathListComponent',
},
},
{
path: 'logs',
@@ -259,8 +239,15 @@ export const routes: Routes = [
},
{
path: 'customfields',
redirectTo: '/attributes/customfields',
pathMatch: 'full',
component: CustomFieldsComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.CustomField,
},
componentName: 'CustomFieldsComponent',
},
},
{
path: 'workflows',

View File

@@ -195,8 +195,8 @@ export class AppComponent implements OnInit, OnDestroy {
},
{
anchorId: 'tour.tags',
content: $localize`Attributes like tags, correspondents, document types, storage paths and custom fields can all be managed here. They can also be created from the document edit view.`,
route: '/attributes/tags',
content: $localize`Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view.`,
route: '/tags',
backdropConfig: {
offset: 0,
},

View File

@@ -19,18 +19,13 @@
<div class="col">
<div class="card bg-light">
<div class="card-body">
<div class="card-title d-flex align-items-center">
<h6 class="mb-0">
{{option.title}}
<div class="card-title">
<h6>
{{option.title}}
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
<i-bs name="info-circle"></i-bs>
</a>
</h6>
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
<i-bs name="info-circle"></i-bs>
</a>
@if (isSet(option.key)) {
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
</button>
}
</div>
<div class="mb-n3">
@switch (option.type) {

View File

@@ -144,18 +144,4 @@ describe('ConfigComponent', () => {
component.uploadFile(new File([], 'test.png'), 'app_logo')
expect(initSpy).toHaveBeenCalled()
})
it('should reset option to null', () => {
component.configForm.patchValue({ output_type: OutputTypeConfig.PDF_A })
expect(component.isSet('output_type')).toBeTruthy()
component.resetOption('output_type')
expect(component.configForm.get('output_type').value).toBeNull()
expect(component.isSet('output_type')).toBeFalsy()
component.configForm.patchValue({ app_title: 'Test Title' })
component.resetOption('app_title')
expect(component.configForm.get('app_title').value).toBeNull()
component.configForm.patchValue({ barcodes_enabled: true })
component.resetOption('barcodes_enabled')
expect(component.configForm.get('barcodes_enabled').value).toBeNull()
})
})

View File

@@ -210,12 +210,4 @@ export class ConfigComponent
},
})
}
public isSet(key: string): boolean {
return this.configForm.get(key).value != null
}
public resetOption(key: string) {
this.configForm.get(key).setValue(null)
}
}

View File

@@ -5,13 +5,13 @@
i18n-info
>
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
<i-bs class="me-2" name="airplane"></i-bs><ng-container i18n>Start tour</ng-container>
<i-bs class="me-1" name="airplane"></i-bs>&nbsp;<ng-container i18n>Start tour</ng-container>
</button>
@if (permissionsService.isAdmin()) {
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
[disabled]="!systemStatus">
@if (!systemStatus) {
<div class="spinner-border spinner-border-sm me-2 h-75" role="status"></div>
<div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
} @else {
<i-bs class="me-2" name="card-checklist"></i-bs>
@if (systemStatusHasErrors) {
@@ -28,7 +28,7 @@
</button>
<a class="btn btn-sm btn-primary" href="admin/" target="_blank">
<ng-container i18n>Open Django Admin</ng-container>
<i-bs class="ms-2" name="arrow-up-right"></i-bs>
&nbsp;<i-bs name="arrow-up-right"></i-bs>
</a>
}
</pngx-page-header>
@@ -277,7 +277,7 @@
<div class="col">
<select class="form-select" formControlName="pdfEditorDefaultEditMode">
<option [ngValue]="PdfEditorEditMode.Create" i18n>Create new document(s)</option>
<option [ngValue]="PdfEditorEditMode.Update" i18n>Add document version</option>
<option [ngValue]="PdfEditorEditMode.Update" i18n>Update existing document</option>
</select>
</div>
</div>

View File

@@ -6,10 +6,10 @@
>
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
<i-bs name="check2-all" class="me-1"></i-bs>{{dismissButtonText}}
<i-bs name="check2-all"></i-bs>&nbsp;{{dismissButtonText}}
</button>
<div class="form-inline d-flex align-items-center">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
@@ -113,12 +113,12 @@
<td scope="row">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
<i-bs name="check"></i-bs>&nbsp;<ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document) {
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
<i-bs name="file-text" class="me-1"></i-bs><ng-container i18n>Open Document</ng-container>
<i-bs name="file-text"></i-bs>&nbsp;<ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>

View File

@@ -5,16 +5,16 @@
i18n-info
infoLink="usage/#document-trash">
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments.size === 0">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Clear selection</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments)" [disabled]="selectedDocuments.size === 0">
<i-bs name="arrow-counterclockwise" class="me-1"></i-bs><ng-container i18n>Restore selected</ng-container>
<i-bs name="arrow-counterclockwise"></i-bs>&nbsp;<ng-container i18n>Restore selected</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments)" [disabled]="selectedDocuments.size === 0">
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Delete selected</ng-container>
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Delete selected</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash.length === 0">
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Empty trash</ng-container>
<i-bs name="trash"></i-bs>&nbsp;<ng-container i18n>Empty trash</ng-container>
</button>
</pngx-page-header>
@@ -75,10 +75,10 @@
</div>
<div class="btn-group d-none d-sm-block">
<button class="btn btn-sm btn-outline-secondary" (click)="restore(document); $event.stopPropagation();">
<i-bs width="1em" height="1em" name="arrow-counterclockwise" class="me-1"></i-bs><ng-container i18n>Restore</ng-container>
<i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs>&nbsp;<ng-container i18n>Restore</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" (click)="delete(document); $event.stopPropagation();">
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</td>

View File

@@ -11,7 +11,7 @@
<h4 class="d-flex">
<ng-container i18n>Users</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add User</ng-container>
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add User</ng-container>
</button>
</h4>
<ul class="list-group">
@@ -32,10 +32,10 @@
<div class="col">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editUser(user)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.User }">
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container>
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteUser(user)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.User }">
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
@@ -49,7 +49,7 @@
<h4 class="mt-4 d-flex">
<ng-container i18n>Groups</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add Group</ng-container>
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Group</ng-container>
</button>
</h4>
<ul class="list-group">
@@ -70,10 +70,10 @@
<div class="col">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="editGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Group }">
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><ng-container i18n>Edit</ng-container>
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteGroup(group)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Group }">
<i-bs width="1em" height="1em" name="trash" class="me-1"></i-bs><ng-container i18n>Delete</ng-container>
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>

View File

@@ -86,20 +86,25 @@
<a class="nav-link" routerLink="dashboard" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Dashboard" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="house"></i-bs><span><ng-container i18n>Dashboard</ng-container></span>
<i-bs class="me-1" name="house"></i-bs><span>&nbsp;<ng-container i18n>Dashboard</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<a class="nav-link" routerLink="documents" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Documents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="files"></i-bs><span><ng-container i18n>Documents</ng-container></span>
<i-bs class="me-1" name="files"></i-bs><span>&nbsp;<ng-container i18n>Documents</ng-container></span>
</a>
</li>
</ul>
<div class="nav-group mt-3 mb-1" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
@if (savedViewService.sidebarViews?.length > 0) {
@if (savedViewService.loading) {
<h6 class="sidebar-heading px-3 text-muted">
<span i18n>Saved views</span>
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
</h6>
} @else if (savedViewService.sidebarViews?.length > 0) {
<h6 class="sidebar-heading px-3 text-muted">
<span i18n>Saved views</span>
</h6>
@@ -112,7 +117,8 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim">
<i-bs class="me-2" name="funnel"></i-bs><span><div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div>
<i-bs class="me-1" name="funnel"></i-bs>
<span>&nbsp;<div class="d-inline-flex view-name"><span class="overflow-hidden" [class.text-wrap]="!slimSidebarEnabled">{{view.name}}</span></div>
@if (showSidebarCounts && !slimSidebarEnabled) {
<span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span>
}
@@ -129,11 +135,6 @@
</li>
}
</ul>
} @else if (savedViewService.loading) {
<h6 class="sidebar-heading px-3 text-muted">
<span i18n>Saved views</span>
<div class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
</h6>
}
</div>
@@ -150,7 +151,7 @@
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="d.title | documentTitle"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim">
<i-bs class="me-2" name="file-text"></i-bs><span>{{d.title | documentTitle}}</span>
<i-bs class="me-1" name="file-text"></i-bs><span>&nbsp;{{d.title | documentTitle}}</span>
<span class="close flex-column justify-content-center" (click)="closeDocument(d); $event.preventDefault()">
<i-bs name="x"></i-bs>
</span>
@@ -162,7 +163,7 @@
<a class="nav-link app-link" [class.text-truncate]="!slimSidebarEnabled" [routerLink]="[]" (click)="closeAll()"
ngbPopover="Close all" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="x"></i-bs><span><ng-container i18n>Close all</ng-container></span>
<i-bs class="me-1" name="x"></i-bs><span>&nbsp;<ng-container i18n>Close all</ng-container></span>
</a>
</li>
}
@@ -174,65 +175,49 @@
<span i18n>Manage</span>
</h6>
<ul class="nav flex-column mb-2">
@if (canManageAttributes) {
<li class="nav-item app-link" tourAnchor="tour.tags">
<div class="d-flex align-items-center attributes-row">
<a class="nav-link flex-fill" routerLink="attributes" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Attributes" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="stack"></i-bs><span><ng-container i18n>Attributes</ng-container></span>
</a>
@if (!slimSidebarEnabled) {
<button
type="button"
class="btn btn-link btn-sm text-muted p-0 me-3 attributes-expand-btn"
(click)="toggleAttributesSections($event)"
[attr.aria-label]="attributesSectionsCollapsed ? 'Expand attributes sections' : 'Collapse attributes sections'"
i18n-aria-label
>
<i-bs [name]="attributesSectionsCollapsed ? 'plus-circle' : 'dash-circle'"></i-bs>
</button>
}
</div>
<div
class="attributes-submenu ms-2"
[ngbCollapse]="slimSidebarEnabled || attributesSectionsCollapsed"
>
<ul class="nav flex-column">
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
<a class="nav-link py-1" routerLink="attributes/tags" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="tags"></i-bs><span><ng-container i18n>Tags</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a class="nav-link py-1" routerLink="attributes/correspondents" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="person"></i-bs><span><ng-container i18n>Correspondents</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a class="nav-link py-1" routerLink="attributes/documenttypes" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="hash"></i-bs><span><ng-container i18n>Document types</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link py-1" routerLink="attributes/storagepaths" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="folder"></i-bs><span><ng-container i18n>Storage paths</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link py-1" routerLink="attributes/customfields" routerLinkActive="active" (click)="closeMenu()">
<i-bs class="me-2" name="ui-radios"></i-bs><span><ng-container i18n>Custom fields</ng-container></span>
</a>
</li>
</ul>
</div>
</li>
}
<li class="nav-item app-link"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a class="nav-link" routerLink="correspondents" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Correspondents" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="person"></i-bs><span>&nbsp;<ng-container i18n>Correspondents</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"
tourAnchor="tour.tags">
<a class="nav-link" routerLink="tags" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Tags"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="tags"></i-bs><span>&nbsp;<ng-container i18n>Tags</ng-container></span>
</a>
</li>
<li class="nav-item app-link"
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Document Types" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="hash"></i-bs><span>&nbsp;<ng-container i18n>Document Types</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Storage Paths" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="folder"></i-bs><span>&nbsp;<ng-container i18n>Storage Paths</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Custom Fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-1" name="ui-radios"></i-bs><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<a class="nav-link" routerLink="savedviews" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Saved Views" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="window-stack"></i-bs><span><ng-container i18n>Saved Views</ng-container></span>
<i-bs class="me-1" name="window-stack"></i-bs><span>&nbsp;<ng-container i18n>Saved Views</ng-container></span>
</a>
</li>
<li class="nav-item app-link"
@@ -241,7 +226,7 @@
<a class="nav-link" routerLink="workflows" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Workflows" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="boxes"></i-bs><span><ng-container i18n>Workflows</ng-container></span>
<i-bs class="me-1" name="boxes"></i-bs><span>&nbsp;<ng-container i18n>Workflows</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.MailAccount }"
@@ -249,14 +234,14 @@
<a class="nav-link" routerLink="mail" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Mail"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="envelope"></i-bs><span><ng-container i18n>Mail</ng-container></span>
<i-bs class="me-1" name="envelope"></i-bs><span>&nbsp;<ng-container i18n>Mail</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
<a class="nav-link" routerLink="trash" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Trash"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="trash"></i-bs><span><ng-container i18n>Trash</ng-container></span>
<i-bs class="me-1" name="trash"></i-bs><span>&nbsp;<ng-container i18n>Trash</ng-container></span>
</a>
</li>
</ul>
@@ -272,21 +257,21 @@
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Settings" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="gear"></i-bs><span><ng-container i18n>Settings</ng-container></span>
<i-bs class="me-1" name="gear"></i-bs><span>&nbsp;<ng-container i18n>Settings</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.AppConfig }">
<a class="nav-link" routerLink="config" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Configuration" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="sliders2-vertical"></i-bs><span><ng-container i18n>Configuration</ng-container></span>
<i-bs class="me-1" name="sliders2-vertical"></i-bs><span>&nbsp;<ng-container i18n>Configuration</ng-container></span>
</a>
</li>
<li class="nav-item app-link" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.User }">
<a class="nav-link" routerLink="usersgroups" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Users & Groups" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="people"></i-bs><span><ng-container i18n>Users & Groups</ng-container></span>
<i-bs class="me-1" name="people"></i-bs><span>&nbsp;<ng-container i18n>Users & Groups</ng-container></span>
</a>
</li>
<li class="nav-item app-link"
@@ -295,7 +280,7 @@
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="list-task"></i-bs><span><ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
<i-bs class="me-1" name="list-task"></i-bs><span>&nbsp;<ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2 d-inline">{{tasksService.failedFileTasks.length}}</span></span>
}</span>
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
@@ -308,7 +293,7 @@
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="text-left"></i-bs><span><ng-container i18n>Logs</ng-container></span>
<i-bs class="me-1" name="text-left"></i-bs><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
</a>
</li>
}
@@ -317,7 +302,7 @@
target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body"
triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="d-flex me-2" name="question-circle"></i-bs><span><ng-container i18n>Documentation</ng-container></span>
<i-bs class="d-flex" name="question-circle"></i-bs><span class="ms-1">&nbsp;<ng-container i18n>Documentation</ng-container></span>
</a>
</li>
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
@@ -356,9 +341,9 @@
href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
container="body">
<i-bs width="1.2em" height="1.2em" name="info-circle" class="me-1"></i-bs>
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
@if (appRemoteVersion?.update_available) {
<ng-container i18n>Update available</ng-container>
&nbsp;<ng-container i18n>Update available</ng-container>
}
</a>
}

View File

@@ -177,15 +177,6 @@ main {
}
}
.attributes-row .attributes-expand-btn {
opacity: 0.2;
transition: opacity 0.15s ease-in-out;
}
.attributes-row:hover .attributes-expand-btn {
opacity: 1;
}
.sidebar-heading {
font-size: 0.75rem;
text-transform: uppercase;
@@ -290,7 +281,7 @@ main {
.navbar .dropdown-menu {
font-size: 0.875rem; // body size
a i-bs, button i-bs {
a i-bs {
opacity: 0.6;
}
}

View File

@@ -28,10 +28,7 @@ import {
DjangoMessagesService,
} from 'src/app/services/django-messages.service'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import {
PermissionType,
PermissionsService,
} from 'src/app/services/permissions.service'
import { PermissionsService } from 'src/app/services/permissions.service'
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service'
@@ -243,19 +240,9 @@ describe('AppFrameComponent', () => {
it('should support toggling slim sidebar and saving', fakeAsync(() => {
const saveSettingSpy = jest.spyOn(settingsService, 'set')
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
expect(component.slimSidebarEnabled).toBeFalsy()
expect(component.slimSidebarAnimating).toBeFalsy()
component.toggleSlimSidebar()
const requests = httpTestingController.match(
`${environment.apiBaseUrl}ui_settings/`
)
expect(requests).toHaveLength(1)
expect(requests[0].request.body.settings.slim_sidebar).toBe(true)
expect(
requests[0].request.body.settings.attributes_sections_collapsed
).toEqual(['attributes'])
requests[0].flush({ success: true })
expect(component.slimSidebarAnimating).toBeTruthy()
tick(200)
expect(component.slimSidebarAnimating).toBeFalsy()
@@ -264,10 +251,6 @@ describe('AppFrameComponent', () => {
SETTINGS_KEYS.SLIM_SIDEBAR,
true
)
expect(saveSettingSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
['attributes']
)
}))
it('should show error on toggle slim sidebar if store settings fails', () => {
@@ -275,7 +258,7 @@ describe('AppFrameComponent', () => {
const toastSpy = jest.spyOn(toastService, 'showError')
component.toggleSlimSidebar()
httpTestingController
.match(`${environment.apiBaseUrl}ui_settings/`)[0]
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush('error', {
status: 500,
statusText: 'error',
@@ -390,103 +373,4 @@ describe('AppFrameComponent', () => {
it('should call maybeRefreshDocumentCounts after saved views reload', () => {
expect(maybeRefreshSpy).toHaveBeenCalled()
})
it('should indicate attributes management availability when any permission is granted', () => {
jest
.spyOn(permissionsService, 'currentUserCan')
.mockImplementation((action, type) => {
return type === PermissionType.Tag
})
expect(component.canManageAttributes).toBe(true)
})
it('should indicate attributes management availability for other permission types', () => {
const canSpy = jest
.spyOn(permissionsService, 'currentUserCan')
.mockImplementation((action, type) => {
return type === PermissionType.Correspondent
})
expect(component.canManageAttributes).toBe(true)
canSpy.mockImplementation((action, type) => {
return type === PermissionType.DocumentType
})
expect(component.canManageAttributes).toBe(true)
canSpy.mockImplementation((action, type) => {
return type === PermissionType.StoragePath
})
expect(component.canManageAttributes).toBe(true)
canSpy.mockImplementation((action, type) => {
return type === PermissionType.CustomField
})
expect(component.canManageAttributes).toBe(true)
})
it('should toggle attributes sections and stop event bubbling', () => {
const preventDefault = jest.fn()
const stopPropagation = jest.fn()
const setSpy = jest.spyOn(settingsService, 'set')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.toggleAttributesSections({
preventDefault,
stopPropagation,
} as any)
expect(preventDefault).toHaveBeenCalled()
expect(stopPropagation).toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
['attributes']
)
})
it('should show error when saving slim sidebar setting fails', () => {
const toastSpy = jest.spyOn(toastService, 'showError')
jest.spyOn(console, 'warn').mockImplementation(() => {})
jest
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('boom')))
component.slimSidebarEnabled = true
expect(toastSpy).toHaveBeenCalled()
})
it('should show error when saving attributes collapsed setting fails', () => {
const toastSpy = jest.spyOn(toastService, 'showError')
jest.spyOn(console, 'warn').mockImplementation(() => {})
jest
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('boom')))
component.attributesSectionsCollapsed = true
expect(toastSpy).toHaveBeenCalled()
})
it('should persist attributes section collapse state', () => {
const setSpy = jest.spyOn(settingsService, 'set')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.attributesSectionsCollapsed = true
expect(setSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
['attributes']
)
})
it('should collapse attributes sections when enabling slim sidebar', () => {
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [])
settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, false)
component.toggleSlimSidebar()
expect(component.attributesSectionsCollapsed).toBe(true)
})
})

View File

@@ -21,7 +21,7 @@ import { Observable } from 'rxjs'
import { first } from 'rxjs/operators'
import { Document } from 'src/app/data/document'
import { SavedView } from 'src/app/data/saved-view'
import { CollapsibleSection, SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
@@ -140,35 +140,12 @@ export class AppFrameComponent
toggleSlimSidebar(): void {
this.slimSidebarAnimating = true
const slimSidebarEnabled = !this.slimSidebarEnabled
this.settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, slimSidebarEnabled)
if (slimSidebarEnabled) {
this.settingsService.set(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED, [
CollapsibleSection.ATTRIBUTES,
])
}
this.settingsService
.storeSettings()
.pipe(first())
.subscribe({
error: (error) => {
this.toastService.showError(
$localize`An error occurred while saving settings.`
)
console.warn(error)
},
})
this.slimSidebarEnabled = !this.slimSidebarEnabled
setTimeout(() => {
this.slimSidebarAnimating = false
}, 200) // slightly longer than css animation for slim sidebar
}
toggleAttributesSections(event?: Event): void {
event?.preventDefault()
event?.stopPropagation()
this.attributesSectionsCollapsed = !this.attributesSectionsCollapsed
}
get versionString(): string {
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
}
@@ -190,31 +167,6 @@ export class AppFrameComponent
)
}
get canManageAttributes(): boolean {
return (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.Tag
) ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.Correspondent
) ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.DocumentType
) ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.StoragePath
) ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.CustomField
)
)
}
get slimSidebarEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
}
@@ -234,31 +186,6 @@ export class AppFrameComponent
})
}
get attributesSectionsCollapsed(): boolean {
return this.settingsService
.get(SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED)
?.includes(CollapsibleSection.ATTRIBUTES)
}
set attributesSectionsCollapsed(collapsed: boolean) {
// TODO: refactor to be able to toggle individual sections, if implemented
this.settingsService.set(
SETTINGS_KEYS.ATTRIBUTES_SECTIONS_COLLAPSED,
collapsed ? [CollapsibleSection.ATTRIBUTES] : []
)
this.settingsService
.storeSettings()
.pipe(first())
.subscribe({
error: (error) => {
this.toastService.showError(
$localize`An error occurred while saving settings.`
)
console.warn(error)
},
})
}
get aiEnabled(): boolean {
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
}

View File

@@ -49,13 +49,17 @@
[disabled]="disablePrimaryButton(type, item)"
(mouseenter)="onButtonHover($event)">
@if (type === DataType.Document) {
<i-bs width="1em" height="1em" name="file-earmark-richtext" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
<i-bs width="1em" height="1em" name="file-earmark-richtext"></i-bs>
<span>&nbsp;<ng-container i18n>Open</ng-container></span>
} @else if (type === DataType.SavedView) {
<i-bs width="1em" height="1em" name="eye" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
<i-bs width="1em" height="1em" name="eye"></i-bs>
<span>&nbsp;<ng-container i18n>Open</ng-container></span>
} @else if (type === DataType.Workflow || type === DataType.CustomField || type === DataType.Group || type === DataType.User || type === DataType.MailAccount || type === DataType.MailRule) {
<i-bs width="1em" height="1em" name="pencil" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
<i-bs width="1em" height="1em" name="pencil"></i-bs>
<span>&nbsp;<ng-container i18n>Open</ng-container></span>
} @else {
<i-bs width="1em" height="1em" name="filter" class="me-1"></i-bs><span><ng-container i18n>Filter documents</ng-container></span>
<i-bs width="1em" height="1em" name="filter"></i-bs>
<span>&nbsp;<ng-container i18n>Filter documents</ng-container></span>
}
</button>
@if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) {
@@ -65,9 +69,11 @@
[disabled]="disableSecondaryButton(type, item)"
(mouseenter)="onButtonHover($event)">
@if (type === DataType.Document) {
<i-bs width="1em" height="1em" name="download" class="me-1"></i-bs><span><ng-container i18n>Download</ng-container></span>
<i-bs width="1em" height="1em" name="download"></i-bs>
<span>&nbsp;<ng-container i18n>Download</ng-container></span>
} @else {
<i-bs width="1em" height="1em" name="file-earmark-richtext" class="me-1"></i-bs><span><ng-container i18n>Open</ng-container></span>
<i-bs width="1em" height="1em" name="file-earmark-richtext"></i-bs>
<span>&nbsp;<ng-container i18n>Open</ng-container></span>
}
</button>
}

View File

@@ -10,22 +10,10 @@
<ul class="list-group"
cdkDropList
(cdkDropListDropped)="onDrop($event)">
@for (document of documents; track document.id) {
<li class="list-group-item d-flex align-items-center" cdkDrag>
@for (documentID of documentIDs; track documentID) {
<li class="list-group-item" cdkDrag>
<i-bs name="grip-vertical" class="me-2"></i-bs>
<div class="d-flex flex-column">
<div>
@if (document.correspondent) {
<b>{{document.correspondent | correspondentName | async}}: </b>
}{{document.title}}
</div>
<small class="text-muted">
{{document.created | customDate:'mediumDate'}}
@if (document.page_count) {
| {document.page_count, plural, =1 {One page} other {{{document.page_count}} pages}}
}
</small>
</div>
{{getDocument(documentID)?.title}}
</li>
}
</ul>

View File

@@ -3,14 +3,11 @@ import {
DragDropModule,
moveItemInArray,
} from '@angular/cdk/drag-drop'
import { AsyncPipe } from '@angular/common'
import { Component, OnInit, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { takeUntil } from 'rxjs'
import { Document } from 'src/app/data/document'
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@@ -20,9 +17,6 @@ import { ConfirmDialogComponent } from '../confirm-dialog.component'
templateUrl: './merge-confirm-dialog.component.html',
styleUrl: './merge-confirm-dialog.component.scss',
imports: [
AsyncPipe,
CorrespondentNamePipe,
CustomDatePipe,
DragDropModule,
FormsModule,
ReactiveFormsModule,

View File

@@ -20,7 +20,7 @@
@for (docId of value; track docId) {
@if (getDocumentTitle(docId)) {
<a routerLink="/documents/{{docId}}" class="badge bg-body text-primary" title="View" i18n-title>
<i-bs width="0.9em" height="0.9em" name="file-text" class="me-1"></i-bs><span>{{ getDocumentTitle(docId) }}</span>
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<span>{{ getDocumentTitle(docId) }}</span>
</a>
}
}

View File

@@ -1,6 +1,7 @@
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)" [popperOptions]="popperOptions">
<button type="button" class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
<i-bs name="ui-radios"></i-bs><div class="d-none d-lg-inline ms-1"><ng-container i18n>Custom Fields</ng-container></div>
<i-bs name="ui-radios"></i-bs>
<div class="d-none d-lg-inline">&nbsp;<ng-container i18n>Custom Fields</ng-container></div>
</button>
<div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
<div class="list-group list-group-flush" (keydown)="listKeyDown($event)">
@@ -17,7 +18,7 @@
@if (!filterText?.length || filteredFields.length === 0) {
<button class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
<small>
<i-bs width=".9em" height=".9em" name="asterisk" class="me-1"></i-bs><ng-container i18n>Create new field</ng-container>
<i-bs width=".9em" height=".9em" name="asterisk"></i-bs>&nbsp;<ng-container i18n>Create new field</ng-container>
</small>
</button>
}

View File

@@ -1,7 +1,8 @@
@if (useDropdown) {
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
<button class="btn btn-sm btn-outline-primary" id="dropdown_toggle" ngbDropdownToggle [disabled]="disabled">
<i-bs name="{{icon}}"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
<i-bs name="{{icon}}"></i-bs>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
@if (isActive) {
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
}

View File

@@ -1,6 +1,7 @@
<div class="btn-group w-100" ngbDropdown role="group" [popperOptions]="popperOptions" [placement]="placement">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="createdDateTo || createdDateFrom ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
<i-bs width="1em" height="1em" name="calendar-event-fill"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
<i-bs width="1em" height="1em" name="calendar-event-fill"></i-bs>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
</button>
<div class="dropdown-menu date-dropdown shadow p-2" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">

View File

@@ -17,7 +17,7 @@
@switch (objectForm.get('data_type').value) {
@case (CustomFieldDataType.Select) {
<button type="button" class="btn btn-sm btn-primary my-2" (click)="addSelectOption()">
<span i18n>Add option</span><i-bs class="ms-1" name="plus-circle"></i-bs>
<span i18n>Add option</span>&nbsp;<i-bs name="plus-circle"></i-bs>
</button>
<div formArrayName="select_options">
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {

View File

@@ -31,8 +31,8 @@ export enum EditDialogMode {
@Directive()
export abstract class EditDialogComponent<
T extends ObjectWithPermissions | ObjectWithId,
>
T extends ObjectWithPermissions | ObjectWithId,
>
extends LoadingComponentWithPermissions
implements OnInit
{

View File

@@ -9,24 +9,19 @@
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="col-md-4">
<pngx-input-text [horizontal]="true" i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
</div>
<div class="col-md-4">
<div class="col-md-3">
<pngx-input-select [horizontal]="true" i18n-title title="Account" [items]="accounts" formControlName="account"></pngx-input-select>
</div>
<div class="col-md-3">
<pngx-input-number [horizontal]="true" i18n-title title="Order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
</div>
<div class="col-md-2 pt-2">
<pngx-input-switch [horizontal]="true" i18n-title title="Enabled" formControlName="enabled"></pngx-input-switch>
</div>
</div>
<div class="row">
<div class="col-md-6">
<pngx-input-number [horizontal]="true" i18n-title title="Order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number>
</div>
<div class="col-md-6">
<pngx-input-switch [horizontal]="true" i18n-title title="Stop further processing" formControlName="stop_processing" i18n-hint hint="Stop processing further rules if this rule queues any document(s)."></pngx-input-switch>
</div>
</div>
<hr class="mt-0"/>
<div class="row">
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the criteria specified below.</p>

View File

@@ -222,7 +222,6 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
),
assign_correspondent: new FormControl(null),
assign_owner_from_rule: new FormControl(true),
stop_processing: new FormControl(false),
})
}

View File

@@ -30,7 +30,7 @@
<div class="d-flex">
<p class="p-2" i18n>Trigger Workflow On:</p>
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addTrigger()">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add Trigger</ng-container>
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Trigger</ng-container>
</button>
</div>
<div ngbAccordion [closeOthers]="true">
@@ -72,7 +72,7 @@
<div class="d-flex">
<p class="p-2" i18n>Apply Actions:</p>
<button type="button" class="btn btn-sm btn-outline-primary ms-auto mb-3" (click)="addAction()">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add Action</ng-container>
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add Action</ng-container>
</button>
</div>
<div ngbAccordion [closeOthers]="true" cdkDropList (cdkDropListDropped)="onActionDrop($event)">
@@ -187,7 +187,7 @@
(click)="addFilter(formGroup)"
[disabled]="!canAddFilter(formGroup)"
>
<i-bs name="plus-circle" class="me-1"></i-bs><span i18n>Add filter</span>
<i-bs name="plus-circle"></i-bs>&nbsp;<span i18n>Add filter</span>
</button>
</div>
<ul class="mt-2 list-group filters" formArrayName="filters">
@@ -448,13 +448,6 @@
</div>
</div>
}
@case (WorkflowActionType.MoveToTrash) {
<div class="row">
<div class="col">
<p class="text-muted small" i18n>The document will be moved to the trash at the end of the workflow run.</p>
</div>
</div>
}
}
</div>
</ng-template>

View File

@@ -143,10 +143,6 @@ export const WORKFLOW_ACTION_OPTIONS = [
id: WorkflowActionType.PasswordRemoval,
name: $localize`Password removal`,
},
{
id: WorkflowActionType.MoveToTrash,
name: $localize`Move to trash`,
},
]
export enum TriggerFilterType {

View File

@@ -1,6 +1,7 @@
<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)" [popperOptions]="popperOptions">
<button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
<i-bs name="{{icon}}"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
<i-bs name="{{icon}}"></i-bs>
<div class="d-none d-sm-inline">&nbsp;{{title}}</div>
@if (!editing && selectionModel.totalCount > 0) {
<pngx-clearable-badge [number]="selectionModel.totalCount" [selected]="selectionModel.selectionSize() > 0" (cleared)="reset()"></pngx-clearable-badge>
}

View File

@@ -5,7 +5,7 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -4,7 +4,7 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -9,7 +9,7 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
@@ -44,11 +44,11 @@
}
@if (document.title) {
<a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();" title="Open link" i18n-title>
<i-bs width="0.9em" height="0.9em" name="file-text" class="me-1"></i-bs><span>{{document.title}}</span>
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<span>{{document.title}}</span>
</a>
} @else {
<span class="badge bg-light text-muted" (click)="unselect(document)" (mousedown)="$event.stopImmediatePropagation()" type="button" title="Remove link" i18n-title>
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle-fill" class="me-1"></i-bs><span i18n>Not found</span>
<i-bs width="0.9em" height="0.9em" name="exclamation-triangle-fill"></i-bs>&nbsp;<span i18n>Not found</span>
</span>
}
</div>

View File

@@ -5,7 +5,7 @@
<label class="form-label mb-0" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
}
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="addEntry()">
<i-bs name="plus-circle" class="me-1"></i-bs><ng-container i18n>Add</ng-container>
<i-bs name="plus-circle"></i-bs>&nbsp;<ng-container i18n>Add</ng-container>
</button>
</div>
<div class="position-relative">

View File

@@ -6,7 +6,7 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -6,7 +6,7 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -6,7 +6,7 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -7,7 +7,7 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -10,7 +10,7 @@
</label>
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>
@@ -22,7 +22,7 @@
<label class="form-check-label" [class.text-muted]="showUnsetNote && isUnset" [for]="inputId" [ngbTooltip]="showUnsetNote && isUnset ? tipContent: null" placement="end">
{{title}}
@if (showUnsetNote && isUnset) {
<i-bs class="ms-1" width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
&nbsp;<i-bs width="0.9em" height="0.9em" name="exclamation-triangle"></i-bs>
}
</label>
}

View File

@@ -6,7 +6,7 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -6,7 +6,7 @@
}
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -4,7 +4,7 @@
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
@if (removable) {
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Remove</ng-container>
<i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
</button>
}
</div>

View File

@@ -5,7 +5,7 @@
@if (id) {
<span class="badge bg-primary text-primary-text-contrast ms-3 small fs-normal cursor-pointer" (click)="copyID()">
@if (copied) {
<i-bs width="1em" height="1em" name="clipboard-check" class="me-1"></i-bs><ng-container i18n>Copied!</ng-container>
<i-bs width="1em" height="1em" name="clipboard-check"></i-bs>&nbsp;<ng-container i18n>Copied!</ng-container>
} @else {
ID: {{id}}
}

View File

@@ -84,7 +84,7 @@
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="PdfEditorEditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()">
<label for="editModeUpdate" class="btn btn-outline-primary btn-sm">
<i-bs name="pencil"></i-bs>
<span class="form-check-label ms-2" i18n>Add document version</span>
<span class="form-check-label ms-2" i18n>Update existing document</span>
</label>
</div>
@if (editMode === PdfEditorEditMode.Create) {

View File

@@ -3,7 +3,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { DocumentService } from 'src/app/services/rest/document.service'
import { PDFEditorComponent } from './pdf-editor.component'
describe('PDFEditorComponent', () => {
@@ -140,16 +139,4 @@ describe('PDFEditorComponent', () => {
expect(component.pages[1].page).toBe(2)
expect(component.pages[2].page).toBe(3)
})
it('should include selected version in preview source when provided', () => {
const documentService = TestBed.inject(DocumentService)
const previewSpy = jest
.spyOn(documentService, 'getPreviewUrl')
.mockReturnValue('preview-version')
component.documentID = 3
component.versionID = 10
expect(component.pdfSrc).toBe('preview-version')
expect(previewSpy).toHaveBeenCalledWith(3, false, 10)
})
})

View File

@@ -46,7 +46,6 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
activeModal: NgbActiveModal = inject(NgbActiveModal)
documentID: number
versionID?: number
pages: PageOperation[] = []
totalPages = 0
editMode: PdfEditorEditMode = this.settingsService.get(
@@ -56,11 +55,7 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
includeMetadata: boolean = true
get pdfSrc(): string {
return this.documentService.getPreviewUrl(
this.documentID,
false,
this.versionID
)
return this.documentService.getPreviewUrl(this.documentID)
}
pdfLoaded(pdf: PngxPdfDocumentProxy) {

View File

@@ -150,8 +150,4 @@
position: absolute;
inset: 0;
pointer-events: none;
& .annotationTextContent {
opacity: 0;
}
}

Some files were not shown because too many files have changed in this diff Show More